Crossfilter Plotly Tutorial broken?

I am trying to run the basic Crossfiltering tutorial found here Part 4. Interactive Graphing and Crossfiltering | Dash for Python Documentation | Plotly (under Generic Crossfilter Recipe) and am struggling to get it working.

I am encountering two problems:

(1) The following two lines are seen as a syntax error in my Python 3 implementation:

if selectedpoints_local and selectedpoints_local['range']:
if selected_data and selected_data['points']:

When they are run the first time, because both variables selectedpoints_local and selected_data are “None” and thus don’t have a ‘range’ or a ‘points’ key, which causes Python to crash.

(2) I tried fixing this problem with “Try” and “Except” statements, but I am ending up with a functional user interface, except that every time I make a selection, the callback is triggered twice, once with the correct selection, and a second time with an empty selection, causing the graph to refresh with an empty selection. I am really stumped here, and have tried to gradually remove everything from the code to understand why the callback gets triggered twice systematically.

Even this really simplified version of the tutorial shows this problem, where the callback gets triggered twice with a single selection:

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

app = Dash(__name__)

# make a sample data frame with 2 columns
np.random.seed(0)  # no-display
df = pd.DataFrame({"Col " + str(i+1): np.random.rand(30) for i in range(2)})

app.layout = html.Div([
    dcc.Graph(id='g1')
])

def get_figure(df, x_col, y_col):
    fig = px.scatter(df, x=x_col, y=y_col)
    return fig

@app.callback(
    Output('g1','figure'),
    Input('g1', 'selectedData')
)
def callback(selection1):
    print("CALLBACK")
    return get_figure(df, "Col 1", "Col 2")

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

I am under the impression something is wrong with the syntax of the callback in the tutorial, but being unfamiliar with plotly, I have not been able to fix this.

Any help would be greatly appreciated.

I managed to get a modified version that kind of works if I only use the first plot to update the selection of the other two plots in the tutorial.

It seems that having a plot update itself through the Callback causes the double-triggering of the Callback with an empty selection the second time. I am still not sure how the tutorial could be fixed such as to behave like in the provided video…

Here is the code I have now to demonstrate this:

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

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

app = Dash(__name__, external_stylesheets=external_stylesheets)

# make a sample data frame with 6 columns
np.random.seed(0)  # no-display
df = pd.DataFrame({"Col " + str(i+1): np.random.rand(30) for i in range(6)})
selectedpoints = df.index
fig = px.scatter(df, x=df["Col 1"], y=df["Col 2"], text=df.index)
fig.update_traces(selectedpoints=selectedpoints,
                      customdata=df.index,
                      mode='markers+text', marker={ 'color': 'rgba(0, 116, 217, 0.7)', 'size': 20 }, unselected={'marker': { 'opacity': 0.3 }, 'textfont': { 'color': 'rgba(0, 0, 0, 0)' } })

app.layout = html.Div([
    html.Div(
        dcc.Graph(id='g1', config={'displayModeBar': True}, figure=fig),
        className='four columns'
    ),
    html.Div(
        dcc.Graph(id='g2', config={'displayModeBar': False}),
        className='four columns'
        ),
    html.Div(
        dcc.Graph(id='g3', config={'displayModeBar': False}),
        className='four columns'
    )
], className='row')

def get_figure(df, x_col, y_col, selectedpoints, selectedpoints_local):

    #import pdb; pdb.set_trace()
    ok = 0
    if selectedpoints_local:
    	if 'range' in selectedpoints_local.keys():
    		ok = 1
    if ok:
        ranges = selectedpoints_local['range']
        selection_bounds = {'x0': ranges['x'][0], 'x1': ranges['x'][1],
                            'y0': ranges['y'][0], 'y1': ranges['y'][1]}
    else:
        selection_bounds = {'x0': np.min(df[x_col]), 'x1': np.max(df[x_col]),
                            'y0': np.min(df[y_col]), 'y1': np.max(df[y_col])}
    
    #if selectedpoints is not None:
    #	if selectedpoints.shape == (0,):
    #		return None

    # set which points are selected with the `selectedpoints` property
    # and style those points with the `selected` and `unselected`
    # attribute. see
    # https://medium.com/@plotlygraphs/notes-from-the-latest-plotly-js-release-b035a5b43e21
    # for an explanation
    fig = px.scatter(df, x=df[x_col], y=df[y_col], text=df.index)

    fig.update_traces(selectedpoints=selectedpoints,
                      customdata=df.index,
                      mode='markers+text', marker={ 'color': 'rgba(0, 116, 217, 0.7)', 'size': 20 }, unselected={'marker': { 'opacity': 0.3 }, 'textfont': { 'color': 'rgba(0, 0, 0, 0)' } })

    fig.update_layout(margin={'l': 20, 'r': 0, 'b': 15, 't': 5}, dragmode='select', hovermode=False)

    fig.add_shape(dict({'type': 'rect',
                        'line': { 'width': 1, 'dash': 'dot', 'color': 'darkgrey' } },
                       **selection_bounds))
    return fig

