Skip to content

visualdynamics.viz

viz

The 3-D scene: geometry, DOF arrows and moving mode shapes.

PyVista over VTK, shown in the app's own pane or rendered off-screen to an image. The plots on an axis are visualdynamics.plot; this is the half that has a camera in it.

Functions:

Name Description
geometry_scene

Build (or add to) a PyVista plotter showing the geometry.

plot_geometry

Show the geometry interactively, or render to screenshot headlessly.

plot_waterfall

Show the waterfall interactively, or render it to screenshot.

waterfall_scene

Build (or add to) a PyVista plotter showing the waterfall.

undeferred

Make this Qt plotter render synchronously, and return it.

Functions:

geometry_scene

geometry_scene(
    geometry: Geometry,
    unit_system: UnitSystem | None = None,
    plotter: Any = None,
    node_size: float = 8.0,
    line_width: float = 2.0,
    show_edges: bool = True,
    opacity: float = 1.0,
    labels: Sequence[str] | None = None,
    off_screen: bool = False,
    theme: Any = None,
    components: Sequence[str] | None = None,
) -> Any

Build (or add to) a PyVista plotter showing the geometry.

theme is 'light', 'dark', or a colors dict; it sets the scene background and annotation color. components limits what is drawn to a subset of {'nodes', 'tracelines', 'elements'} — selecting one in the project tree shows just that part. Returns the plotter; call .show() on it (or .screenshot() if off_screen).

Source code in src/visualdynamics/viz/geometry.py
def geometry_scene(geometry: Geometry, unit_system: UnitSystem | None = None,
                   plotter: Any = None, node_size: float = 8.0,
                   line_width: float = 2.0, show_edges: bool = True,
                   opacity: float = 1.0,
                   labels: Sequence[str] | None = None,
                   off_screen: bool = False, theme: Any = None,
                   components: Sequence[str] | None = None) -> Any:
    """Build (or add to) a PyVista plotter showing the geometry.

    `theme` is 'light', 'dark', or a colors dict; it sets the scene
    background and annotation color. `components` limits what is drawn to a
    subset of {'nodes', 'tracelines', 'elements'} — selecting one in the
    project tree shows just that part. Returns the plotter; call .show() on
    it (or .screenshot() if off_screen).
    """
    import pyvista as pv

    us = unit_system or DEFAULT_SYSTEM
    colors = resolve_theme(theme)
    if plotter is None:
        plotter = pv.Plotter(off_screen=off_screen)
    plotter.set_background(colors['scene_background'])
    axis_unit = add_geometry(plotter, geometry, unit_system=us,
                             node_size=node_size, line_width=line_width,
                             show_edges=show_edges, opacity=opacity,
                             labels=labels, components=components,
                             text_color=colors['scene_text'])
    annotate_scene(plotter, axis_unit, colors)
    return plotter

plot_geometry

plot_geometry(
    geometry: Geometry,
    unit_system: UnitSystem | None = None,
    screenshot: str | None = None,
    theme: Any = None,
    show: bool = True,
    **kwargs: Any,
) -> Any

Show the geometry interactively, or render to screenshot headlessly.

Shown, it comes up in the app's own 3-D pane — the labelled axes and the orientation triad are toggles on the bar over it, exactly as in the window. Returns the pane (its .plotter is the PyVista one), or the image array when rendering to a file.

Source code in src/visualdynamics/viz/geometry.py
def plot_geometry(geometry: Geometry, unit_system: UnitSystem | None = None,
                  screenshot: str | None = None, theme: Any = None,
                  show: bool = True, **kwargs: Any) -> Any:
    """Show the geometry interactively, or render to `screenshot` headlessly.

    Shown, it comes up in the app's own 3-D pane — the labelled axes and
    the orientation triad are toggles on the bar over it, exactly as in
    the window. Returns the pane (its `.plotter` is the PyVista one), or
    the image array when rendering to a file.
    """
    if screenshot is not None:
        plotter = geometry_scene(geometry, unit_system=unit_system,
                                 theme=theme, off_screen=True, **kwargs)
        img = plotter.screenshot(screenshot)
        plotter.close()
        return img
    from ..gui.windows import scene_window

    us = unit_system or DEFAULT_SYSTEM
    return scene_window(
        lambda plotter: geometry_scene(geometry, unit_system=us,
                                       plotter=plotter, theme=theme,
                                       **kwargs),
        theme=theme, axis_unit=axis_unit_text(geometry, us),
        title='Geometry', show=show)

plot_waterfall

plot_waterfall(
    data: DataArray,
    records: Sequence[int] | None = None,
    *,
    screenshot: str | None = None,
    unit_system: UnitSystem | None = None,
    theme: Any = None,
    component: str = "magnitude",
    show: bool = True,
    quantity: tuple[str, str | None] | None = None,
) -> Any

Show the waterfall interactively, or render it to screenshot.

The scriptable face of the plot bar's 3-D reading, like plot_geometry for the geometry scene. Returns the pane (its .plotter is the PyVista one), or the image array when rendering to a file.

