clickData for multiple plots

#1

Hi guys,

I am trying to implement an interface where click event on any plot highlights the same data point on another plots. Here is an example:

import dash
from dash.dependencies import Input, Output, State
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objs as go

import numpy as np

app = dash.Dash()
app.css.append_css({'external_url': 'https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css'})

x = np.random.uniform(size=10)
y = np.random.uniform(size=10)
z = np.random.uniform(size=10)

app.layout = html.Div([
html.Div([
    dcc.Graph(
        id='g1', 
        figure=dict(
            data = [go.Scatter(x=x,y=y,mode='markers',showlegend=False)],
            layout = go.Layout(
                xaxis = dict(title = 'X'),
                yaxis = dict(title = 'Y'),
                hovermode = 'closest'))
    )], 
    className="col-4"),
html.Div([
    dcc.Graph(
        id='g2', 
        figure=dict(
            data = [go.Scatter(x=y,y=z,mode='markers',showlegend=False)],
            layout = go.Layout(
                xaxis = dict(title = 'Y'),
                yaxis = dict(title = 'Z'),
                hovermode = 'closest'))
    )], 
    className="col-4"),
html.Div([
    dcc.Graph(
        id='g3', 
        figure=dict(
            data = [go.Scatter(x=x,y=z,mode='markers',showlegend=False)],
            layout = go.Layout(
                xaxis = dict(title = 'X'),
                yaxis = dict(title = 'Z'),
                hovermode = 'closest'))
    )], 
    className="col-4")
],className="row")


@app.callback(
Output('g1', 'figure'),
[Input('g1', 'clickData'), Input('g2', 'clickData'), Input('g3', 'clickData')],
state = [State('g1', 'figure')])
def highligh_points_g1(clickData1, clickData2, clickData3, figure):

    # for temporal usage
    clickData = clickData3

    if clickData != None:
        point_id = clickData['points'][0]['pointNumber']

        point_highlight = go.Scatter(
            x=[x[point_id]],
            y=[y[point_id]],
            mode='markers',
            showlegend=False,
            marker=go.Marker(size=10, line={'width': 1}, color='black', symbol='circle-open')
            )
        if len(figure['data']) == 2:
            figure['data'][1] = point_highlight
        else:
            figure['data'].append(point_highlight)

    return figure


@app.callback(
Output('g2', 'figure'),
[Input('g1', 'clickData'), Input('g2', 'clickData'), Input('g3', 'clickData')],
state = [State('g2', 'figure')])
def highligh_points_g2(clickData1, clickData2, clickData3, figure):
    # ....

@app.callback(
Output('g3', 'figure'),
[Input('g1', 'clickData'), Input('g2', 'clickData'), Input('g3', 'clickData')],
state = [State('g3', 'figure')])
def highligh_points_g3(clickData1, clickData2, clickData3, figure):
    # ...

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

In that example, I use events from third panel only but I would like to be able to handle click events come from any plot. The problem is that clickData is not cleaned like hoverData in case of option clear_on_unhover=True and I cannot choose the last handled clickData.

Does anyone idea how to implement such behavior?

With best regards,
Ivan

#2

Hey @ikatkov - Have you seen this example? It uses selecting instead of clicking, but it might help you out regardless:

Dash Crossfiltering Example

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)
#3

Hi Chris,

Thanks for your reply!

Finally, I have found appropriate solution for desired interface behavior!
Your example helps me to make my code cleaner, but key solution is to use clickData as a trigger of callback function then one can get recently clicked data point through the state keyword from hoverData.

import dash
from dash.dependencies import Input, Output, State
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objs as go

import numpy as np

app = dash.Dash()

app.css.append_css({'external_url': 'https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css'})

xyz = np.random.uniform(size=(10,3))

app.layout = html.Div([
    html.Div(
        dcc.Graph(
            id='g1', 
            clear_on_unhover=True,
            figure = dict(
                data = [go.Scatter(x=xyz[:,0],y=xyz[:,1],mode='markers',showlegend=False)],
                layout = go.Layout(
                    xaxis = dict(title = 'X'), 
                    yaxis = dict(title = 'Y'),
                    hovermode = 'closest')
                )
            ), 
        className="col-4",
        id='div1'
        ),
    html.Div(
        dcc.Graph(
            id='g2', 
            clear_on_unhover=True,
            figure = dict(
                data = [go.Scatter(x=xyz[:,2],y=xyz[:,1],mode='markers',showlegend=False)],
                layout = go.Layout(
                    xaxis = dict(title = 'Z'), 
                    yaxis = dict(title = 'Y'),
                    hovermode = 'closest')
                )
            ), 
        className="col-4",
        id='div2'
        ),
    html.Div(
        dcc.Graph(
            id='g3', 
            clear_on_unhover=True,
            figure = dict(
                data = [go.Scatter(x=xyz[:,2],y=xyz[:,0],mode='markers',showlegend=False)],
                layout = go.Layout(
                    xaxis = dict(title = 'Z'), 
                    yaxis = dict(title = 'X'),
                    hovermode = 'closest')
                )
            ), 
        className="col-4",
        id='div3'
        ),
    ], className="row")

def highlight(x, y):
    def callback(*Datas):

        hoverDatas = Datas[3:6]
        figure = Datas[6]

        # Choose unique a not None element which corresponds to clicked data point
        hoverData = [h for h in hoverDatas if h is not None]

        if len(hoverData) == 1:
            point_id = hoverData[0]['points'][0]['pointNumber']

            point_highlight = go.Scatter(
                x=[xyz[point_id, x]],
                y=[xyz[point_id, y]],
                mode='markers',
                showlegend=False,
                marker=go.Marker(size=10, line={'width': 1}, color='black', symbol='circle-open')
                )

            if len(figure['data']) == 2:
                figure['data'][1] = point_highlight
            else:
                figure['data'].append(point_highlight)

        return figure

    return callback


app.callback(
    Output('g1', 'figure'),
    inputs = [Input('g1', 'clickData'), Input('g2', 'clickData'), Input('g3', 'clickData')],
    state = [State('g1', 'hoverData'), State('g2', 'hoverData'), State('g3', 'hoverData'),
        State('g1', 'figure')]
)(highlight(0, 1))

app.callback(
    Output('g2', 'figure'),
    inputs = [Input('g1', 'clickData'), Input('g2', 'clickData'), Input('g3', 'clickData')],
    state = [State('g1', 'hoverData'), State('g2', 'hoverData'), State('g3', 'hoverData'),
        State('g2', 'figure')]
)(highlight(2, 1))

app.callback(
    Output('g3', 'figure'),
    inputs = [Input('g1', 'clickData'), Input('g2', 'clickData'), Input('g3', 'clickData')],
    state = [State('g1', 'hoverData'), State('g2', 'hoverData'), State('g3', 'hoverData'),
        State('g3', 'figure')]
)(highlight(2, 0))


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

1 Like
#4

Hey everyone, I know it’s been quite a while since this has had activity, but I wrote a gist about my troubles with this issue: https://gist.github.com/shawkinsl/22a0f4e0bf519330b92b7e99b3cfee8a

I’m not sure why, but I found hoverData to be incredible unreliable, so we came up with a different solution. Hope someone in the future will find this useful!

3 Likes
#5

Thank you @shawkinsl for the very extensive writeup!

#6

Hi,
I’m trying to adapt this example to highlight corresponding lines in different plots. I’m wondering, why I actually need ‘hoverData’? I tried to put only the ‘figure’ in State, but without ‘hoverData’ it gives out an error, that the figure is not a list of dash.dependencies.States. I would like to understand why.