Dash_vtk: Display XYZ axes in 3D scene (orientation marker / coordinate helper)

I am working with dash_vtk to render a 3D scene. I want to display a small XYZ coordinate axis (like an orientation marker in VTK or similar to Plotly 3D axes). Like shown in this demo picture

However, I couldn’t find a built-in way to add this in dash_vtk.
How i can go further and implement such axis?

———–Code Snippet ———

import dash
from dash import dcc, html, Input, Output, State
import plotly.graph_objects as go

app = dash.Dash(__name__)

def create_figure():
    fig = go.Figure()

    # Main geometry: cube
    fig.add_trace(go.Mesh3d(
        x=[0,1,1,0,0,1,1,0],
        y=[0,0,1,1,0,0,1,1],
        z=[0,0,0,0,1,1,1,1],
        color="lightgray",
        opacity=1
    ))

    # Corner axes
    axes = [
        ([0,1],[0,0],[0,0],"red","X"),
        ([0,0],[0,1],[0,0],"green","Y"),
        ([0,0],[0,0],[0,1],"blue","Z")
    ]
    for x,y,z,c,label in axes:
        fig.add_trace(go.Scatter3d(
            x=x, y=y, z=z,
            mode="lines+text",
            line=dict(color=c,width=4),
            text=["",label],
            textposition="top center",
            scene="scene2",
            showlegend=False
        ))
    return fig
"""
    fig.update_layout(
        margin=dict(l=0,r=0,t=0,b=0),
        scene=dict(
            aspectmode="cube",
            xaxis=dict(visible=False, range=[-0.5,1.5]),
            yaxis=dict(visible=False, range=[-0.5,1.5]),
            zaxis=dict(visible=False, range=[-0.5,1.5]),
        ),
        scene2=dict(
            domain=dict(x=[0.02,0.18], y=[0.02,0.18]),
            aspectmode="cube",
            xaxis=dict(visible=False),
            yaxis=dict(visible=False),
            zaxis=dict(visible=False),
            bgcolor="rgba(0,0,0,0)",
            camera=dict(eye=dict(x=1.8,y=1.8,z=1.8))
        ),
        uirevision="locked"
    )

    return fig
"""

fig = create_figure()

app.layout = html.Div([
    dcc.Graph(id="viewer", figure=fig, style={"height":"600px","width":"600px","bg-color":"black"}),
    # Hidden div to store last updated scene id to avoid infinite loop
    html.Div(id="last-updated-scene", style={"display":"none"})
])


@app.callback(
    Output("viewer", "figure"),
    Output("last-updated-scene", "children"),
    Input("viewer", "relayoutData"),
    State("viewer", "figure"),
    State("last-updated-scene", "children"),
    prevent_initial_call=True
)
def two_way_sync(relayout, fig_state, last_updated):
    
    if not relayout:
        return fig_state, last_updated

    # Determine which camera moved: 'scene.camera' or 'scene2.camera'
    moved_scene = None
    if "scene.camera" in relayout:
        moved_scene = "scene"
    elif "scene2.camera" in relayout:
        moved_scene = "scene2"

    # If no camera moved or same scene triggered last update, do nothing
    if moved_scene is None or moved_scene == last_updated:
        return fig_state, last_updated

    cam = relayout.get(f"{moved_scene}.camera")
    if not cam:
        return fig_state, last_updated

    # Calculate scaled eye for scene2 to keep separation
    eye = cam.get("eye")
    mag = (eye["x"]**2 + eye["y"]**2 + eye["z"]**2)**0.5 if eye else 1

    # Copy camera to the *other* scene
    target_scene = "scene2" if moved_scene == "scene" else "scene"
    new_eye = {
        "x": eye["x"]/mag*1.8 if eye else 1.8,
        "y": eye["y"]/mag*1.8 if eye else 1.8,
        "z": eye["z"]/mag*1.8 if eye else 1.8,
    }

    # Update the other scene's camera
    fig_state["layout"][f"{target_scene}_camera"] = dict(
        eye=new_eye,
        center=cam.get("center", {"x":0,"y":0,"z":0}),
        up=cam.get("up", {"x":0,"y":0,"z":1})
    )

    return fig_state, moved_scene


if __name__ == "__main__":
    app.run(debug=True)

Hey @Abhay_DJ, does this help?

Thanks for the pointer, but unfortunately, this hasn’t solved the issue. While the Plotly go.Figure approach works for static vector visualization, it doesn’t solve the core requirement: a real-time orientation marker that behaves like the native vtkOrientationMarkerWidget.

