diff --git a/README.md b/README.md index e61b1dc..06861e1 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,11 @@ can be a list of such colors, defining a colormap. **property `VolumeSlicer.axis`** (`int`): The axis to slice. +**property `VolumeSlicer.extra_traces`**: A `dcc.Store` that can be used as an output to define +additional traces to be shown in this slicer. The data must be +a list of dictionaries, with each dict representing a raw trace +object. + **property `VolumeSlicer.graph`**: The `dcc.Graph` for this slicer. Use `graph.figure` to access the Plotly Figure object. diff --git a/dash_slicer/slicer.py b/dash_slicer/slicer.py index 6300bb8..66d4bee 100644 --- a/dash_slicer/slicer.py +++ b/dash_slicer/slicer.py @@ -238,6 +238,15 @@ def state(self): """ return self._state + @property + def extra_traces(self): + """A `dcc.Store` that can be used as an output to define + additional traces to be shown in this slicer. The data must be + a list of dictionaries, with each dict representing a raw trace + object. + """ + return self._extra_traces + @property def overlay_data(self): """A `dcc.Store` containing the overlay data. The form of this @@ -420,6 +429,9 @@ def _create_dash_components(self): # Store indicator traces for the slicer. self._indicator_traces = Store(id=self._subid("indicator-traces"), data=[]) + # Store user traces for the slider. + self._extra_traces = Store(id=self._subid("extra-traces"), data=[]) + # A timer to apply a rate-limit between slider.value and index.data self._timer = Interval(id=self._subid("timer"), interval=100, disabled=True) @@ -436,6 +448,7 @@ def _create_dash_components(self): self._server_data, self._img_traces, self._indicator_traces, + self._extra_traces, self._timer, self._state, self._setpos, @@ -787,11 +800,12 @@ def _create_client_callbacks(self): app.clientside_callback( """ - function update_figure(img_traces, indicators, info, ori_figure) { + function update_figure(img_traces, indicator_traces, extra_traces, info, ori_figure) { // Collect traces let traces = []; for (let trace of img_traces) { traces.push(trace); } - for (let trace of indicators) { if (trace.line.color) traces.push(trace); } + for (let trace of extra_traces) { traces.push(trace); } + for (let trace of indicator_traces) { if (trace.line.color) traces.push(trace); } // Update figure let figure = {...ori_figure}; figure.data = traces; @@ -802,6 +816,7 @@ def _create_client_callbacks(self): [ Input(self._img_traces.id, "data"), Input(self._indicator_traces.id, "data"), + Input(self._extra_traces.id, "data"), ], [State(self._info.id, "data"), State(self._graph.id, "figure")], ) diff --git a/examples/threshold_contour.py b/examples/threshold_contour.py new file mode 100644 index 0000000..ffda504 --- /dev/null +++ b/examples/threshold_contour.py @@ -0,0 +1,70 @@ +""" +An example demonstrating adding traces. + +This shows a volume with contours overlaid on top. The `extra_traces` +property is used to add scatter traces that represent the contours. +""" + +import plotly +import dash +import dash_html_components as html +import dash_core_components as dcc +from dash.dependencies import Input, Output +from dash_slicer import VolumeSlicer +import imageio +from skimage import measure + + +app = dash.Dash(__name__, update_title=None) +server = app.server + +vol = imageio.volread("imageio:stent.npz") +mi, ma = vol.min(), vol.max() +slicer = VolumeSlicer(app, vol) + + +app.layout = html.Div( + [ + slicer.graph, + slicer.slider, + dcc.Slider( + id="level-slider", + min=mi, + max=ma, + step=1, + value=mi + 0.2 * (ma - mi), + ), + *slicer.stores, + ] +) + + +@app.callback( + Output(slicer.extra_traces.id, "data"), + [Input("level-slider", "value"), Input(slicer.state.id, "data")], +) +def apply_levels(level, state): + if not state: + return dash.no_update + slice = vol[state["index"]] + contours = measure.find_contours(slice, level) + # Create a trace for each contour, each a different color + traces = [] + for i, contour in enumerate(contours): + colors = plotly.colors.qualitative.D3 + color = colors[i % len(colors)] + traces.append( + { + "type": "scatter", + "mode": "lines", + "line": {"color": color, "width": 3}, + "x": contour[:, 1], + "y": contour[:, 0], + } + ) + return traces + + +if __name__ == "__main__": + # Note: dev_tools_props_check negatively affects the performance of VolumeSlicer + app.run_server(debug=True, dev_tools_props_check=False) diff --git a/examples/threshold_overlay.py b/examples/threshold_overlay.py index 997039f..5938950 100644 --- a/examples/threshold_overlay.py +++ b/examples/threshold_overlay.py @@ -22,13 +22,6 @@ mi, ma = vol.min(), vol.max() slicer = VolumeSlicer(app, vol) -slicer.graph.config.update( - modeBarButtonsToAdd=[ - "drawclosedpath", - "eraseshape", - ] -) - app.layout = html.Div( [