|
import dash |
|
import dash_core_components as dcc |
|
import dash_html_components as html |
|
import datetime |
|
import random |
|
from dash.dependencies import Output, Input, State |
|
|
|
|
|
app = dash.Dash() |
|
app.css.append_css({'external_url': 'https://codepen.io/chriddyp/pen/bWLwgP.css'}) # noqa: E501 |
|
|
|
graph_names = ["foo", "bar", "baz"] |
|
|
|
|
|
def random_points(length): |
|
return [random.randint(5, 10) for i in range(length)] |
|
|
|
|
|
graph_figures = [{'data': [{'x': [0, 1, 2], 'y': random_points(3), 'type': 'bar', 'name': '{} widgets'.format(name)}, |
|
{'x': [0, 1, 2], 'y': random_points(3), 'type': 'bar', 'name': '{} trinkets'.format(name)}], |
|
'layout': {'title': name}} for name in graph_names] |
|
|
|
pre_style = {"backgroundColor": "#ddd", "fontSize": 20, "padding": "10px", "margin": "10px"} |
|
hidden_style = {"display": "none"} |
|
hidden_inputs = html.Div(id="hidden-inputs", style=hidden_style, children=[]) |
|
|
|
app.layout = html.Div(children=[ |
|
dcc.Graph(id='foo', figure=graph_figures[0], className="four columns"), |
|
dcc.Graph(id='bar', figure=graph_figures[1], className="four columns"), |
|
dcc.Graph(id='baz', figure=graph_figures[2], className="four columns"), |
|
html.Label('Most recent clickdata'), |
|
html.Pre(id='update-on-click-data', style=pre_style), |
|
html.Label('Bar and Baz, but not Foo'), |
|
html.Pre(id='update-on-click-data-bar-baz', style=pre_style), |
|
html.Label('Foo and Bar, but not Baz'), |
|
html.Pre(id='update-on-click-data-foo-bar', style=pre_style), |
|
hidden_inputs |
|
]) |
|
|
|
|
|
def last_clicked(*dash_input_keys): |
|
""" Get the clickData of the most recently clicked graph in a list of graphs. |
|
|
|
The `value` you will receive as a parameter in your callback will be a dict. The keys you will want to |
|
pay attention to are: |
|
- "last_clicked": the id of the graph that was last clicked |
|
- "last_clicked_data": what clickData would usually return |
|
|
|
This function working depends on a `hidden_inputs` variable existing in the global / file scope. It should be an |
|
html.Div() input with styles applied to be hidden ({"display": "none"}). |
|
|
|
but why, I hear you ask? |
|
clickData does not get set back to None after you've used it. That means that if a callback needs the latest |
|
clickData from two different potential clickData sources, if it uses them both, it will get two sets of clickData |
|
and no indication which was the most recent. |
|
|
|
:type dash_input_keys: list of strings representing dash components |
|
:return: dash.dependencies.Input() to watch value of |
|
""" |
|
dash_input_keys = sorted(list(dash_input_keys)) |
|
str_repr = str(dash_input_keys) |
|
last_clicked_id = str_repr + "_last-clicked" |
|
existing_child = None |
|
for child in hidden_inputs.children: |
|
if child.id == str_repr: |
|
existing_child = child |
|
break |
|
|
|
if existing_child: |
|
return dash.dependencies.Input(last_clicked_id, 'value') |
|
|
|
# If we get to here, this is the first time calling this function with these inputs, so we need to do some setup |
|
# make feeder input/outputs that will store the last time a graph was clicked in addition to it's clickdata |
|
if existing_child is None: |
|
existing_child = html.Div(id=str_repr, children=[]) |
|
hidden_inputs.children.append(existing_child) |
|
|
|
input_clicktime_trackers = [str_repr + key + "_clicktime" for key in dash_input_keys] |
|
existing_child.children.append(dcc.Input(id=last_clicked_id, style=hidden_style, value=None)) |
|
for hidden_input_key in input_clicktime_trackers: |
|
existing_child.children.append(dcc.Input(id=hidden_input_key, style=hidden_style, value=None)) |
|
|
|
# set up simple callbacks that just append the time of click to clickData |
|
for graph_key, clicktime_out_key in zip(dash_input_keys, input_clicktime_trackers): |
|
@app.callback(dash.dependencies.Output(clicktime_out_key, 'value'), |
|
[dash.dependencies.Input(graph_key, 'clickData')], |
|
[dash.dependencies.State(graph_key, 'id')]) |
|
def update_clicktime(clickdata, graph_id): |
|
result = { |
|
"click_time": datetime.datetime.now().timestamp(), |
|
"click_data": clickdata, |
|
"id": graph_id |
|
} |
|
return result |
|
|
|
cb_output = dash.dependencies.Output(last_clicked_id, 'value') |
|
cb_inputs = [dash.dependencies.Input(clicktime_out_key, 'value') for clicktime_out_key in input_clicktime_trackers] |
|
cb_current_state = dash.dependencies.State(last_clicked_id, 'value') |
|
|
|
# use the outputs generated in the callbacks above _instead_ of clickData |
|
@app.callback(cb_output, cb_inputs, [cb_current_state]) |
|
def last_clicked_callback(*inputs_and_state): |
|
clicktime_inputs = inputs_and_state[:-1] |
|
last_state = inputs_and_state[-1] |
|
if last_state is None: |
|
last_state = { |
|
"last_clicked": None, |
|
"last_clicked_data": None, |
|
} |
|
else: |
|
largest_clicktime = -1 |
|
largest_clicktime_input = None |
|
for clicktime_input in clicktime_inputs: |
|
click_time = int(clicktime_input['click_time']) |
|
if clicktime_input['click_data'] and click_time > largest_clicktime: |
|
largest_clicktime_input = clicktime_input |
|
largest_clicktime = click_time |
|
if largest_clicktime: |
|
last_state['last_clicked'] = largest_clicktime_input["id"] |
|
last_state['last_clicked_data'] = largest_clicktime_input["click_data"] |
|
return last_state |
|
|
|
return dash.dependencies.Input(last_clicked_id, 'value') |
|
|
|
|
|
@app.callback(dash.dependencies.Output('update-on-click-data', 'children'), |
|
[last_clicked('bar', 'foo', 'baz')]) |
|
def foobarbaz_last_clicked_callback(last_clickdata): |
|
click_data = last_clickdata["last_clicked_data"] |
|
clicked_id = last_clickdata["last_clicked"] |
|
return "{} was last clicked and contains clickdata:\n{}".format(clicked_id, click_data) |
|
|
|
|
|
@app.callback(dash.dependencies.Output('update-on-click-data-bar-baz', 'children'), |
|
[last_clicked('bar', 'baz')]) |
|
def barbaz_last_clicked_callback(last_clickdata): |
|
click_data = last_clickdata["last_clicked_data"] |
|
clicked_id = last_clickdata["last_clicked"] |
|
return "{} was last clicked and contains clickdata:\n{}".format(clicked_id, click_data) |
|
|
|
|
|
@app.callback(dash.dependencies.Output('update-on-click-data-foo-bar', 'children'), |
|
[last_clicked('foo', 'bar')]) |
|
def foobar_last_clicked_callback(last_clickdata): |
|
click_data = last_clickdata["last_clicked_data"] |
|
clicked_id = last_clickdata["last_clicked"] |
|
return "{} was last clicked and contains clickdata:\n{}".format(clicked_id, click_data) |
|
|
|
|
|
if __name__ == '__main__': |
|
app.run_server(host='0.0.0.0', debug=True) |
I just used the store method to store the last clicked...when the graph is clicked you just check to see which clickData changed from what is sitting in the store component (bring it into the callback using State) - seems to work fine and it is only a couple of lines of code.