Skip to content

Could use example on how to read from and write to slicer index positions from interactive element #44

Description

@surchs

Hey,

I am trying to have a simple app where I show the current slicer index positions in a datatable or similar element and allow for a user to change the slicer values from this datatable (this could also just be Input fields or something like NumericInput from dash-daq). My goal is something like this:
image

Here is the general strategy I am using:
Suppose I have three slicers:

slicer0 = VolumeSlicer(app, vol, axis=0)
slicer1 = VolumeSlicer(app, vol, axis=1)
slicer2 = VolumeSlicer(app, vol, axis=2)

then to

Write to slicer position:

  1. Make store for slicer positions like described in the examples:
setpos_store = dcc.Store(
    id={"context": "app", "scene": slicer0.scene_id, "name": "setpos"}
)
  1. Write new index positions to the store like so:
return x, y, z 
...
Output(setpos_store.id, "data")

Read from slicer position

This is more difficult. I cannot just listen to Input({"scene": slicer0.scene_id, "context": ALL, "name": "state"}, "data"), as described in the examples, because that gives me circular dependencies. As per the new devnotes, I tried listening to the slider values instead like so:

[
        Input(slicer0.slider.id, "value"),
        Input(slicer1.slider.id, "value"),
        Input(slicer2.slider.id, "value"),
    ],

However, this also results in a circular dependency problem (i.e. the values in the datatable both depend on and affect the slider positions).

A nice workaround proposed by @emmanuelle is to listen to the clickData events from the slicer.graph objects as the trigger, and have the slicer position info as state. This is possible. For example like so:

[
        Input(slicer0.graph.id, "clickData"),
        Input(slicer1.graph.id, "clickData"),
        Input(slicer2.graph.id, "clickData"),
    ],
    [
        State({"scene": slicer0.scene_id, "context": ALL, "name": "state"}, "data"),
    ],

This works, but the slicer position reported by the State here lags one user click event behind the true position. In other words: the coordinates I get in this way are from the position before the current click event. So also not ideal, particularly not if I want to use the coordinates as starting points to let the user make changes. Here is a brief example to show the latter problem:

import dash
import dash_html_components as html
import dash_core_components as dcc
from dash_slicer import VolumeSlicer
from dash.dependencies import Input, Output, State, ALL
import imageio
import dash_daq as daq

app = dash.Dash(__name__, update_title=None)
server = app.server

# Read volume
vol = imageio.volread("imageio:stent.npz")
vol = vol[::3, :, :]
spacing = 1, 1, 1
ori = 0, 0, 0

# Create slicer objects
slicer0 = VolumeSlicer(app, vol, spacing=spacing, origin=ori, axis=0, thumbnail=False)
slicer1 = VolumeSlicer(
    app, vol, spacing=spacing, origin=ori, axis=1, thumbnail=8, reverse_y=False
)
slicer2 = VolumeSlicer(app, vol, spacing=spacing, origin=ori, axis=2, color="#00ff99")

setpos_store = dcc.Store(
    id={"context": "app", "scene": slicer0.scene_id, "name": "setpos"}
)

app.layout = html.Div(style={
    "display": "grid",
    "gridTemplateColumns": "33% 33% 33%",
}, children=[
    html.Div(
        [
            slicer0.graph,
            html.Div(slicer0.slider, style={"display": "none"}),
            *slicer0.stores
        ]
    ),
    html.Div(
        [
            slicer1.graph,
            html.Div(slicer1.slider, style={"display": "none"}),
            *slicer1.stores
        ]
    ),
    html.Div(
        [
            slicer2.graph,
            html.Div(slicer2.slider, style={"display": "none"}),
            *slicer2.stores
        ]
    ),
    html.Pre(id="slicer_click_output"),
    html.Pre(id="slicer_direct_output"),
]
)


# Show the current slicer coordinates in the numeric input fields
@app.callback(
    Output("slicer_click_output", "children"),
    [
        Input(slicer0.graph.id, "clickData"),
        Input(slicer1.graph.id, "clickData"),
        Input(slicer2.graph.id, "clickData"),
    ],
    [
        State({"scene": slicer0.scene_id, "context": ALL, "name": "state"}, "data"),
    ],
    prevent_initial_call=True
)
def read_slicer_pos(fire1, fire2, fire3, slicer_index):
    if slicer_index is None or any([i is None for i in slicer_index]):
        return dash.no_update
    z_pos = slicer_index[0]["index"]
    y_pos = slicer_index[1]["index"]
    x_pos = slicer_index[2]["index"]
    return f"ClickEvent output from slicers\nx: {x_pos}\ny: {y_pos}\nz: {z_pos}"


@app.callback(
    Output("slicer_direct_output", "children"),
    Input({"scene": slicer0.scene_id, "context": ALL, "name": "state"}, "data"),
    prevent_initial_call=True
)
def write_coords(slicer_index):
    if slicer_index is None or any([i is None for i in slicer_index]):
        return dash.no_update
    z_pos = slicer_index[0]["index"]
    y_pos = slicer_index[1]["index"]
    x_pos = slicer_index[2]["index"]
    return f"Direct output from slicers\nx: {x_pos}\ny: {y_pos}\nz: {z_pos}"


if __name__ == "__main__":
    app.run_server(debug=True, dev_tools_props_check=False)

Summary

It's not clear (to me) how to both listen to and change through user input the slicer positions with the same interactive component (e.g. datatable or input field) in an app. It would probably be good to add an example that illustrates this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions