4

I have a scatter plot in dash, with some on-click callbacks. I would like to display the annotation for a point when I click on it. The annotation should remain visible for any point that has been clicked. Does anyone know if this is possible; how should I approach this? My initial searches did not yield any specific examples or leads.

import json
from textwrap import dedent as d
import dash
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html
import random

userSeq = []


app = dash.Dash(__name__)

styles = {
    'pre': {
        'border': 'thin lightgrey solid',
        'overflowX': 'scroll'
    }
}

app.layout = html.Div([
    dcc.Graph(
        id='basic-interactions',
        figure={
            'data': [
                {
                    'x': [random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100)],
                    'y': [random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100)],
                    'text': ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'],
                    #'customdata': ['c.a', 'c.b', 'c.c', 'c.d'],
                    'name': 'Trace 1',
                    'mode': 'markers+text', 
                    'marker': {'size': 12},
                    'textposition': 'bottom'
                }

            ]
        }
    ),

    html.Div(className='row', children=[

        html.Div([
            dcc.Markdown(d("""
                **Click Data**

                Click on points in the graph.
            """)),
            html.Pre(id='click-data', style=styles['pre']),
        ], className='three columns'),

    ])
])



@app.callback(
    Output('click-data', 'children'),
    [Input('basic-interactions', 'clickData')])
def display_click_data(clickData):
    if clickData != None:
        userSeq.append(clickData['points'][0]['x'])
        print(userSeq)
    return json.dumps(clickData, indent=2)


if __name__ == '__main__':
    app.run_server(debug=True)
user3431083
  • 404
  • 5
  • 19

1 Answers1

0

Maybe it's a bit overkill, but here is what you can do: you redefine the style of the scatter plot annotations in a dash callback.

As far as I know, the only way to do it is to redefine the Figure of the dcc.Graph component.

import json
from textwrap import dedent as d
import dash
import plotly.graph_objs as go
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html
import random

# NOTE: this variable will be shared across users!
userSeq = []

x_coords = [random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100)]
y_coords = [random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100), random.randint(0, 100)]


app = dash.Dash(__name__)

styles = {
    'pre': {
        'border': 'thin lightgrey solid',
        'overflowX': 'scroll'
    }
}


app.layout = html.Div([
    dcc.Graph(
        id='basic-interactions',
        figure={
            'data': [
                {
                    'x': x_coords,
                    'y': y_coords,
                    'text': ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'],
                    #'customdata': ['c.a', 'c.b', 'c.c', 'c.d'],
                    'name': 'Trace 1',
                    'mode': 'markers+text',
                    'marker': {'size': 12},
                    'textposition': 'bottom'
                }

            ],
        },
    ),

    html.Div(className='row', children=[

        html.Div([
            dcc.Markdown(d("""
                **Click Data**

                Click on points in the graph.
            """)),
            html.Pre(id='click-data', style=styles['pre']),
        ], className='three columns'),

    ])
])


@app.callback(
    output=Output('click-data', 'children'),
    inputs=[Input('basic-interactions', 'clickData')])
def display_click_data(clickData):
    if clickData is not None:
        point = clickData['points'][0]
        userSeq.append({'x': point['x'], 'y': point['y']})
        print(userSeq)
    return json.dumps(clickData, indent=2)


@app.callback(
    Output('basic-interactions', 'figure'),
    [Input('basic-interactions', 'clickData')])
def update_annotation_style(clickData):
    """Redefine Figure with an updated style for the scatter plot annotations."""
    data = go.Data([
        go.Scatter(
            x=x_coords,
            y=y_coords,
            text=['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'],
            name='Trace 1',
            mode='markers+text',
            marker={'size': 12},
            textposition='bottom',
        )
    ])
    annotations = []
    for point in userSeq:
        annotation = {
            'x': point['x'],
            'y': point['y'],
            'xref': 'x',
            'yref': 'y',
            'text': '({}; {})'.format(point['x'], point['y']),
            'align': 'center',
            'ay': -15,
            'opacity': 0,
            'bgcolor': 'yellow',
        }
        annotations.append(annotation)

    if clickData is None:
        layout = go.Layout(annotations=annotations)
    else:
        updated_annotations = list(map(lambda ann: {**ann, 'opacity': 1.0}, annotations))
        layout = go.Layout(annotations=updated_annotations)
    figure = go.Figure(data=data, layout=layout)
    return figure


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

There is a bug in my implementation though: the annotations show up for all clicked points, except the current one (so they start to show up when you click two points).

I think this issue is caused by the order the two dash callbacks are run: the one with Output('basic-interactions', 'figure') should run second.

Keep in mind that in your app userSeq is shared across users, so if user A clicks on 3 points in the scatter plot, user B clicks on 2 points in the scatter plot, they will both see 5 annotations.

jackdbd
  • 4,583
  • 3
  • 26
  • 36