ExtendData for updating 3dMesh trace. No error but no update either

Hello everyone!

I have been trying to implement extendData to update the values of the y axis of a 3dMesh using a slider. The main purpose of using extendData to update the y axis is to move the dark blue/gray squared marker (see screenshot below) across the lung mesh (the green mesh in the screenshot).

I have tried following the documentation and multiple examples I have found but nothing works for my graph. Can someone help me find out if I am missing something or doing something wrong? The following is my script. The data to run the script is here Box

Thank you so much for your help :slight_smile:

import numpy as np

# DASH 
import dash
from dash import Dash, dcc, html, Input, Output, State
import dash_bootstrap_components as dbc
from dash_slicer import VolumeSlicer
from jupyter_dash import JupyterDash
JupyterDash.infer_jupyter_proxy_config()

# GRAPHS
import plotly.graph_objects as go
from plotly.subplots import make_subplots

# LOAD DATA
fx = np.loadtxt("fx.txt")
fy = np.loadtxt("fy.txt")
fz = np.loadtxt("fz.txt")
fi = np.loadtxt("fi.txt")
fj = np.loadtxt("fj.txt")
fk = np.loadtxt("fk.txt")


makersOpacity = 0.18
markerColor = "#244063"
# DISPLAY LUNG MESH
fig_mesh = make_subplots(rows= 1, cols=1, specs=[[{"type": "scene"}]])
fig_mesh.add_trace(go.Mesh3d(x=fz, y=fy, z=fx, opacity=1, i=fk, j=fj, k=fi, color='#0be02f', name= "Fixed Image", showlegend=False, flatshading = False), row=1, col=1)

fig_mesh.add_trace(go.Mesh3d(
        # 8 vertices of a rectangle
        x=[130, 130, 360, 360, 130, 130, 360, 360],
        y=[250, 251, 251, 250, 250, 251, 251, 250], # CORONAL MARKER
        z=[150, 150, 150, 150, 400, 400, 400, 400],

        i = [7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2],
        j = [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3],
        k = [0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6],
        opacity=makersOpacity,
        color= markerColor,
        name = "Coronal Marker",
        showlegend=False,
        flatshading = False                    
    ), row=1, col=1)

### SETTING CAMERA ANGLE FOR VIEW ###
camera_settings = dict(
    up=dict(x=1, y=0, z=1),
    center=dict(x=0, y=0, z=0),
    eye=dict(x=-1.2, y=-1.5, z=0.8)
)

### LAYOUT FOR SCENCE OR 3D ANIMATION ###
fig_mesh.update_layout(scene_camera= camera_settings,
                       scene=dict(
        xaxis=dict(showticklabels=False, visible=False),
        yaxis=dict(showticklabels=False, visible=False),
        zaxis=dict(showticklabels=False, visible=False)), 
                       width=800, height=600, paper_bgcolor='rgba(0,0,0,0.5)')

# DASH 
app = JupyterDash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP, dbc.icons.FONT_AWESOME])

app.layout = html.Div(children=[
    dcc.Graph(
        id='lungMesh',
        figure=fig_mesh
    ),
    html.Div(
        [
            dcc.Slider(
                id='coronalSlider',
                min=160.,
                max=360,
                step=1,
                value=0,
                marks=None,
                updatemode='drag',
            ),
        ],
        style=dict(width='50%'),
    ),
])

@app.callback(
    Output('lungMesh', 'extendData'), 
    Input('coronalSlider', 'value'), 
    prevent_initial_call=True)

def update_data(corVal):
    coronalMarker=[corVal,corVal+1, corVal+1, corVal, corVal, corVal+1, corVal+1, corVal]
    return dict(y=[coronalMarker]), [1]

app.run_server(mode ="external", debug=True)

Hi @numam , is there a special reason why you use extend data? What I did in the past is pretty similar to what you are trying to do.

I had a static figure and a plane slicing thru the volume. I had a callback returning a new figure every time the slider value changed. Something like this:

...
...

lung = go.Mesh3d(
    x=fz,
    y=fy,
    z=fx,
    opacity=1,
    i=fk,
    j=fj,
    k=fi,
    color='#0be02f',
    name="Fixed Image",
    showlegend=False,
    flatshading=False
)
...
...

@app.callback(
    Output('lungMesh', 'figure'),
    Input('coronalSlider', 'value'),
)
def update_data(corVal):
    plane = go.Mesh3d(
        # 8 vertices of a rectangle
        x=[130, 130, 360, 360, 130, 130, 360, 360],
        y=[corVal, corVal+1, corVal+1, corVal, corVal, corVal+1, corVal+1, corVal],  # CORONAL MARKER
        z=[150, 150, 150, 150, 400, 400, 400, 400],

        i=[7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2],
        j=[3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3],
        k=[0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6],
        opacity=makersOpacity,
        color=markerColor,
        name="Coronal Marker",
        showlegend=False,
        flatshading=False
    )

    return go.Figure(data=[lung, plane], layout=layout)
