Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Choosing the last clickData event from a list of graphs in plot.ly dash

I've been working with plot.ly dash a bit lately, and let me tell you: this tool is incredible. I won't spend too long gushing about it, but definitely check it out if you ever need to draw graphs, but don't want to have to learn a million javascript graphing libraries to get it done. This gist will assume you have a decent base knowledge of plot.ly dash, so if you don't have much experience, you probably will want to go play around with it some.

While composing various interactive graphs, I found that I needed a way to use clickData events from multiple graphs as inputs to a callback, so that when a user clicked on any of three graphs, some other graph could redraw using a subset of data from whichever graph was clicked. This starts out sounding like a pretty simple problem. Let's take a whack at it. (Note: if you'd like to skip straight to the final solution, click here)

(note: here are our requirements.txt for this code)

dash==0.21.0  # The core dash backend
dash-renderer==0.11.3  # The dash front-end
dash-html-components==0.9.0  # HTML components
dash-core-components==0.21.0  # Supercharged components
plotly # Latest Plotly graphing library
import dash
import dash_core_components as dcc
import dash_html_components as html
import pprint
import random
from dash.dependencies import Output, Input
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 _ 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]
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('Data you clicked on!'),
html.Pre(id='update-on-click-data', style={"backgroundColor": "#ddd", "fontSize": 20, "padding": "10px"}),
])
@app.callback(Output('update-on-click-data', 'children'),
[Input('foo', 'clickData'),
Input('bar', 'clickData'),
Input('baz', 'clickData')])
def update_pre_callback(foo_click_data, bar_click_data, baz_click_data):
if foo_click_data:
return pprint.pformat(foo_click_data)
elif bar_click_data:
return pprint.pformat(bar_click_data)
elif baz_click_data:
return pprint.pformat(baz_click_data)
return "Nothing clicked yet! :)"
if __name__ == '__main__':
app.run_server(host='0.0.0.0', debug=True)

Looks pretty good, no? Play around with it for a bit, and it should be obvious what's wrong, but in case you can't find it: for some reason, after selecting any graph once, only clicking on the first graph causes our pre to update. What gives?

bad demo.gif

Well, I bet you expected (like I did) that a clickData from a graph would reasonably be None if it wasn't the graph that was clicked. Surprise! It isn't. Let's see what's actually coming back on each click:

def update_pre_callback(foo_click_data, bar_click_data, baz_click_data):
    result = "foo clickdata: {}\n".format(foo_click_data)
    result += "bar clickdata: {}\n".format(bar_click_data)
    result += "baz clickdata: {}".format(baz_click_data)
    return result

Click around the graphs, and eventually you'll start to see output like this:

foo clickdata: {'points': [{'curveNumber': 0, 'pointNumber': 2, 'pointIndex': 2, 'x': 2, 'y': 7}, ...]}
bar clickdata: {'points': [{'curveNumber': 0, 'pointNumber': 0, 'pointIndex': 0, 'x': 0, 'y': 6}, ...]}
baz clickdata: {'points': [{'curveNumber': 0, 'pointNumber': 1, 'pointIndex': 1, 'x': 1, 'y': 10}, ...]}

This is obviously pretty silly; we know that users can't physically click on three things at the same time. But it turns out that clickData is not invalidated at the end of each event, and remains its previous value until the next time it changes. This is problematic if you want to take clickData events from multiple sources, but only act on the latest.

There are a couple questions and answers from plot.ly regarding this, and the proposed solutions I've found entail:

  1. redesigning your graphs to rely only on "current state and not... the order of events" 1
  2. using selections instead of clickData 1 2
  3. using clickData as an Input, but couple it with hoverData as a State (since you can make hoverData invalidate itself whenever the element is not actively being hovered over) 1

For us, 1. was not palatable given the time restrictions of our project--we wanted a graph that is useful now, not a graph that follows every dash design rule in 2 weeks. 2. Was also not something we were willing to consider, just because our users preferred the intuitive nature of clicking rather than switching to the selector tool--not every user was guaranteed to be well versed with plot.ly. And we did try 3. but found hoverData to be incredibly unreliable. Perhaps it won't be for you. YMMV. At any rate, we needed something else.

Our first idea was to keep track of the last clickData value for each graph, so that we would know when the selection on a graph had changed. Let's look at that!

import dash
import dash_core_components as dcc
import dash_html_components as html
import pprint
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 _ 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]
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('Data you clicked on!'),
html.Pre(id='update-on-click-data', style={"backgroundColor": "#ddd", "fontSize": 20, "padding": "10px"}),
dcc.Input(id='last-clickdata-hidden', style={"display": "none"})
])
@app.callback(Output('update-on-click-data', 'children'),
[Input('last-clickdata-hidden', 'value')])
def update_pre_callback(last_clicked_data):
return pprint.pformat(last_clicked_data["last_clicked"])
@app.callback(Output('last-clickdata-hidden', 'value'),
[Input('foo', 'clickData'),
Input('bar', 'clickData'),
Input('baz', 'clickData')],
[State('last-clickdata-hidden', 'value')])
def update_last_callback(foo_click_data, bar_click_data, baz_click_data, current_value):
if current_value is None:
current_value = {
"foo": None,
"bar": None,
"baz": None,
"last_clicked": {
"id": "N/A",
"value": "Nothing clicked yet"
}
}
else:
if current_value["foo"] != foo_click_data:
current_value["last_clicked"]["id"] = "foo"
current_value["last_clicked"]["value"] = foo_click_data
elif current_value["bar"] != bar_click_data:
current_value["last_clicked"]["id"] = "bar"
current_value["last_clicked"]["value"] = bar_click_data
elif current_value["baz"] != baz_click_data:
current_value["last_clicked"]["id"] = "baz"
current_value["last_clicked"]["value"] = baz_click_data
current_value["foo"] = foo_click_data
current_value["bar"] = bar_click_data
current_value["baz"] = baz_click_data
return current_value
if __name__ == '__main__':
app.run_server(host='0.0.0.0', debug=True)

