Dash clickData when go.layout.dragmode == 'drawrect'

In my dash app, I’ve set dragmode='drawrect' in my figure layout, because I want to be able to click and drag to create shapes. I also want to be able to click individual data points, but it appears that when dragmode='drawrect' (instead of 'zoom'), clickData is not reported, and so you cannot click on individual points.

Can anyone offer a solution for this? I know this has been discussed before, but it seemed unresolved. Any advice would be helpful!

Using the first example provided in the interactive visualizations page as a minimally working example:

import json

from dash import Dash, dcc, html
from dash.dependencies import Input, Output, State
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = Dash(__name__, external_stylesheets=external_stylesheets)

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

df = pd.DataFrame({
    "x": [1, 2, 1, 2],
    "y": [1, 2, 3, 4],
    "customdata": [1, 2, 3, 4],
    "fruit": ["apple", "apple", "orange", "orange"]
})

fig = px.scatter(df, x="x", y="y", color="fruit", custom_data=["customdata"])


drawn_shapes_format = {"drawdirection": "vertical",
                       "layer": "below",
                       "fillcolor": "red",
                       "opacity": 0.51,
                       "line": {"width": 0}}
fig.update_layout(clickmode='event+select', dragmode='drawrect',
                  newshape=drawn_shapes_format,
                  yaxis=dict(fixedrange=True),
                  xaxis=dict(fixedrange=True))

fig.update_traces(marker_size=20)

app.layout = html.Div([
    dcc.Graph(
        id='basic-interactions',
        figure=fig,
        config={'modeBarButtonsToAdd': ["drawrect", "eraseshape"]}
    ),

    html.Div(className='row', children=[
        html.Div([
            dcc.Markdown("""
                **Hover Data**

                Mouse over values in the graph.
            """),
            html.Pre(id='hover-data', style=styles['pre'])
        ], className='three columns'),

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

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

        html.Div([
            dcc.Markdown("""
                **Selection Data**

                Choose the lasso or rectangle tool in the graph's menu
                bar and then select points in the graph.

                Note that if `layout.clickmode = 'event+select'`, selection data also
                accumulates (or un-accumulates) selected data if you hold down the shift
                button while clicking.
            """),
            html.Pre(id='selected-data', style=styles['pre']),
        ], className='three columns'),

        html.Div([
            dcc.Markdown("""
                **Zoom and Relayout Data**

                Click and drag on the graph to zoom or click on the zoom
                buttons in the graph's menu bar.
                Clicking on legend items will also fire
                this event.
            """),
            html.Pre(id='relayout-data', style=styles['pre']),
        ], className='three columns')
    ])
])


@app.callback(
    Output('hover-data', 'children'),
    Input('basic-interactions', 'hoverData'))
def display_hover_data(hoverData):
    return json.dumps(hoverData, indent=2)


@app.callback(
    Output('click-data', 'children'),
    Input('basic-interactions', 'clickData'))
def display_click_data(clickData):
    return json.dumps(clickData, indent=2)


@app.callback(
    Output('selected-data', 'children'),
    Input('basic-interactions', 'selectedData'))
def display_selected_data(selectedData):
    return json.dumps(selectedData, indent=2)


@app.callback(
    Output('relayout-data', 'children'),
    Input('basic-interactions', 'relayoutData'))
def display_relayout_data(relayoutData):
    return json.dumps(relayoutData, indent=2)


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

Hej Scott,

Unfortunately, I don’t have any new suggestions at this time. However, I was able to turn it to an advantage by using a button to change the dragmode. Though I agree with you that it would be great to have ‘draw’ modes be compatible with ClickData.

As suggested by @AIMPED a ‘milder’ version is to use a keyboard stroke to change dragmode instead of a button, which may be an option if you want to avoid the extra button. However, you would need to ensure that the user is aware of the option, what you may not want to rely on.

Best,
ivo

1 Like

Hi @ivo , thanks for your reply! I actually think your solution, using an html.Button component as an Input to a Callback, which changes the dragmode of a dcc.Graph figure layout is quite nice.