# this callback defines 3 figures
# as a function of the intersection of their 3 selections
@app.callback(
    #Output('g1', 'figure'),
    Output('g2', 'figure'),
    Output('g3', 'figure'),
    [Input('g1', 'selectedData'),
    Input('g2', 'selectedData'),
    Input('g3', 'selectedData')]
)
def callback(selection1, selection2, selection3):
    selectedpoints = df.index
    #import pdb; pdb.set_trace()
    for selected_data in [selection1, selection2, selection3]:
        ok = 0
        if selected_data:
            if 'points' in selected_data.keys():
                ok = 1
        #if selected_data:
        #if selected_data and selected_data['points']:
        if ok:
            selectedpoints = np.intersect1d(selectedpoints,
                [p['customdata'] for p in selected_data['points']])
    
    return [#get_figure(df, "Col 1", "Col 2", selectedpoints, selection1),
            get_figure(df, "Col 3", "Col 4", selectedpoints, selection2),
            get_figure(df, "Col 5", "Col 6", selectedpoints, selection3)]


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

Is there an elegant way of preventing g1 from updating itself when a selection in g1 causes the callback ?

Thank you

I am running into the same problem. Is there a version of dash/plotly where this tutorial will work?

For me it worked worked prior to dash 2.6.1.

1 Like

It doesn’t work for me before 2.6.1. Here is my environment, is there something I’m missing? Could this be a plotly problem?

Brotli==1.0.9
click==8.1.3
dash==2.6.0
dash-core-components==2.0.0
dash-html-components==2.0.0
dash-table==5.0.0
Flask==2.2.3
Flask-Compress==1.13
importlib-metadata==6.0.0
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.2
numpy==1.24.2
pandas==1.5.3
plotly==5.13.0
python-dateutil==2.8.2
pytz==2022.7.1
six==1.16.0
tenacity==8.2.1
Werkzeug==2.2.3
zipp==3.14.0

hmm - that’s odd. I created a new venv with the dependencies you listed and it worked fine - for all versions of Dash. :woman_shrugging:

I just changed the get_figure function to:

def get_figure(df, x_col, y_col, selectedpoints, selectedpoints_local):

    if selectedpoints_local:
        if selectedpoints_local['range']:
            ranges = selectedpoints_local['range']
            selection_bounds = {'x0': ranges['x'][0], 'x1': ranges['x'][1],
                                'y0': ranges['y'][0], 'y1': ranges['y'][1]}
        else:
            selection_bounds = {'x0': np.min(df[x_col]), 'x1': np.max(df[x_col]),
                                'y0': np.min(df[y_col]), 'y1': np.max(df[y_col])}
    else:
        selection_bounds = {'x0': np.min(df[x_col]), 'x1': np.max(df[x_col]),
                            'y0': np.min(df[y_col]), 'y1': np.max(df[y_col])}

    # set which points are selected with the `selectedpoints` property
    # and style those points with the `selected` and `unselected`
    # attribute. see
    # https://medium.com/@plotlygraphs/notes-from-the-latest-plotly-js-release-b035a5b43e21
    # for an explanation
    fig = px.scatter(df, x=df[x_col], y=df[y_col], text=df.index)

    fig.update_traces(selectedpoints=selectedpoints,
                      customdata=df.index,
                      mode='markers+text', marker={ 'color': 'rgba(0, 116, 217, 0.7)', 'size': 20 }, unselected={'marker': { 'opacity': 0.3 }, 'textfont': { 'color': 'rgba(0, 0, 0, 0)' } })

    fig.update_layout(margin={'l': 20, 'r': 0, 'b': 15, 't': 5}, dragmode='select', hovermode=False)

    fig.add_shape(dict({'type': 'rect',
                        'line': { 'width': 1, 'dash': 'dot', 'color': 'darkgrey' } },
                       **selection_bounds))
    return fig

and it worked with dash version 2.6.0

@jgagneastro , @lspen, and @AnnMarieW I actually opened a git issue about this problem:

Would help if you give it a thumbs up :grin:

1 Like

Thank you @jgagneastro for reporting.
We are looking into this right now and we’ll post here as soon as we have an update.

1 Like