After several deep-dives and implementation attempts, here is the “Real Truth” regarding the current limitations of dash-vtk for this specific feature:

  1. The “Dead Axis” Problem: In a standard VTK environment, the orientation marker is a slave to the main renderer. In dash-vtk, even when using a Python callback to sync cameraPosition, there is a significant lag. The axes are either “fixed” (don’t move) or they stutter because the camera data has to travel from the browser to the Python server and back for every single degree of rotation.

  2. Missing Client-Side Sync: Without a native OrientationMarker component in the dash-vtk library, we are forced to use two separate dash_vtk.View components. Because these components don’t share a single internal JavaScript state, they cannot be perfectly synced without a Client-Side Callback (which requires writing custom React/JS, defeating the purpose of a pure Python Dash implementation).

  3. Label Rendering Issues: Desktop VTK (as shown in my earlier screenshots) uses vtkAxesActor with high-quality 2D text labels. In dash-vtk, trying to render 3D text actors or even HTML overlays results in “length of null” errors or labels that don’t rotate correctly with the coordinate system.

  4. Camera Auto-Reset: Every time the child geometry of the axis-view is updated via a callback, the view attempts to reset.

    import dash
    from dash import html, dcc, Input, Output, State
    import dash_vtk
    import numpy as np
    
    app = dash.Dash(_name_)
    
    def build_axes():
    l = 1.0
    configs = [
    {‘d’: [l, 0, 0], ‘c’: [1, 0, 0]},  # X: Red
    
    
    
    
    {‘d’: [0, l, 0], ‘c’: [0, 1, 0]},  # Y: Green
    {‘d’: [0, 0, l], ‘c’: [0, 0, 1]},  # Z: Blue
    ]
    geoms = []
    for cfg in configs:
    geoms.append(dash_vtk.GeometryRepresentation(
    children=[dash_vtk.PolyData(points=[0, 0, 0, *cfg[‘d’]], lines=[2, 0, 1])],
    property={“color”: cfg[‘c’], “lineWidth”: 5}
    ))
    geoms.append(dash_vtk.GeometryRepresentation(
    children=[dash_vtk.Algorithm(
    vtkClass=“vtkConeSource”,
    state={“direction”: cfg[‘d’], “center”: cfg[‘d’], “height”: 0.2, “radius”: 0.08}
    )],
    property={“color”: cfg[‘c’]}
    ))
    return geoms
    
    app.layout = html.Div([
    html.Div([
    
    MAIN VIEW
    
    dash_vtk.View(
    id=“main-view”,
    background=[0.1, 0.1, 0.1],
    cameraPosition=[3, 3, 3],
    cameraViewUp=[0, 0, 1],
    children=[
    dash_vtk.GeometryRepresentation(
    id=“obj-repr”,
    children=[
    
    Placeholder for your OBJ
    
    dash_vtk.Algorithm(vtkClass=“vtkConeSource”, state={“height”: 2.0, “radius”: 1.0})
    ],
    )
    ],
    style={“width”: “100%”, “height”: “90vh”},
    ),
    
        # AXIS WIDGET
        html.Div(\[
            dash_vtk.View(
                id="axis-view",
                cameraPosition=\[3, 3, 3\],
                cameraViewUp=\[0, 0, 1\],
                cameraParallelProjection=True,
                interactorSettings=\[{"button": 1, "action": "None"}\],  # Follower only
                children=build_axes(),
                style={"width": "100%", "height": "100%"},
            ),
            # Absolute labels relative to the mini-box
            html.Div("X", style={"position": "absolute", "right": "10px", "top": "45%", "color": "red",
                                 "fontWeight": "bold"}),
            html.Div("Y", style={"position": "absolute", "left": "45%", "top": "10px", "color": "green",
                                 "fontWeight": "bold"}),
            html.Div("Z", style={"position": "absolute", "left": "10px", "bottom": "10px", "color": "blue",
                                 "fontWeight": "bold"}),
        \], style={
            "position": "absolute", "bottom": "20px", "left": "20px",
            "width": "140px", "height": "140px",
            "border": "2px solid #555", "background": "rgba(0,0,0,0.4)",
            "borderRadius": "8px", "pointerEvents": "none"
        }),
    \], style={"position": "relative"}),
    
    # Hidden component to keep the camera synced even if no sliders are used
    dcc.Interval(id="sync-trigger", interval=200)
    
    ])
    
    @app.callbackapp.callbackapp.callbackapp.callback(
    Output(“axis-view”, “cameraPosition”),
    Output(“axis-view”, “cameraViewUp”),
    Input(“main-view”, “cameraPosition”),
    Input(“main-view”, “cameraViewUp”),
    
    We use the interval as a backup to catch movement if the view doesn’t fire
    
    Input(“sync-trigger”, “n_intervals”),
    State(“main-view”, “cameraPosition”),
    State(“main-view”, “cameraViewUp”),
    )
    def sync_camera(p_in, u_in, n, p_state, u_state):
    
    Use state if inputs are null to avoid the ‘length’ error
    
    pos = p_in or p_state or [3, 3, 3]
    up = u_in or u_state or [0, 0, 1]
    
    # Normalize to a unit vector so the axes don't zoom
    mag = np.linalg.norm(pos)
    if mag == 0: mag = 1
    
    new_pos = \[(p / mag) \* 4.0 for p in pos\]  # Fixed distance 4.0
    return new_pos, up
    
    if _name_ == “_main_”:
    app.run(debug=True)
    

Conclusion: It seems this is a fundamental limitation of the current dash-vtk wrapper. Unless there is a way to link two views at the React/WebGL layer without a Python round-trip, a true “Movable Orientation Marker” isn’t feasible for professional use-cases in Dash right now.

Looking for further updates on above issue.

will be appreciated!

@Abhay_DJ Do you know whether this is a feature that exists in VTK itself? If so, it should be possible to expose it in Dash VTK, but it might require modifying the Python code. If that’s the case, we would welcome a PR.

Hi @emilykl,

Yes, I am aware that this feature exists in VTK, and it seemed obvious that it should exist in Dash VTK too, given its importance for engineering applications.

Since it isn’t currently exposed in the Python wrapper for dash-vtk, I managed to implement a working version by switching to Plotly and using two separate scenes within the same dcc.Graph. By defining a secondary scene (scene2) with a smaller domain in the corner and using a callback to sync the scene.camera to scene2.camera, I achieved the synchronized orientation marker effect.

While this works for Plotly geometries, it would still be a massive value-add to have a native showOrientationMarker property in dash-vtk to handle heavy OBJ/STL files that Plotly might struggle with.

Also there are limitations and issues with my implementation that the axis has a lag doesn’t react instant when geometry moves.

If anyone has already implemented a workaround—perhaps by using a specific vtkTransform or a custom React component—could you provide a small demo or code snippet? It would be a huge help for those of us trying to build professional engineering viewers in Dash.