So, it’s all working fairly well except for these 2 issues:
- The tree view is not in leaf selection mode, that is I cannot select deselect the root nodes of the tree no matter which options I choose
- Using the card, the select and tree control are rendered twice.  Once in the card, and again at the bottom of the screen.  Here’s the code
My dataset is a pvd file that consists of multiblock data
from pathlib import Path
from trame.app import TrameApp
from trame.ui.vuetify3 import VAppLayout
from trame.widgets import vuetify3 as v3, vtklocal, html
from trame.decorators import change
from paraview.modules.vtkPVVTKExtensionsIOCore import vtkPVDReader
import vtk
OVERLAY_TOP_LEFT = "position: absolute; top: 1rem; left: 1rem;z-index: 1; width: 20rem; max-width: 22rem; background: rgba(255,255,255,0.1);"
v3.enable_lab()
class PVDViewer(TrameApp):
    def _build_multiblock_tree(self, dataset, id_counter, renderer, lut, actors, block_id_to_actor, block_ids, fields):
        import vtk
        tree = []
        if hasattr(dataset, "GetNumberOfBlocks") and dataset.GetNumberOfBlocks() > 0:
            for i in range(dataset.GetNumberOfBlocks()):
                child = dataset.GetBlock(i)
                name = dataset.GetMetaData(i).Get(vtk.vtkCompositeDataSet.NAME()) if dataset.GetMetaData(i) and dataset.GetMetaData(i).Has(vtk.vtkCompositeDataSet.NAME()) else f"Block {i}"
                node_id = next(id_counter)
                if hasattr(child, "GetNumberOfBlocks") and child.GetNumberOfBlocks() > 0:
                    children = self._build_multiblock_tree(child, id_counter, renderer, lut, actors, block_id_to_actor, block_ids, fields)
                    tree.append({"id": node_id, "name": name, "children": children})
                elif child and child.IsA('vtkDataSet'):
                    # Only create actor for leaf
                    mapper = vtk.vtkDataSetMapper()
                    mapper.SetInputData(child)
                    mapper.SetLookupTable(lut)
                    actor = vtk.vtkActor(mapper=mapper)
                    renderer.AddActor(actor)
                    actors.append(actor)
                    block_id_to_actor[node_id] = actor
                    block_ids.append(node_id)
                    # Collect point data arrays
                    pd = child.GetPointData()
                    arr_names = [pd.GetArrayName(j) for j in range(pd.GetNumberOfArrays())]
                    for name_field in arr_names:
                        if name_field:
                            fields.add(name_field)
                    tree.append({"id": node_id, "name": name})
                else:
                    tree.append({"id": node_id, "name": name})
        return tree
    def __init__(self, server=None):
        super().__init__(server)
        # CLI handling
        self.server.cli.add_argument("--data", required=True)
        args, _ = self.server.cli.parse_known_args()
        # Init app
        self._setup_vtk(args.data)
        self._build_ui()
    def _setup_vtk(self, file_to_load):
        reader = vtkPVDReader(
            file_name=str(Path(file_to_load).resolve())
        )
        reader.Update()
        renderer = vtk.vtkRenderer(background=(0.5, 0.5, 0.5))
        render_window = vtk.vtkRenderWindow()
        render_window.AddRenderer(renderer)
        render_window.OffScreenRenderingOn()
        interactor = vtk.vtkRenderWindowInteractor()
        interactor.SetRenderWindow(render_window)
        interactor.GetInteractorStyle().SetCurrentStyleToTrackballCamera()
        # Create a shared 12-band rainbow lookup table
        lut = vtk.vtkLookupTable()
        lut.SetNumberOfTableValues(12)
        lut.SetHueRange(0.66667, 0.0)  # VTK rainbow: blue to red
        lut.SetTableRange(0, 1)  # Will be set to data range later
        lut.SetRampToLinear()
        lut.SetNumberOfColors(12)
        lut.Build()
        self.lut = lut
        # Add all blocks as actors and collect all unique point data arrays
        output = reader.GetOutputDataObject(0)
        fields = set()
        actors = []
        self._block_id_to_actor = {}
        self._tree_data = []
        self._block_ids = []
        def id_counter_gen():
            n = 1
            while True:
                yield n
                n += 1
        id_counter = id_counter_gen()
        if output and output.IsA('vtkMultiBlockDataSet'):
            print("MultiBlockDataSet with blocks")
            self._tree_data = self._build_multiblock_tree(output, id_counter, renderer, lut, actors, self._block_id_to_actor, self._block_ids, fields)
        elif output and output.IsA('vtkDataSet'):
            print("Single vtkDataSet")
            node_id = 1
            mapper = vtk.vtkDataSetMapper()
            mapper.SetInputData(output)
            mapper.SetLookupTable(lut)
            actor = vtk.vtkActor(mapper=mapper)
            renderer.AddActor(actor)
            actors.append(actor)
            self._block_id_to_actor[node_id] = actor
            self._block_ids = [node_id]
            self._tree_data = [{"id": node_id, "name": "Block 0"}]
            pd = output.GetPointData()
            arr_names = [pd.GetArrayName(i) for i in range(pd.GetNumberOfArrays())]
            print(f"Single block arrays: {arr_names}")
            for name in arr_names:
                if name:
                    fields.add(name)
        renderer.ResetCamera()
        self.reader = reader
        self.actors = actors
        self.render_window = render_window
        self.fields = sorted(fields)
        self.server.state.tree_data = self._tree_data
        self.server.state.visible_blocks = list(self._block_ids)
    @change("visible_blocks")
    def _on_visible_blocks(self, visible_blocks, **_):
        # Set actor visibility based on selected block ids
        for block_id, actor in self._block_id_to_actor.items():
            actor.SetVisibility(block_id in visible_blocks)
        self.ctx.view.update()
    @change("color_by")
    def _on_color_by(self, color_by, **_):
        if not color_by:
            for actor in getattr(self, 'actors', []):
                mapper = actor.GetMapper()
                mapper.SetScalarVisibility(0)
            return
        # First, find the global min/max for the selected array across all blocks
        global_min = None
        global_max = None
        for idx, actor in enumerate(getattr(self, 'actors', [])):
            mapper = actor.GetMapper()
            ds = mapper.GetInput()
            pd = ds.GetPointData() if ds else None
            arr = pd.GetArray(color_by) if pd else None
            if arr is not None:
                arr_range = arr.GetRange()
                if global_min is None or arr_range[0] < global_min:
                    global_min = arr_range[0]
                if global_max is None or arr_range[1] > global_max:
                    global_max = arr_range[1]
        print(f"Global range for '{color_by}': {global_min}, {global_max}")
        # Set the LUT range globally
        if hasattr(self, 'lut') and global_min is not None and global_max is not None:
            self.lut.SetTableRange(global_min, global_max)
            self.lut.Build()
        # Now, apply this range to all actors that have the array
        for idx, actor in enumerate(getattr(self, 'actors', [])):
            mapper = actor.GetMapper()
            ds = mapper.GetInput()
            pd = ds.GetPointData() if ds else None
            arr_names = [pd.GetArrayName(i) for i in range(pd.GetNumberOfArrays())] if pd else []
            print(f"Block {idx} arrays: {arr_names}")
            arr = pd.GetArray(color_by) if pd else None
            print(f"Trying to color by: '{color_by}'")
            if arr is not None and global_min is not None and global_max is not None:
                mapper.SetScalarVisibility(1)
                mapper.SetColorModeToMapScalars()
                mapper.SelectColorArray(color_by)
                mapper.SetScalarModeToUsePointFieldData()
                mapper.SetScalarRange(global_min, global_max)
                print(f"{color_by} set to global range: {global_min}, {global_max}")
            else:
                mapper.SetScalarVisibility(0)
                print(f"Block missing array '{color_by}', not colored.")
        # Update view
        self.ctx.view.update()
    def _build_ui(self):
        print(self._block_ids)
        print(self._tree_data)
        with VAppLayout(self.server, full_height=True) as self.ui:
            with v3.VMain():
                # 3D view
                vtklocal.LocalView(
                    self.render_window,
                    ctx_name="view",
                )
                # Floating card with treeview and select
                v3.VCard(
                    style=OVERLAY_TOP_LEFT,
                    children=[
                        v3.VSelect(
                            v_model=("color_by", None),
                            items=("fields", self.fields),
                            variant="outlined",
                            hide_details=True,
                            density="compact",
                            style="background: rgba(255,255,255,0.1);",
                        ),
                        v3.VTreeview(
                            v_model_selected="visible_blocks",
                            items=("tree_data", self._tree_data),
                            selectable=True,
                            active_strategy ="leaf",
                            select_strategy="leaf",
                            open_all=True,
                            item_title="name",
                            item_value="id",
                            variant="outlined",
                            hide_details=True,
                            density="compact",
                            style="margin-bottom: 1rem; background: rgba(255,255,255,0.1);",
                        ),
                    ],
                )
def main():
    app = PVDViewer()
    app.server.start()
if __name__ == "__main__":
    main()