LB Create Legend and Preview VisualizationSet – Some Edits

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 :white_check_mark: 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 :white_check_mark: 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.

2 Likes

i need to understand first completly

Hi @tutu7931,

It is up to @chris to make the decision on including any of these changes into the official release.

I just wanted to leave a note here and appreciate your willingness to document and share your work. Thank you!