This looks pretty good! And even better, it seems like it works! Challenge mode... find the bug!

Need a hint? Try changing random_points(3) in lines 19 & 20 to random_points(1) and see how the graph behaves.

closer_example.gif

So. It's not enough to know which datapoint was the last to be clicked within a graph, because we might return to that graph after having clicked around in other graphs, and expect to be able to click on that same point again, and still cause an update to trigger. In this design, that doesn't work.

At this point, we hacked on this iteration for a while. Probably a few hours. It seems so close! But we eventually slammed our fists on the table and exclaimed in our best Raymond Hettinger voices: "There must be a better way!" It took simplifying the problem enough to ask a coworker who had never seen this code how they'd solve it before I figured it out, but it turns out, there is a better way! And to figure out what it is, we need to return to the statement of the problem we're trying to solve:

Given a three values that can change over time, and fire events whether or not they changed, decide which was the most recent to fire. What are we missing? Well, we don't know when the events fire! But we really only need to add one more intermediate callback layer between our clickData's and our outputs to be able to know when an event is fired. We can write a callback that attaches a datetime.now() alongside the clickData before passing the output along to another callback. Then the problem becomes super super simple: just find the element that has the largest timestamp! Let's see what that looks like:

import dash
import dash_core_components as dcc
import dash_html_components as html
import datetime
import json
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 _ 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"}
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('Foo clickDatas'),
html.Pre(id='foo-click-datas', style=pre_style),
html.Label('Bar clickDatas'),
html.Pre(id='bar-click-datas', style=pre_style),
html.Label('Baz clickDatas'),
html.Pre(id='baz-click-datas', style=pre_style),
html.Label('Most recent clickdata'),
html.Pre(id='update-on-click-data', style=pre_style),
])
for name in graph_names:
@app.callback(Output('{}-click-datas'.format(name), 'children'),
[Input(name, 'clickData')],
[State('{}-click-datas'.format(name), 'id'), State('{}-click-datas'.format(name), 'children')])
def graph_clicked(click_data, clicked_id, children):
if not children:
children = []
if click_data is None:
return []
else:
click_data["time"] = int(datetime.datetime.now().timestamp())
click_data["id"] = clicked_id
children.append(json.dumps(click_data) + "\n")
children = children[-3:]
return children
@app.callback(Output('update-on-click-data', 'children'),
[Input("{}-click-datas".format(name), 'children') for name in graph_names])
def determine_last_click(*clickdatas):
most_recent = None
for clickdata in clickdatas:
if clickdata:
last_child = json.loads(clickdata[-1].strip())
if clickdata and (most_recent is None or int(last_child['time']) > json.loads(most_recent)['time']):
most_recent = json.dumps(last_child)
return most_recent
if __name__ == '__main__':
app.run_server(host='0.0.0.0', debug=True)

This looks pretty good too! And even better: it works! Try out the test case that failed before, and you should see that this version handles it correctly! However, we're still passing a lot of unnecessary data around, and this is still a lot of code to write (and rewrite), especially if you have multiple groups of graphs from which you want to "group" clickData events like this. Let's channel Mr. Hettinger again: There must be a better way!

I'll spare you the walkthrough of this solution, but the main takeaway is that by using this one code block that generates a bunch of unique hidden inputs and callbacks on those inputs in a loop, you can listen to only the last clickData from groups of graph clickData events in a single line of code!

@app.callback(Output('update-on-click-data-foo-bar', 'children'),
              [last_clicked('foo', 'bar')])  # one line!
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)

For a fun challenge, we leave reducing the amount of data passed across callbacks as an exercise for the reader.

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),
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 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(Output(clicktime_out_key, 'value'),
[Input(graph_key, 'clickData')],
[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 = Output(last_clicked_id, 'value')
cb_inputs = [Input(clicktime_out_key, 'value') for clicktime_out_key in input_clicktime_trackers]
cb_current_state = 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 Input(last_clicked_id, 'value')
@app.callback(Output('update-on-click-data', 'children'),
[last_clicked('bar', 'foo', 'baz')])
def update_onclick_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)

You can even chain them together in nutty ways. Want one output to match clicks from foo, bar, or baz, and one to match only bar and baz, but not foo? Maybe also one that only matches foo and bar, but never baz? That works too!

best_combos_demo.gif

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)

Hope this code helps gets someone out of a hole, the way it did for me. This gist is licensed under wtfpl.

@davidolmo

This comment has been minimized.

Copy link

davidolmo commented Nov 7, 2018

This code helped a lot. Thanks for sharing

@luizpvas

This comment has been minimized.

Copy link

luizpvas commented Dec 14, 2018

Amazing writing with great details. I love when people share the problem and thought process and not just the solution. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.