1 Like

Thank you very much for your reply @AIMPED :slight_smile: I tried this approach you are suggesting but as shown in the animation below:

  1. It is very laggy. The plane does not slide through the lung mesh smoothly. I think this is because all the meshes are reloaded into the scene every time the slide’s value changes.
  2. Each time the plane is repositioned, the camera settings are also reloaded and reset. This causes that jumpy effect you observe in the animation.
  3. The camera resetting does not allow one to keep a camera angle while changing the position of the plane slicing through the lungs. I would like to rotate the lung mesh as I wish when inspecting it and be able to change the position of the plane slicer without resetting the camera’s view.

So, I thought that changing the values of the y axis instead of reloading all the scene, I can attain better results. This is why I am trying to implement the extendData property.

PS: In case that you can not see the gif image looping, open it in a new tab.

ScreenRecording2

1 Like

Perfect reasoning @numam ! Looking at it now, my approach is indeed not too appealing :wink: When I find the time, I try to do it “your” way.

1 Like

Are you planning on pulling this slide frame out into a separate graph as well?

Hi @jinnyzor, no. The slide frame will remain in this graph. I have two more slide frames in the x and z planes for that 3d mesh of the lungs. Each is linked to a slide frame or plane marker ( the green, orange and blue vertical and horizontal lines shown in the CT scans below). The purpose of all these plane markers is to help one know better where one is when inspecting the lungs.

So, in order to maintain uirevision, ie moving the camera around, you need to pass this to the layout:

uirevision=True

I can get the slide to move through the object easily with a clientside callback. It’s still not ultra smooth, do probably just to the amount of data:

import numpy as np

# DASH
import dash
from dash import Dash, dcc, html, Input, Output, State
import dash_bootstrap_components as dbc
#from dash_slicer import VolumeSlicer
#from jupyter_dash import JupyterDash
#JupyterDash.infer_jupyter_proxy_config()

# GRAPHS
import plotly.graph_objects as go
from plotly.subplots import make_subplots

path = "C:\\Users\\*\\Downloads\\dataSample\\"

# LOAD DATA
fx = np.loadtxt(path+"fx.txt")
fy = np.loadtxt(path+"fy.txt")
fz = np.loadtxt(path+"fz.txt")
fi = np.loadtxt(path+"fi.txt")
fj = np.loadtxt(path+"fj.txt")
fk = np.loadtxt(path+"fk.txt")


makersOpacity = 0.18
markerColor = "#244063"
# DISPLAY LUNG MESH
fig_mesh = make_subplots(rows= 1, cols=1, specs=[[{"type": "scene"}]])
fig_mesh.add_trace(go.Mesh3d(x=fz, y=fy, z=fx, opacity=1, i=fk, j=fj, k=fi, color='#0be02f', name= "Fixed Image", showlegend=False, flatshading = False), row=1, col=1)

fig_mesh.add_trace(go.Mesh3d(
        # 8 vertices of a rectangle
        x=[130, 130, 360, 360, 130, 130, 360, 360],
        y=[250, 251, 251, 250, 250, 251, 251, 250], # CORONAL MARKER
        z=[150, 150, 150, 150, 400, 400, 400, 400],

        i = [7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2],
        j = [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3],
        k = [0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6],
        opacity=makersOpacity,
        color= markerColor,
        name = "Coronal Marker",
        showlegend=False,
        flatshading = False
    ), row=1, col=1)

### SETTING CAMERA ANGLE FOR VIEW ###
camera_settings = dict(
    up=dict(x=1, y=0, z=1),
    center=dict(x=0, y=0, z=0),
    eye=dict(x=-1.2, y=-1.5, z=0.8)
)

### LAYOUT FOR SCENCE OR 3D ANIMATION ###
fig_mesh.update_layout(scene_camera= camera_settings,
                       scene=dict(
        xaxis=dict(showticklabels=False, visible=False),
        yaxis=dict(showticklabels=False, visible=False),
        zaxis=dict(showticklabels=False, visible=False)),
                       width=800, height=600, paper_bgcolor='rgba(0,0,0,0.5)',
                       uirevision=True)

# DASH
app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP, dbc.icons.FONT_AWESOME])

app.layout = html.Div(children=[
    dcc.Graph(
        id='lungMesh',
        figure=fig_mesh
    ),
    html.Div(
        [
            dcc.Slider(
                id='coronalSlider',
                min=160.,
                max=360,
                step=1,
                value=0,
                marks=None,
                updatemode='drag',
            ),
        ],
        style=dict(width='50%'),
    ),
])