I’m going to leave two other possible work arounds here, for anyone in the future who might stumble upon this thread. Again, I’m building off of the first example provided in the interactive visualizations page.

The first example I’ll provide uses the updatemenus property of the figure layout, to allow the user to switch dragmodes between zoom and drawrect.

The second example avoids using any draw mode in dragmode entirely. instead, dragmode is set to select. When dragmode is set to select, clickData is NOT suppressed.

Any new selection by the user is reported in relayoutData. The positions of the new selection can be used to draw a new shape via a callback. Thus, this example could be useful for someone who wants to draw new shapes, and be able to access clickData (without having to toggle between different dragmodes)

Note that both of these examples are limited to drawing rectangle shapes. I think that one could expand on this to draw more complicated shapes or paths, but I’ve not taken the time to look into that.

Example 1: Use updatemenus to toggle between dragmodes

import json

from dash import Dash, dcc, html
from dash.dependencies import Input, Output
import plotly.express as px
import pandas as pd

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = Dash(__name__, external_stylesheets=external_stylesheets)

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

df = pd.DataFrame({
    "x": [1, 2, 1, 2],
    "y": [1, 2, 3, 4],
    "customdata": [1, 2, 3, 4],
    "fruit": ["apple", "apple", "orange", "orange"]
})

fig = px.scatter(df, x="x", y="y", color="fruit", custom_data=["customdata"])


drawn_shapes_format = {"drawdirection": "vertical",
                       "layer": "below",
                       "fillcolor": "red",
                       "opacity": 0.51,
                       "line": {"width": 0}}
fig.update_layout(clickmode='event+select', dragmode='drawrect',
                  newshape=drawn_shapes_format,
                  yaxis=dict(fixedrange=True),
                  xaxis=dict(fixedrange=True))
menus = list([dict(type='buttons', buttons=list([dict(label='use clickData',
                                                       method='relayout',
                                                       args=[{'dragmode':'select'}]),
                                                 dict(label='use drawrect',
                                                      method='relayout',
                                                      args=[{'dragmode':'drawrect'}]
                                                      )
                                                  ])
                    )
              ])
fig.update_layout(updatemenus=menus)

fig.update_traces(marker_size=20)

app.layout = html.Div([
    dcc.Graph(
        id='basic-interactions',
        figure=fig,
        config={'modeBarButtonsToAdd': ["eraseshape"]}
    ),

    html.Div(className='row', children=[
        html.Div([
            dcc.Markdown("""
                **Hover Data**

                Mouse over values in the graph.
            """),
            html.Pre(id='hover-data', style=styles['pre'])
        ], className='three columns'),

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

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

        html.Div([
            dcc.Markdown("""
                **Selection Data**

                Choose the lasso or rectangle tool in the graph's menu
                bar and then select points in the graph.

                Note that if `layout.clickmode = 'event+select'`, selection data also
                accumulates (or un-accumulates) selected data if you hold down the shift
                button while clicking.
            """),
            html.Pre(id='selected-data', style=styles['pre']),
        ], className='three columns'),

        html.Div([
            dcc.Markdown("""
                **Zoom and Relayout Data**

                Click and drag on the graph to zoom or click on the zoom
                buttons in the graph's menu bar.
                Clicking on legend items will also fire
                this event.
            """),
            html.Pre(id='relayout-data', style=styles['pre']),
        ], className='three columns')
    ])
])


@app.callback(
    Output('hover-data', 'children'),
    Input('basic-interactions', 'hoverData'))
def display_hover_data(hoverData):
    return json.dumps(hoverData, indent=2)


@app.callback(
    Output('click-data', 'children'),
    Input('basic-interactions', 'clickData'))
def display_click_data(clickData):
    return json.dumps(clickData, indent=2)


@app.callback(
    Output('selected-data', 'children'),
    Input('basic-interactions', 'selectedData'))
def display_selected_data(selectedData):
    return json.dumps(selectedData, indent=2)


@app.callback(
    Output('relayout-data', 'children'),
    Input('basic-interactions', 'relayoutData'))
def display_relayout_data(relayoutData):
    return json.dumps(relayoutData, indent=2)


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

Example 2: Set dragmode to select, to both draw new shapes and access clickData without having to toggle between dragmodes

