Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 76 additions & 52 deletions dash_slicer/slicer.py
Original file line number Diff line number Diff line change
Expand Up @@ -574,26 +574,81 @@ def _create_client_callbacks(self):
)

# ----------------------------------------------------------------------
# Callback to rate-limit the index (using a timer/interval).
# Callback to rate-limit the state (using a timer/interval).
# This callback has as input anything that defines the state. The callback
# checks what was changed, sets a timeout, and enables the timer.

app.clientside_callback(
"""
function update_index_rate_limiting(index, relayoutData, n_intervals, info, figure) {
function update_rate_limiting_info(index, relayoutData, n_intervals) {

if (!window._slicer_{{ID}}) window._slicer_{{ID}} = {};
let private_state = window._slicer_{{ID}};
let now = window.performance.now();

// Get whether the slider was moved
// Get whether the slider was moved, layout was changed, or timer ticked
let slider_value_changed = false;
let graph_layout_changed = false;
let timer_ticked = false;
for (let trigger of dash_clientside.callback_context.triggered) {
if (trigger.prop_id.indexOf('slider') >= 0) slider_value_changed = true;
if (trigger.prop_id.indexOf('graph') >= 0) graph_layout_changed = true;
if (trigger.prop_id.indexOf('timer') >= 0) timer_ticked = true;
if (trigger.prop_id.indexOf('graph') >= 0) {
for (let key in relayoutData) {
if (key.startsWith("xaxis.range") || key.startsWith("yaxis.range")) {
graph_layout_changed = true;
}
}
}
}

// Set timeout and whether to disable the timer
let disable_timer = false;
if (slider_value_changed) {
private_state.timeout = now + 200;
} else if (graph_layout_changed) {
private_state.timeout = now + 400; // need longer timeout for smooth scroll zoom
} else if (!n_intervals) {
private_state.timeout = now + 100; // initialize
} else if (!private_state.timeout) {
disable_timer = true;
}

return disable_timer;
}
""".replace(
"{{ID}}", self._context_id
),
Output(self._timer.id, "disabled"),
[
Input(self._slider.id, "value"),
Input(self._graph.id, "relayoutData"),
Input(self._timer.id, "n_intervals"),
],
)

# ----------------------------------------------------------------------
# Callback to produce the (rate-limited) state.
# Note how this callback only has the interval as input. This breaks
# any loops in applications that want to both get and set the slicer
# position.

app.clientside_callback(
"""
function update_state(n_intervals, index, info, figure) {

if (!window._slicer_{{ID}}) window._slicer_{{ID}} = {};
let private_state = window._slicer_{{ID}};
let now = window.performance.now();

// Ready to apply and stop the timer, or return early?
if (!(private_state.timeout && now >= private_state.timeout)) {
return dash_clientside.no_update;
}

// Disable the timer
private_state.timeout = 0;

// Calculate view range based on the volume
let xrangeVol = [
info.offset[0] - 0.5 * info.stepsize[0],
Expand Down Expand Up @@ -625,66 +680,35 @@ def _create_client_callbacks(self):
let xrange = [Math.max(xrangeVol[0], xrangeFig[0]), Math.min(xrangeVol[1], xrangeFig[1])];
let yrange = [Math.max(yrangeVol[0], yrangeFig[0]), Math.min(yrangeVol[1], yrangeFig[1])];

// Initialize return values
let new_state = dash_clientside.no_update;
let disable_timer = false;

// If the slider moved, remember the time when this happened
private_state.new_time = private_state.new_time || 0;


if (slider_value_changed) {
private_state.new_time = now;
private_state.timeout = 200;
} else if (graph_layout_changed) {
private_state.new_time = now;
private_state.timeout = 400; // need longer timeout for smooth scroll zoom
} else if (!n_intervals) {
private_state.new_time = now;
private_state.timeout = 100;
}

// We can either update the rate-limited index timeout ms after
// the real index changed, or timeout ms after it stopped
// changing. The former makes the indicators come along while
// dragging the slider, the latter is better for a smooth
// experience, and the timeout can be set much lower.
if (private_state.timeout && timer_ticked && now - private_state.new_time >= private_state.timeout) {
private_state.timeout = 0;
disable_timer = true;
new_state = {
index: index,
index_changed: false,
xrange: xrange,
yrange: yrange,
zpos: info.offset[2] + index * info.stepsize[2],
axis: info.axis,
color: info.color,
};
if (index != private_state.index) {
private_state.index = index;
new_state.index_changed = true;
}
// Create new state
let new_state = {
index: index,
index_changed: false,
xrange: xrange,
yrange: yrange,
zpos: info.offset[2] + index * info.stepsize[2],
axis: info.axis,
color: info.color,
};
if (index != private_state.last_index) {
private_state.last_index = index;
new_state.index_changed = true;
}

return [new_state, disable_timer];
return new_state;
}
""".replace(
"{{ID}}", self._context_id
),
Output(self._state.id, "data"),
[
Output(self._state.id, "data"),
Output(self._timer.id, "disabled"),
],
[
Input(self._slider.id, "value"),
Input(self._graph.id, "relayoutData"),
Input(self._timer.id, "n_intervals"),
],
[
State(self._slider.id, "value"),
State(self._info.id, "data"),
State(self._graph.id, "figure"),
],
# prevent_initial_call=True,
)

# ----------------------------------------------------------------------
Expand Down
56 changes: 56 additions & 0 deletions examples/get_and_set_position.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""
An example that demonstrates how the slicer's index can be both read and written.
"""

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


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

vol = imageio.volread("imageio:stent.npz")
slicer = VolumeSlicer(app, vol)

# We create a slider as an element that gets set from the slicer's state
# and is also used to set the slicer's position. But this element can be anything,
slider = dcc.Slider(id="slider", max=slicer.nslices)

# Create a store with a specific ID so we can set the slicer position.
setpos_store = dcc.Store(
id={"context": "app", "scene": slicer.scene_id, "name": "setpos"}
)

app.layout = html.Div(
[slicer.graph, slicer.slider, slider, setpos_store, *slicer.stores]
)


# Add callback to listen to changes in state of any slicers with a matching scene_id.
# Note that we could also use a simpler callback using slicer.state as input.
@app.callback(
Output("slider", "value"),
Input({"scene": slicer.scene_id, "context": ALL, "name": "state"}, "data"),
)
def respond_to_slicer_state(states):
for state in states:
if state and state["axis"] == 0:
return state["index"]
return dash.no_update


# Add a callback to set the slicer position.
@app.callback(
Output(setpos_store.id, "data"),
Input("slider", "value"),
)
def set_position_of_all_slicers_with_scene_id(value):
return None, None, value # x, y, z (in scene coordinates)


if __name__ == "__main__":
# Note: dev_tools_props_check negatively affects the performance of VolumeSlicer
app.run_server(debug=True, dev_tools_props_check=False)