app.clientside_callback(
    """
    function(corVal, fig) {
        newFig = JSON.parse(JSON.stringify(fig))
        newFig['data'][1]['y'] = [corVal,corVal+1, corVal+1, corVal, corVal, corVal+1, corVal+1, corVal]
        return newFig
    }
    """,
    Output('lungMesh', 'figure'),
    Input('coronalSlider', 'value'),
    State('lungMesh', 'figure'),
    prevent_initial_call=True)


app.run(debug=True)
1 Like

Once more: I have to learn JS. These clientside callbacks are quite handy.

1 Like

@jinnyzor This is it. You killed it :mechanical_arm: !!!
It actually runs smooth on my computer. Thank you very much! :slight_smile:

It is probably a lot to ask but do you know a way to also drag the slide frame directly instead of using the slider? I just was able to do this on the ct scans (2D) and not on the 3d mesh.

1 Like

Unfortunately, right now, no.

You could add an event listener, but not sure how well that would do. I’d have to take a look at someway that this would work. I think it would start to conflict with the orient direction.

Was the other chart available via Dash?

I see what you mean. Right now I am trying to figure out how to directly drag the 3d slide frame and it is messing up with the camera orientation.

Which chart are you referring to? The one with the CT scans?

@numam , would you mind sharing how you achieved the dragging in 2D?

1 Like

Yes! I’ll post it on here shortly

If you prefer to stick in Python rather than writing JS callbacks, another possible approach is to use the OperatorTransform. Here is a small example,

import os.path
import numpy as np
import dash_bootstrap_components as dbc
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from dash_extensions.enrich import DashProxy, OperatorTransform, OperatorOutput, Operator, dcc, html, Input

ROOT = "/home/emher/Data"

makersOpacity = 0.18
markerColor = "#244063"


def load_data(key):
    return np.loadtxt(os.path.join(ROOT, key))


def lung_mesh():
    fx = load_data("fx.txt")
    fy = load_data("fy.txt")
    fz = load_data("fz.txt")
    fi = load_data("fi.txt")
    fj = load_data("fj.txt")
    fk = load_data("fk.txt")
    # Draw main fiture.
    fig_mesh = make_subplots(rows=1, cols=1, specs=[[{"type": "scene"}]])
    fig_mesh.add_trace(
        go.Mesh3d(x=fz, y=fy, z=fx, opacity=1, i=fk, j=fj, k=fi, color='#0be02f', name="Fixed Image", showlegend=False,
                  flatshading=False), row=1, col=1)
    fig_mesh.add_trace(go.Mesh3d(
        # 8 vertices of a rectangle
        x=[130, 130, 360, 360, 130, 130, 360, 360],
        y=[250, 251, 251, 250, 250, 251, 251, 250],  # CORONAL MARKER
        z=[150, 150, 150, 150, 400, 400, 400, 400],

        i=[7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2],
        j=[3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3],
        k=[0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6],
        opacity=makersOpacity,
        color=markerColor,
        name="Coronal Marker",
        showlegend=False,
        flatshading=False
    ), row=1, col=1)
    # Set camera settings.
    camera_settings = dict(
        up=dict(x=1, y=0, z=1),
        center=dict(x=0, y=0, z=0),
        eye=dict(x=-1.2, y=-1.5, z=0.8)
    )
    # Set layout.
    fig_mesh.update_layout(scene_camera=camera_settings,
                           scene=dict(
                               xaxis=dict(showticklabels=False, visible=False),
                               yaxis=dict(showticklabels=False, visible=False),
                               zaxis=dict(showticklabels=False, visible=False)),
                           width=800, height=600, paper_bgcolor='rgba(0,0,0,0.5)')
    return fig_mesh

app = DashProxy(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP, dbc.icons.FONT_AWESOME],
                transforms=[OperatorTransform()])
app.layout = html.Div(children=[
    dcc.Graph(
        id='lung_mesh',
        figure=lung_mesh()
    ),
    html.Div(
        [
            dcc.Slider(
                id='coronal_slider',
                min=160.,
                max=360,
                step=1,
                value=0,
                marks=None,
                updatemode='drag',
            ),
        ],
        style=dict(width='50%'),
    ),
])


@app.callback(
    OperatorOutput('lung_mesh', 'figure'),
    Input('coronal_slider', 'value'),
    prevent_initial_call=True)
def update_data(cor_val):
    fig = Operator()
    fig['data'][1]['y'] = [cor_val, cor_val + 1, cor_val + 1, cor_val, cor_val, cor_val + 1, cor_val + 1, cor_val]
    return fig


if __name__ == "__main__":
    app.run_server()
1 Like