Source code in src/visualdynamics/viz/waterfall.py
def plot_waterfall(data: DataArray,
                   records: Sequence[int] | None = None, *,
                   screenshot: str | None = None,
                   unit_system: UnitSystem | None = None,
                   theme: Any = None, component: str = 'magnitude',
                   show: bool = True,
                   quantity: tuple[str, str | None] | None = None) -> Any:
    """Show the waterfall interactively, or render it to `screenshot`.

    The scriptable face of the plot bar's 3-D reading, like
    `plot_geometry` for the geometry scene. Returns the pane (its
    `.plotter` is the PyVista one), or the image array when rendering
    to a file.
    """
    if screenshot is not None:
        plotter = waterfall_scene(data, records, unit_system,
                                  off_screen=True, theme=theme,
                                  component=component, quantity=quantity)
        img = plotter.screenshot(screenshot)
        plotter.close()
        return img
    from ..gui.windows import scene_window

    return scene_window(
        lambda plotter: waterfall_scene(data, records, unit_system,
                                        plotter=plotter, theme=theme,
                                        component=component,
                                        quantity=quantity),
        theme=theme, axis_unit='', title='Waterfall', show=show)

waterfall_scene

waterfall_scene(
    data: DataArray,
    records: Sequence[int] | None = None,
    unit_system: UnitSystem | None = None,
    plotter: Any = None,
    off_screen: bool = False,
    theme: Any = None,
    component: str = "magnitude",
    budget: int = POINT_BUDGET,
    quantity: tuple[str, str | None] | None = None,
) -> Any

Build (or add to) a PyVista plotter showing the waterfall.

Same shape as geometry_scene: theme is 'light', 'dark' or a colors dict; returns the plotter — .show() it, or .screenshot() if off_screen. The camera is placed once, here: from the front-left and above, abscissa reading left to right, records receding.

Source code in src/visualdynamics/viz/waterfall.py
def waterfall_scene(data: DataArray,
                    records: Sequence[int] | None = None,
                    unit_system: UnitSystem | None = None,
                    plotter: Any = None, off_screen: bool = False,
                    theme: Any = None, component: str = 'magnitude',
                    budget: int = POINT_BUDGET,
                    quantity: tuple[str, str | None] | None = None) -> Any:
    """Build (or add to) a PyVista plotter showing the waterfall.

    Same shape as `geometry_scene`: `theme` is 'light', 'dark' or a
    colors dict; returns the plotter — `.show()` it, or `.screenshot()`
    if off_screen. The camera is placed once, here: from the front-left
    and above, abscissa reading left to right, records receding.
    """
    import pyvista as pv

    colors = resolve_theme(theme)
    if plotter is None:
        plotter = pv.Plotter(off_screen=off_screen)
    plotter.set_background(colors['scene_background'])
    add_waterfall(plotter, data, records, unit_system, colors, component,
                  budget, quantity)
    place_camera(plotter)
    return plotter

undeferred

undeferred(plotter)

Make this Qt plotter render synchronously, and return it.

On macOS pyvistaqt wraps render() in a worker thread that emits a signal back to the GUI thread, so every render becomes a queued metacall delivered later by the event loop. That indirection exists to make render() safe to call from another thread — which nothing here ever does — and it opens a race that nothing here can close: if the widget's native window goes away between the emit and the delivery, the queued call walks into vtkCocoaRenderWindow with a dead QPlatformWindow and the process dies with SIGSEGV (seen in the field: QPlatformWindow::window() dereferencing null out of NSOpenGLContext update).

Calling the unthreaded path directly is what the queued delivery would have done, minus the window between emit and delivery in which the target can die. _rendered is forced the way the threaded override forces it, because BasePlotter.render skips the flag when it skips the render, and pyvistaqt marked that flag "crucial".

Bound on the instance at construction rather than patched on the class, so a pyvistaqt used by anything else in the process keeps its own behaviour.

Source code in src/visualdynamics/viz/__init__.py
def undeferred(plotter):
    """Make this Qt plotter render synchronously, and return it.

    On macOS pyvistaqt wraps ``render()`` in a worker thread that emits
    a signal back to the GUI thread, so every render becomes a queued
    metacall delivered later by the event loop. That indirection exists
    to make ``render()`` safe to call *from* another thread — which
    nothing here ever does — and it opens a race that nothing here can
    close: if the widget's native window goes away between the emit and
    the delivery, the queued call walks into ``vtkCocoaRenderWindow``
    with a dead ``QPlatformWindow`` and the process dies with SIGSEGV
    (seen in the field: ``QPlatformWindow::window()`` dereferencing
    null out of ``NSOpenGLContext update``).

    Calling the unthreaded path directly is what the queued delivery
    would have done, minus the window between emit and delivery in
    which the target can die. ``_rendered`` is forced the way the
    threaded override forces it, because ``BasePlotter.render`` skips
    the flag when it skips the render, and pyvistaqt marked that flag
    "crucial".

    Bound on the instance at construction rather than patched on the
    class, so a pyvistaqt used by anything else in the process keeps
    its own behaviour.
    """
    unthreaded = plotter._render

    def render():
        plotter._rendered = True
        unthreaded()

    plotter.render = render
    # And nothing renders behind the app's back: QtInteractor defaults
    # to auto_update=5.0, a QTimer calling render() five times a second
    # for the life of the widget. Every draw here is explicit, so those
    # ticks bought nothing — and the first one to fire after the native
    # window died walked into vtkCocoaRenderWindow with a dead
    # QPlatformWindow, the same SIGSEGV as the threaded render, arriving
    # by timer instead of by queued metacall (Brandon's crash report,
    # 2026-08-30).
    timer = getattr(plotter, 'render_timer', None)
    if timer is not None:
        timer.stop()
    return plotter