pvd , vtu , vti files with vtk wasm

Hi, I am a paraview and pyvista user. I’ve stayed away from trame and vtk.js due to server side rendering and slow js rendering. Now that you have vtk wasm I want a simple page to just load a pvd file from disk and plot it along with a drop down of it’s data arrays. GH Copilot couldnt do it for me so I’m asking here. any ideas how to do this?

Thanks

Joe

They are a couple of trickiness to the setup but the following code, works for me

from pathlib import Path

from trame.app import TrameApp
from trame.ui.vuetify3 import VAppLayout
from trame.widgets import vuetify3 as v3, vtklocal
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: 10rem;"


class PVDViewer(TrameApp):
    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())
        )
        geometry = vtk.vtkGeometryFilter(
           fast_mode=False,
           remove_ghost_interfaces=False,
        )
        mapper = vtk.vtkPolyDataMapper()
        mapper.GetLookupTable().SetHueRange(0.6, 0)
        actor = vtk.vtkActor(mapper=mapper)
        reader >> geometry >> mapper

        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()

        renderer.AddActor(actor)
        renderer.ResetCamera()

        self.reader = reader
        self.mapper = mapper
        self.render_window = render_window
        self.fields = [
            reader.GetPointArrayName(i)
            for i in range(reader.GetNumberOfPointArrays())
        ]

    @change("color_by")
    def _on_color_by(self, color_by, **_):
        if not color_by:
            self.mapper.SetScalarVisibility(0)
            return

        self.mapper.SetScalarVisibility(1)
        self.mapper.SetColorModeToMapScalars()
        self.mapper.SelectColorArray(color_by)
        self.mapper.SetScalarModeToUsePointFieldData()

        # Update LUT (not great)
        color_range = (
           self.reader
               .GetOutputAsDataSet()
               .point_data[color_by]
               .GetRange()
        )
        self.mapper.SetScalarRange(color_range)
        print(f"{color_by}={color_range}")

        # Update view
        self.ctx.view.update()

    def _build_ui(self):
        with VAppLayout(self.server, full_height=True) as self.ui:
            vtklocal.LocalView(
                self.render_window,
                ctx_name="view",
            )
            v3.VSelect(
                v_model=("color_by", None),
                items=("fields", self.fields),
                variant="outlined",
                hide_details=True,
                density="compact",
                style=OVERLAY_TOP_LEFT,
            )


def main():
    app = PVDViewer()
    app.server.start()

if __name__ == "__main__":
    main()

So for running the following code, you need ParaView 6.0 and do the following:

  1. Create a venv using Python 3.12 (MUST match ParaView Python version)
  2. Install in that venv trame, trame-vtklocal, trame-vuetify and vtk==9.5.0
  3. Execute python -m trame.tools.www --output www from your venv to trigger the download of the wasm bundle matching the VTK of your bundle. (In this case vtk==9.5.0 which is the one used in ParaView).
  4. Remove vtk from venv (pip uninstall vtk) to prevent conflict between venv and ParaView’s VTK.
  5. Run ParaView with the venv /Applications/ParaView-6.0.0.app/Contents/bin/pvpython --venv .venv main.py --data ./path/to/file.pvd

Hopefully this sample code show you the basics of what can be done with trame, ParaView and WASM. :wink:

Seb

PS: You have a full VTK example with WASM available here.

1 Like

Thank you so much for the help!

Joe

So, it’s all working fairly well except for these 2 issues:

  1. 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
  2. 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()

I haven’t look at the code in detail, but to fix the duplicate, you need to write your card like so

with v3.VCard(style=OVERLAY_TOP_LEFT):
   v3.VSelect(...)
   v3.VTreeview(...)

Otherwise the VSelect and VTree get linked to the parent container (with self.ui) and then added as children in the VCard. So by tagging the VCard as the parent, they only get added once.

Thanks! That did it, also vuetify doc is a bit conflicting, the fix is “clasic”

Are you talking about that doc or something else?

Ideally for testing and getting a nice UI, I use their playground that I then convert into Python for trame…

@Sebastien_Jourdain Yes, those docs. Good tips. On another note, I am trying to decide between using pyvsta and raw vtk for the app. My team and I are very comfortable with pyvista but vtk is more nuanced (less help than pyvista). The big blocker for me now is that I had a multiblock tree working with the vtk version. When I ported it to pyvista, the parent tree nodes don’t get selected in classic view. Is there other stuff going on behind the scenes with pyvista that vtk doesnt get involved with that would causes this to occur?

Trying to find the best way forward and most supported way forward in creating this post processing application. Your guidance on pyvista vs vtk would be appreciated.

Another thing w/pyvista is that I got trame working without having to install paraview. When containerizing this is a plus to not have to download 840+ MB.

Thanks in advance!

Joe

@Sebastien_Jourdain when I reload geometry in my pvd from a different timestep the app blows up with all sorts of mapper errors on the wasm side. I’ve tried multiple things like removing the actors, resetting just the mappers, updating the vtkLocalView but I cannot get a new geometry to render. I would also like this for a file →new, file→open operation where the user deliberatley changes the geometry and updates the entire app. However, my case here is much simpler, the number of blocks and everything stays the same except the geometry and it’s arrays.

Do you have examples anywhere that does this with Trame and VTK Wasm?

Thanks

Joe