Draw new shapes by clicking and dragging, or click on individual data points to highlight them…

import json

from dash import Dash, dcc, html, no_update
from dash.dependencies import Input, Output, State
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = Dash(__name__, external_stylesheets=external_stylesheets)

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

df = pd.DataFrame({
    "x": [1, 2, 1, 2],
    "y": [1, 2, 3, 4],
    "customdata": [1, 2, 3, 4],
    "fruit": ["apple", "apple", "orange", "orange"]
})

fig = px.scatter(df, x="x", y="y", color="fruit", custom_data=["customdata"])


drawn_shapes_format = {"drawdirection": "vertical",
                       "layer": "below",
                       "fillcolor": "crimson",
                       "opacity": 0.51,
                       "line": {"width": 0}}
fig.update_layout(clickmode='event+select', dragmode='select',
                  shapes=[],
                  newshape=drawn_shapes_format,
                  yaxis=dict(fixedrange=True),
                  xaxis=dict(fixedrange=True))

fig.update_traces(marker_size=20)

app.layout = html.Div([
    dcc.Graph(
        id='basic-interactions',
        figure=fig,
        config={'modeBarButtonsToAdd': ["eraseshape"]}
    ),

    html.Div(className='row', children=[
        html.Div([
            dcc.Markdown("""
                **Hover Data**

                Mouse over values in the graph.
            """),
            html.Pre(id='hover-data', style=styles['pre'])
        ], className='three columns'),

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

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

        html.Div([
            dcc.Markdown("""
                **Selection Data**

                Choose the lasso or rectangle tool in the graph's menu
                bar and then select points in the graph.

                Note that if `layout.clickmode = 'event+select'`, selection data also
                accumulates (or un-accumulates) selected data if you hold down the shift
                button while clicking.
            """),
            html.Pre(id='selected-data', style=styles['pre']),
        ], className='three columns'),

        html.Div([
            dcc.Markdown("""
                **Zoom and Relayout Data**

                Click and drag on the graph to zoom or click on the zoom
                buttons in the graph's menu bar.
                Clicking on legend items will also fire
                this event.
            """),
            html.Pre(id='relayout-data', style=styles['pre']),
        ], className='three columns')
    ])
])


# Helper function for drawing new shapes
# edit this dict as needed to your own preferences
def _shape_from_selection(selections, fig):
    return dict(
                editable=True,
                type="rect",
                xref="x",
                yref="y",
                x0=selections[0]['x0'],
                y0=fig['layout']['yaxis']['range'][0],
                x1=selections[0]['x1'],
                y1=fig['layout']['yaxis']['range'][1],
                fillcolor='red',
                opacity=0.51,
                line_width=1,
                line_color='black',
                layer="below")

@app.callback(
    Output('hover-data', 'children'),
    Input('basic-interactions', 'hoverData'))
def display_hover_data(hoverData):
    return json.dumps(hoverData, indent=2)


@app.callback(
    Output('click-data', 'children'),
    Input('basic-interactions', 'clickData'))
def display_click_data(clickData):
    return json.dumps(clickData, indent=2)


@app.callback(
    Output('selected-data', 'children'),
    Input('basic-interactions', 'selectedData'))
def display_selected_data(selectedData):
    return json.dumps(selectedData, indent=2)


@app.callback(
    Output('relayout-data', 'children'),
    Input('basic-interactions', 'relayoutData'))
def display_relayout_data(relayoutData):
    return json.dumps(relayoutData, indent=2)

# THE RELEVANT CALLBACK
@app.callback(
    Output('basic-interactions','figure'),
    Input('basic-interactions','relayoutData'),
    State('basic-interactions', 'figure'),
    prevent_initial_call=True
)
def shape_from_selection(relayout_data, fig):
    if 'selections' in relayout_data:
        assert fig is not None
        new_shape = _shape_from_selection(relayout_data['selections'], fig)
        if 'shapes' in fig['layout']:
            fig['layout']['shapes'] += (new_shape,)
        else:
            fig['layout']['shapes'] = (new_shape,)
        return fig
    else:
        return no_update


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