Appending to dcc.Graph trace data by passing clickData to expandData

Hi, everyone!

I’m working on part of a Dash app that I want to use to add points to one trace in a scatterpoint graph by clicking on the graph canvas.

So far, my work has progressed as far as:

  • recognizing I need clickData to trigger on the point I’m clicking
  • creating a second trace in a grid with 0 opacity to trigger clickData
  • writing a callback to print that clickData to the app (partially for troubleshooting purposes)
  • recognizing I need a way to update the graph, preferably just the data rather than running the whole figure generation again
  • finding the dcc.Graph.extendData property, both here and in the docs

I’m currently having problems getting the point from clickData into the graph’s trace; I had to deal with some datatype problems at first, but now, after having printed to terminal to see if the “new” x and y data in the callback is getting the clickData appended, I can’t figure out how to get that data into the graph traces and plotted as output. All code is as follows:

import plotly.graph_objects as go
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input,Output,State
import plotly.graph_objs as go
import numpy as np
np.random.seed(1)

extentLeft = 0.
extentRight = 100.
extentBottom = 0.
extentTop = 60.
stepSize = 0.1
xGridX = np.arange(extentLeft, extentRight, stepSize)
yGridY = np.arange(extentBottom, extentTop, stepSize)
xzx, yzy = np.meshgrid(xGridX, yGridY, sparse=False)
xzx = np.concatenate(xzx, axis=0 )
yzy = np.concatenate(yzy, axis=0 )
GridTrace = go.Scatter(x=xzx, y=yzy, opacity=0)

app = dash.Dash()

x = np.random.uniform(extentLeft, extentRight, 100)
y = np.random.uniform(extentBottom, extentTop, 100)
scatter = go.Figure(go.Scatter(x=x, y=y, mode='markers'))
color = '#a3a7e4'
scatter.update_traces(overwrite=True, marker={"color":color, "size": 10})
scatter.update_layout(hovermode='closest')
scatter.add_trace(GridTrace)

app.layout = html.Div(children=[
    html.H1(id='clickOutput'),
    dcc.Graph(id='exampleGraph',figure=scatter)
])

@app.callback(
    Output('clickOutput','children'),
    Output('exampleGraph','extendData'),
    [Input('exampleGraph','clickData')],
    [State('exampleGraph','figure')]
)
def callback_point(clickData, existing):
    clickPoint = (np.round_([clickData["points"][0]["x"], clickData["points"][0]["y"]], decimals = 2))
    x_new = [existing['data'][0]['x'] + [float(str(clickPoint[0]))]]
    y_new = [existing['data'][0]['y'] + [float(str(clickPoint[1]))]]
    return "{} is the retrieved clickData".format(clickPoint), [dict(x=[[x_new[0]]], y=[[y_new[0]]]), [0]]

if __name__ == '__main__':
    app.run_server(debug=True)

It seems to me like I have too many references to the graph across the callback’s Input, Output and State, but I can’t figure out what else would go there instead. All canonical examples of expandData seem to be triggered by dcc.Interval, but I don’t see why that should be the problem here; it’s just a different triggering event. Any ideas?

Thanks!

The issue in your app is not really related to the data type, but just that you are not using extendData correctly. You should add in the dict just the coordinates to the point you want to add, not the merged existing data and the new point.

So your callback should be actually something like:

@app.callback(
    Output('exampleGraph','extendData'),
    [Input('exampleGraph','clickData')],
    # [State('exampleGraph','figure')] #not needed
)
def callback_point(clickData):
   if not clickData:
     raise dash.exceptions.PreventUpdate
   
   return [dict(x=[[clickData["points"][0]["x"]]], y=[[clickData["points"][0]["y"]]]), [0]]

Note that the double brackets in the dictionary correspond to the trace you are adding the point (outer list) and the x, y coordinates (respectively) of points you are updating. I added a prevent update as a bonus if the callback is triggered on page load or something goes wrong in the click event.

1 Like

Thanks so much! It looks like it’s working perfectly for my purposes; a load off my mind!

Sorry, I didn’t mean to imply the issue was necessarily with the datatype; I’d already had to deal with converting a numpyfloat64 to a float because it wouldn’t append to the trace data that I was copying and trying to reload into the graph.

Thanks again! Have a nice weekend.

1 Like