Regarding the recent updates in Ladybug Tools 1.9.0, I’d like to express my sincere gratitude, as always, to the development team [@mostapha , @chris , @MingboPeng and others] for their continued dedication.
Due to some recent project needs, I made a few minor edits to two components for personal use: LB Create Legend and LB Preview VisualizationSet. I’d like to share the rationale and summary of these edits below:
LB Create Legend
In many of my works, I often have to generate multiple graphical outputs with different legends placed in various positions and across different viewports. While some of these visuals are produced directly via Ladybug Tools and others manually, “LB Create Legend” has been especially helpful for handling these legends flexibly.
However, because of the large number of viewports, I wondered if this component could support a viewport_
input, just like LB Preview VisualizationSet, to control which viewports the legend appears in. Additionally, I noticed that while the HUD responds correctly to the component’s Preview on/off state, it still displays when the component is Locked (Disabled).
To address these issues, I made a few targeted edits, which are marked with comments in the code.
Args:
title_: ✅ A text string representing a legend title. Legends are usually
titled with the units of the data.
from ghpythonlib.componentbase import executingcomponent as component
import Grasshopper, GhPython
import System
import Rhino
import rhinoscriptsyntax as rs
class MyComponent(component):
def __init__(self):
super(MyComponent,self).__init__()
self.draw_2d_text = None
self.draw_sprite = None
self.colored_mesh = None
self.viewport = None # ✅ New: Store target viewport
def RunScript(self, _values, _base_plane_, title_, legend_par_, leg_par2d_, viewport_):
ghenv.Component.Name = "LB Create Legend"
ghenv.Component.NickName = 'CreateLegend'
ghenv.Component.Message = '1.9.0'
ghenv.Component.Category = 'Ladybug'
ghenv.Component.SubCategory = '4 :: Extra'
ghenv.Component.AdditionalHelpFromDocStrings = '0'
try:
from ladybug.legend import Legend, LegendParameters
except ImportError as e:
raise ImportError('\nFailed to import ladybug:\n\t{}'.format(e))
try:
from ladybug_rhino.togeometry import to_plane
from ladybug_rhino.fromobjects import legend_objects
from ladybug_rhino.color import color_to_color
from ladybug_rhino.preview import VisualizationSetConverter
from ladybug_rhino.grasshopper import all_required_inputs
except ImportError as e:
raise ImportError('\nFailed to import ladybug_rhino:\n\t{}'.format(e))
if all_required_inputs(ghenv.Component):
# set default values
legend_par_ = legend_par_.duplicate() if legend_par_ is not None else \
LegendParameters()
if _base_plane_:
legend_par_.base_plane = to_plane(_base_plane_)
legend_par_.title = title_
# create the legend
values = []
for val in _values:
try:
values.append(float(val))
except AttributeError: # assume it's a data collection
values.extend(val.values)
legend = Legend(values, legend_par_)
colors = [color_to_color(col) for col in legend.value_colors]
label_text = legend.segment_text
if leg_par2d_ is None: # output a 3D legend
self.draw_2d_text, self.draw_sprite = None, None
rhino_objs = legend_objects(legend)
mesh = rhino_objs[0]
title_obj = rhino_objs[1]
label_objs = rhino_objs[2:]
else: # output a 2D legend that is oriented to the screen
mesh, title_obj, label_objs= None, None, None
legend.legend_parameters.properties_2d = leg_par2d_
d_sprite, self.draw_2d_text = \
VisualizationSetConverter.convert_legend2d(legend)
self.draw_sprite = [d_sprite]
else:
mesh, title_obj, label_objs, label_text, colors = \
None, None, None, None, None
self.draw_2d_text, self.draw_sprite = None, None
self.viewport = None # ✅ Ensure initialization even if skipping main logic
# return outputs if you have them; here I try it for you
self.colored_mesh = mesh
self.viewport = viewport_
return (mesh, title_obj, label_objs, label_text, colors)
def DrawViewportMeshes(self, args):
try:
# ✅ Skip drawing if component is disabled (right-click > disable)
if ghenv.Component.Locked:
return
# get the DisplayPipeline from the event arguments
display = args.Display
# ✅ Graphics also filtered by the input viewport_
if self.viewport is None or \
self.viewport.lower() == args.Viewport.Name.lower():
# draw the objects in the scene
if self.colored_mesh is not None:
display.DrawMeshFalseColors(self.colored_mesh)
if self.draw_2d_text is not None:
for draw_args in self.draw_2d_text:
display.Draw2dText(*draw_args)
if self.draw_sprite is not None:
for draw_args in self.draw_sprite:
display.DrawSprite(*draw_args)
except Exception, e:
System.Windows.Forms.MessageBox.Show(str(e), "script error")
def get_ClippingBox(self):
return Rhino.Geometry.BoundingBox()
LB Preview VisualizationSet
While modifying the LB Create Legend component, I realized that LB Preview VisualizationSet already includes a complete display logic — including support for leg_par_
, leg_par2d_
, and viewport_
. So I wondered if the component could also support HUD-only rendering when _vis_set
is not connected.
However, I quickly discovered that without geometry, the script cannot generate a valid vis_set_obj
. To solve this, I implemented a workaround using a dummy vis_set_obj
, which enables the script to run even without _vis_set
input, while also ensuring the dummy geometry does not render in DrawViewportMeshes
.
Also, just like LB Create Legend, I noticed that HUD elements were still being displayed even when the component is Locked (Disabled), so I made adjustments to respect this state as well. All the modifications I made are marked with comments in the script.
Args:
leg_title_: ✅ A text string representing a legend title. Legends are usually
titled with the units of the data. If _vis_set is connected, its own title
will be used instead.
from ghpythonlib.componentbase import executingcomponent as component
import Grasshopper, GhPython
import System
import Rhino
import rhinoscriptsyntax as rs
class MyComponent(component):
def __init__(self):
super(MyComponent,self).__init__()
self.vis_con = None
self.vs_goo = None
self.viewport = None
def RunScript(self, _vis_set, legend_par_, leg_par2d_, leg_title_, data_set_, viewport_):
ghenv.Component.Name = 'LB Preview VisualizationSet'
ghenv.Component.NickName = 'VisSet'
ghenv.Component.Message = '1.9.0'
ghenv.Component.Category = 'Ladybug'
ghenv.Component.SubCategory = '4 :: Extra'
ghenv.Component.AdditionalHelpFromDocStrings = '1'
try:
from ladybug_display.visualization import VisualizationSet, AnalysisGeometry
except ImportError as e:
raise ImportError('\nFailed to import ladybug_display:\n\t{}'.format(e))
try:
from ladybug_rhino.grasshopper import all_required_inputs, objectify_output
from ladybug_rhino.preview import VisualizationSetConverter
from ladybug_rhino.visset import VisSetGoo, process_vis_set
except ImportError as e:
raise ImportError('\nFailed to import ladybug_rhino:\n\t{}'.format(e))
from ladybug_display.visualization import VisualizationSet, AnalysisGeometry, VisualizationData
from ladybug.legend import LegendParameters, Legend2DParameters
from ladybug.datatype.generic import GenericType
from ladybug_geometry.geometry3d import Point3D, Face3D
from ladybug_geometry.geometry2d import Point2D
vis_set_obj = None
leg3d, leg2d = True, False # ✅ Default to show 3D legend
if leg_par2d_ is not None:
leg3d, leg2d = False, True # ✅ If leg_par2d_ is specified, enable HUD mode
if _vis_set:
# ✅ Original handling: if _vis_set is present, use old process
self.only_legend_mode = False
if len(_vis_set) == 1:
vis_set = _vis_set[0]
else:
vis_objs = []
for vis_obj in _vis_set:
if isinstance(vis_obj, VisualizationSet):
vis_objs.append([vis_obj])
elif hasattr(vis_obj, 'data'):
arr_type = (list, tuple)
if isinstance(vis_obj.data, arr_type) and \
isinstance(vis_obj.data[0], arr_type):
for v_obj in vis_obj.data:
vis_objs.append(v_obj)
else:
vis_objs.append(vis_obj.data)
vis_set = objectify_output('Multiple Vis Sets', vis_objs)
vis_set_obj = process_vis_set(vis_set)
# ✅ Apply 3D legend_par_
if legend_par_ is not None:
for geo in vis_set_obj:
if isinstance(geo, AnalysisGeometry):
for data in geo.data_sets:
if legend_par_.min is not None:
data.legend_parameters.min = legend_par_.min
if legend_par_.max is not None:
data.legend_parameters.max = legend_par_.max
if not legend_par_.are_colors_default:
data.legend_parameters.colors = legend_par_.colors
if not legend_par_.is_segment_count_default:
data.legend_parameters.segment_count = \
legend_par_.segment_count
if data.data_type is not None:
unit = data.unit if data.unit else data.data_type.units[0]
data.legend_parameters.title = \
'{} ({})'.format(data.data_type.name, unit) \
if not legend_par_.vertical else unit
data.legend_parameters.continuous_legend = \
legend_par_.continuous_legend
data.legend_parameters.decimal_count = \
legend_par_.decimal_count
data.legend_parameters.include_larger_smaller = \
legend_par_.include_larger_smaller
data.legend_parameters.font = legend_par_.font
data.legend_parameters.vertical = legend_par_.vertical
if not legend_par_.is_base_plane_default:
data.legend_parameters.base_plane = \
legend_par_.base_plane
if not legend_par_.is_segment_height_default:
data.legend_parameters.segment_height = \
legend_par_.segment_height
if not legend_par_.is_segment_width_default:
data.legend_parameters.segment_width = \
legend_par_.segment_width
if not legend_par_.is_text_height_default:
data.legend_parameters.text_height = \
legend_par_.text_height
# ✅ Apply 2D HUD legend
if leg_par2d_ is not None:
for geo in vis_set_obj:
if isinstance(geo, AnalysisGeometry):
for data in geo.data_sets:
data.legend_parameters.properties_2d = leg_par2d_
# ✅ No _vis_set but has legend input: only show legend
elif not _vis_set and (legend_par_ is not None or leg_par2d_ is not None):
self.only_legend_mode = True
from ladybug.legend import LegendParameters
from ladybug_display.visualization import VisualizationSet
from ladybug_geometry.geometry3d import Face3D, Point3D
# ✅ Create dummy geometry and data
dummy_face = Face3D([
Point3D(-1, 0, 0),
Point3D(0, 0, 0),
Point3D(0, 1, 0),
Point3D(-1, 1, 0)
])
dummy_values = [0.0]
# ✅ Handle legend_par
if legend_par_ is None:
legend_par_ = LegendParameters()
if leg_par2d_ is not None:
legend_par_.properties_2d = leg_par2d_
legend_par_.title = leg_title_ # ✅ Custom title
# ✅ Official recommended method: create vis_set
vis_set_obj = VisualizationSet.from_single_analysis_geo(
'OnlyLegend', [dummy_face], dummy_values, legend_par_
)
# ✅ Final step: build VisualizationSetConverter (regardless of vis_set presence)
if vis_set_obj is not None:
self.vis_con = VisualizationSetConverter(vis_set_obj, leg3d, leg2d)
self.vs_goo = VisSetGoo(vis_set_obj)
self.viewport = viewport_
else:
self.vis_con = None
self.vs_goo = None
self.viewport = None
# return the bake-able version of the visualization set
return self.vs_goo
def DrawViewportWires(self, args):
try:
# ✅ If component is disabled (via right-click), skip drawing
if ghenv.Component.Locked:
return
if self.vis_con is not None:
# get the DisplayPipeline from the event arguments
display = args.Display
# for each object to be rendered, pass the drawing arguments
for draw_args in self.vis_con.draw_3d_text:
display.Draw3dText(*draw_args)
for draw_args in self.vis_con.draw_mesh_wires:
display.DrawMeshWires(*draw_args)
for draw_args in self.vis_con.draw_mesh_vertices:
display.DrawMeshVertices(*draw_args)
for draw_args in self.vis_con.draw_point:
display.DrawPoint(*draw_args)
for draw_args in self.vis_con.draw_arrow:
display.DrawArrow(*draw_args)
for draw_args in self.vis_con.draw_brep_wires:
display.DrawBrepWires(*draw_args)
for draw_args in self.vis_con.draw_line:
display.DrawLine(*draw_args)
for draw_args in self.vis_con.draw_patterned_line:
display.DrawPatternedLine(*draw_args)
for draw_args in self.vis_con.draw_patterned_polyline:
display.DrawPatternedPolyline(*draw_args)
for draw_args in self.vis_con.draw_curve:
display.DrawCurve(*draw_args)
for draw_args in self.vis_con.draw_circle:
display.DrawCircle(*draw_args)
for draw_args in self.vis_con.draw_arc:
display.DrawArc(*draw_args)
for draw_args in self.vis_con.draw_sphere:
display.DrawSphere(*draw_args)
for draw_args in self.vis_con.draw_cone:
display.DrawCone(*draw_args)
for draw_args in self.vis_con.draw_cylinder:
display.DrawCylinder(*draw_args)
if self.viewport is None or \
self.viewport.lower() == args.Viewport.Name.lower():
for draw_args in self.vis_con.draw_2d_text:
display.Draw2dText(*draw_args)
for draw_args in self.vis_con.draw_sprite:
display.DrawSprite(*draw_args)
except Exception, e:
System.Windows.Forms.MessageBox.Show(str(e), "script error")
def DrawViewportMeshes(self, args):
try:
# ✅ If component is disabled (via right-click), skip drawing
if ghenv.Component.Locked:
return
if self.vis_con is not None:
# get the DisplayPipeline from the event arguments
display = args.Display
# ✅ Geometry display can also be filtered via input viewport_
if self.viewport is None or \
self.viewport.lower() == args.Viewport.Name.lower():
# for each object to be rendered, pass the drawing arguments
for draw_args in self.vis_con.draw_mesh_false_colors:
display.DrawMeshFalseColors(draw_args)
for draw_args in self.vis_con.draw_mesh_shaded:
display.DrawMeshFalseColors(draw_args[0])
for draw_args in self.vis_con.draw_brep_shaded:
if self.only_legend_mode:
continue # ✅ Skip dummy brep
display.DrawBrepShaded(*draw_args)
except Exception, e:
System.Windows.Forms.MessageBox.Show(str(e), "script error")
def get_ClippingBox(self):
if self.vis_con is not None:
return self.vis_con.bbox
else:
return Rhino.Geometry.BoundingBox()
I must admit that using dummy data isn’t an ideal solution, especially since it still shows up when passed to LB Deconstruct VisualizationSet
. But since I prefer not to modify the core modules (I believe I’d likely just mess things up), this is the best workaround I’ve found so far.
These are some of my thoughts and contributions regarding LB Create Legend and LB Preview VisualizationSet. If there are any mistakes or areas for improvement, I would be very grateful for your corrections and suggestions.