Closed havok2063 closed 5 years ago
Have I believe you will have to write three different call backs and use the beta feature of EVENT and STATE to provide input to the function to generate output.
Can you provide a bit more detail on what the EVENT and STATE features are and how to use them? I can't find any information in the documentation about them.
https://gist.github.com/chriddyp/e546a2a2c05df29c868e4cef57fb2105
On Jul 1, 2017 5:51 PM, "Brian Cherinka" notifications@github.com wrote:
Can you provide a bit more detail on what the EVENT and STATE features are and how to use them? I can't find any information in the documentation about them.
— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/plotly/dash/issues/59#issuecomment-312429236, or mute the thread https://github.com/notifications/unsubscribe-auth/AMct3xa2VyA-4x6T2xALNkdBVS3GEy_vks5sJjnTgaJpZM4OK1fi .
Thanks for the link, but this isn't entirely helpful. There's no description of what events and states are. I get what events are, i.e. similar to events in javascript, but what are states? In this example code, they look exactly like Inputs. What's the difference between an Input and a State and how do I use States? Can I use them to distinguish which Input triggers a callback?
@havok2063 States are arguments passed to the function of the callback that won't trigger the callback itself. So changing the code you provided:
@app.callback(
Output('plotA', 'figure'),
[Input('plotB', 'relayoutData')],
[State('plotC', 'relayoutData')])
def display_selected_data(data, datac):
In the above code only changes on the relayoutData of plotB would trigger the callback, changes to the layout of plotC would not trigger the callback itself. plotC's relayoutData will be passed into def display_selected_data
as datac and can be accessed when the callback itself is triggered but layout changes on plotC will not directly trigger the callback.
In terms of what you're trying to do, it might be bit tough to figure out directly which input triggered the callback, but one thing you could do is to have two hidden html.P
element that you tie to separate callbacks and when either plotB changes or plotC changes you set the value of the html.P
element associated with either one to a known value - for example, you could set the element to hold the value of 1 if the plot has changed and a 0 otherwise. You could then pass the paragraph elements as inputs:
@app.callback(
Output('plotA', 'figure'),
[Input('pHolderPlotB', 'children')
Input('pHolderPlotC', 'children'],
[State('plotB', 'relayoutData'), State('plotC', 'relayoutData')])
def display_selected_data(bTrigger, cTrigger, data, datac):
if(int(bTrigger) == 1):
# do this
elif(int(cTrigger) == 1):
# do that
And to reset the values of pHolderPlotB
and pHolderPlotC
after you have redrawn plotA you could create a third callback that will reset the values of both pHolderPlotB
and pHolderPlotC
back to 0 when plotA
has changed.
There might be easier ways to do this but this is one that came to mind.
Let me know if my answer was hard to follow or if I could clarify something.
Great question @havok2063 !
Since only a single Output is allowed in a callback, and all Inputs must feed into it, how can we determine which input is being triggered during a callback?
This isn't possible in Dash.
One core concept with Dash is that the app is described entirely by its current state and not by the order of events. This concept makes its easier to reason about the app's logic and it forces the user interface to be consistent with exactly what you see on the screen (and not by the steps that were taken to get there).
I think that in most cases, the Dash app developer shouldn't need to know the order of events. If they do, it usually means that the component needs to be patched in a way to make it more stateful or the UI needs to be redesigned. I'll leave this issue open to invite examples of good UIs that are impossible to create without supporting "order-of-events".
I have three time-series line plots that I want to cross-filter, and update, any time a range is selected in any of the other plots.
In this case, our app logic shouldn't depend on the most recent action, it should depend on the current state of the other graphs. For example, if Graph A
depends on Graph B
and Graph C
, the data should be filtered by the intersect of the regions selected or zoomed in B
and C
, not just the most recently zoomed.
Here is how I recommend doing crossfiltering for now. A few things to note:
1 - We're using selectedData
instead of zoom data with relayoutData
so that the viewer can see the whole picture and can easily edit their selection.
2 - We're styling the selected graph's points themselves to better indicate which points were selected.
3 - We're displaying the most recently "selectedData" by drawing a rectangle shape (documentation on shapes). This requires a recent version of dash-core-components
: pip install dash-core-components==0.5.3
.
4 - To keep the code DRY 🌴 we're generating the callback functions and calling the decorators directly. For more on generating functions and generators in Python, I recommend this SO answer/essay on decorators.
5 - You're free to customize the style of the selected points or of the non-selected points. In this case, we're dimming the non-selected points by setting opacity=0.1
.
6 - The numbers displayed on top of the points are just the IDs of the points to help you better understand how filtering is working. In practice, we would probably hide these or display a more descriptive ID.
import dash
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html
import numpy as np
import pandas as pd
app = dash.Dash()
df = pd.DataFrame({
'Column {}'.format(i): np.random.rand(50) + i*10
for i in range(6)})
app.layout = html.Div([
html.Div(dcc.Graph(id='g1', selectedData={'points': [], 'range': None}), className="four columns"),
html.Div(dcc.Graph(id='g2', selectedData={'points': [], 'range': None}), className="four columns"),
html.Div(dcc.Graph(id='g3', selectedData={'points': [], 'range': None}), className="four columns"),
], className="row")
def highlight(x, y):
def callback(*selectedDatas):
index = df.index;
for i, hover_data in enumerate(selectedDatas):
selected_index = [
p['customdata'] for p in selectedDatas[i]['points']
if p['curveNumber'] == 0 # the first trace that includes all the data
]
if len(selected_index) > 0:
index = np.intersect1d(index, selected_index)
dff = df.iloc[index, :]
color = 'rgb(125, 58, 235)'
trace_template = {
'marker': {
'color': color,
'size': 12,
'line': {'width': 0.5, 'color': 'white'}
}
}
figure = {
'data': [
dict({
'x': df[x], 'y': df[y], 'text': df.index, 'customdata': df.index,
'mode': 'markers', 'opacity': 0.1
}, **trace_template),
dict({
'x': dff[x], 'y': dff[y], 'text': dff.index,
'mode': 'markers+text', 'textposition': 'top',
}, **trace_template),
],
'layout': {
'margin': {'l': 20, 'r': 0, 'b': 20, 't': 5},
'dragmode': 'select',
'hovermode': 'closest',
'showlegend': False
}
}
shape = {
'type': 'rect',
'line': {
'width': 1,
'dash': 'dot',
'color': 'darkgrey'
}
}
if selectedDatas[0]['range']:
figure['layout']['shapes'] = [dict({
'x0': selectedDatas[0]['range']['x'][0],
'x1': selectedDatas[0]['range']['x'][1],
'y0': selectedDatas[0]['range']['y'][0],
'y1': selectedDatas[0]['range']['y'][1]
}, **shape)]
else:
figure['layout']['shapes'] = [dict({
'type': 'rect',
'x0': np.min(df[x]),
'x1': np.max(df[x]),
'y0': np.min(df[y]),
'y1': np.max(df[y])
}, **shape)]
return figure
return callback
app.css.append_css({"external_url": "https://codepen.io/chriddyp/pen/bWLwgP.css"})
app.callback(
Output('g1', 'figure'),
[Input('g1', 'selectedData'), Input('g2', 'selectedData'), Input('g3', 'selectedData')]
)(highlight('Column 0', 'Column 1'))
app.callback(
Output('g2', 'figure'),
[Input('g2', 'selectedData'), Input('g1', 'selectedData'), Input('g3', 'selectedData')]
)(highlight('Column 2', 'Column 3'))
app.callback(
Output('g3', 'figure'),
[Input('g3', 'selectedData'), Input('g1', 'selectedData'), Input('g2', 'selectedData')]
)(highlight('Column 4', 'Column 5'))
if __name__ == '__main__':
app.run_server(debug=True)
Hi,
I have a somewhat related design issue with the single Output
paradigm:
Part of my app is a simple frame by frame player, so just:
a backward button, a frame number input and a forward button like so:
| < | 00 | > |
Those inputs should then update a graph, showing the desired frame. The callback looks like:
@app.callback(
Output('graph', 'figure')
[Input('frame-num', 'value'),],
events=[Event('backward-button', 'click'),
Event('forward-button', 'click'),]
)
How can I distinguish between the two events (to decrement or increment my frame number)?
Thanks for the good work!
@glyg I'm facing the same issue
@glyg I thinks this is key from @chriddyp 's comment above:
One core concept with Dash is that the app is described entirely by its current state and not by the order of events.
For your example, the frame to display is simply the frame_num
- back_clicks
+ forward_clicks
, right? Basically, where did we start and how many times did we increment/decrement via buttons? At least that's how I'm interpreting it.
app.layout = html.Div([
html.Button('<', id='back_clicks', n_clicks=0),
dcc.Input(id='frame_num', type='number', placeholder='starting frame'),
html.Button('>', id='forward_clicks', n_clicks=0),
html.Div(id='graph')])
@app.callback(
Output('graph', 'children'),
[Input('frame_num', 'value'),
Input('back_clicks', 'n_clicks'),
Input('forward_clicks', 'n_clicks')])
def cback(frame_num, back, forward):
return frame_num - back + forward
Hi @jwhendy, thanks for the suggestion! I thought about it actually but was a bit worried about the behavior at boundaries: if you keep on clicking the back button when you already reached 0, you'd need to click forward as many times without seeing anything happening ....
@glyg ah, I get it now. I don't know dash
well enough to solve that, unfortunately.
Indeed, my mind keeps thinking of separate callbacks per button (which would update the same output) or needing to do something inelegant like hide "helper variables" inside of a div
somewhere.
This feature is being considered through the PrevInput
or PrevState
abstraction in https://github.com/plotly/dash-renderer/pull/25. I'm not sure when this work will be finished. If your organization or company would like to expedite this this work through a sponsorship please reach out.
This is going to be a problem for me, as my app will have buttons with id dynamically generated, and one output area. There may be work-around, but it would mean extra labour.
Since only a single Output is allowed in a callback, and all Inputs must feed into it, how can we determine which input is being triggered during a callback? I have three time-series line plots that I want to cross-filter, and update, any time a range is selected in any of the other plots. For example, the range of Plot A should update when either the range of Plot B or Plot C is updated. But the callback collects the ranges from all plots simultaneously. Is there a way to get the id of which input was triggered during a callback?
Here is my code but it only updates with data from plotB, when it is triggered. When I update Plot C the datac variable is indeed updated but the data from plotB is also passed in again, so the callback function has no knowledge of which plot is actually triggering new input.