@AIMPED, here are the two different approaches I took. The easiest one is a native property of Dash Volume Slicer (Check it out here under multiple slicers: Overview & Reference | Dash for Python Documentation | Plotly). This is the one I am currently using and the one shown in the post above with the CT scans. As far as I understand it, you need at least 3 volumes for the property to start working. by using the “scene_id” for each volume, you can link the slice indicators. Those with the same id will get linked. Now, with this approach you choose the slice by clicking on the images. You cannot drag the indicators. However, it works well and for the indicators to work and to choose slices in the volume, you do not need the sliders.

The other approach is one I was implementing before I found volume slicer. I have not been able to finish it but it is promising and I would probably prefer it because seems to be faster at rendering the slices (does not have a fading effect that the volume slicer has when switching slices) and one can actually drag the indicators. A snippet of the code containing the general approach is below. I could not include images for it but in the script below you find the main idea .

The key is for me was to get a line or figure I could drag and its respective coordinates in order to update the slices I display and the indicators at different planes using callbacks. The main problem I encountered was that I could not find a way to limit the drag to vertical or horizontal only and I could not contain the line within the plot or images. I was planning on solving the problem by creating lines (indicators) that extend indefinitely horizontally or vertically. This way, I could create the illusion that the indicators are always bound to the plot (I hope that this makes sense. If not, when you run the code, you will probably understand what I am talking about). However, I have not found either how to extend the lines or indicators indefinitely. If you know how to 1. limit the drag to horizontally or vertically or 2. how to create lines that extend indefinitely outside the graph, let me know. Because, with either of these two things we can get the script I have to work and we would have a cool component.

If you have questions, let me know. I am more than happy to share or help with anything. Thank you for your help today.

Screenshot:

`import dash
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html
import dash_bootstrap_components as dbc
import json
from textwrap import dedent as d
from jupyter_dash import JupyterDash
import plotly.graph_objects as go
JupyterDash.infer_jupyter_proxy_config()

# Constants Values 4 Images
img_width = 500
img_height = 500
scale_factor = 0.5

# PLANE 1
fig1 = go.Figure ()
fig1.add_shape(type="line",
    x0=0, y0=3, x1=1, y1=3,
    line=dict(
        color="MediumPurple",
        width=5,
    )
)

fig1.update_shapes(dict(xref='paper', yref='y'))
fig1.update_xaxes(
    visible=False,
    range=[0, img_width * scale_factor]
)

fig1.update_yaxes(
    visible=False,
    range=[0, img_height * scale_factor],
    scaleanchor="x"
)

# PLANE 2

fig2 = go.Figure()

fig2.add_shape(type="line",
    x0=1, y0=0, x1=1, y1=1,
    line=dict(
        color="black",
        width=5,
    )
)

fig2.update_shapes(dict(xref='x', yref='paper'))
fig2.update_xaxes(
    visible=False,
    range=[0, img_width * scale_factor]
)

fig2.update_yaxes(
    visible=False,
    range=[0, img_height * scale_factor],
    # the scaleanchor attribute ensures that the aspect ratio stays constant
    scaleanchor="x"
)


# DASH 
app = JupyterDash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP, dbc.icons.FONT_AWESOME], update_title=None)

app.layout = dbc.Container([
    dbc.Row([
        dbc.Col([
    html.Div(children=[
    dcc.Graph(
        id='axialView',
        figure = go.Figure(fig1),
        config={'editable': True, 'edits': {'shapePosition': True}}
        ),
        
        html.Div(
            html.Div([
                    dcc.Markdown(d("""**Axial Plane Coordinates**""")),
                    html.Pre(id='axialCoords', style={'pre': {'overflowX': 'scroll'}}),
                ]))
        ])
    ], width = 4),
        
            dbc.Col([
    html.Div(children=[
    dcc.Graph(
        id='sagitalView',
        figure = fig2,
        config={'editable': True, 'edits': {'shapePosition': True}}
        ),
        
        html.Div(
            html.Div([
                    dcc.Markdown(d("""**Sagital Plane Coordinates**""")),
                    html.Pre(id='sagitalCoords', style={'pre': {'overflowX': 'scroll'}}),
                ]))
        ])
    ], width = 4)
        
])
])


@app.callback(
    Output('axialCoords', 'children'),
    [Input('axialView', 'relayoutData')])
def display_selected_data(axialcoords):
    return json.dumps(axialcoords, indent=1)

@app.callback(
    Output('sagitalCoords', 'children'),
    [Input('sagitalView', 'relayoutData')])
def display_selected_data(sagitalcoords):
    return json.dumps(sagitalcoords, indent=1)


app.run_server(mode ="inline", debug=True)`
2 Likes

Thanks for sharing @Emil! I highly appreciate it. If you know how to limit or contain the dragging of shapes or if you know how to create lines with indefinite width or height to solve the problem posted below, let us know :slight_smile: