pythonplotly-dash

python/dash/plotly Updating a figure does not work


I am trying to create two linked plots. When you click a point in the fig_overview plot, the fig_detail plot should change to show a specific view for that.

To my eyes, I have followed this tutorial to the letter. The first plot and the initial dummy plot show up as expected. Unfortunately, the second figure does not update.

The code is running in WSL on Python 3.10.12, jupyterlab 4.1.8, dash 2.17.0.

import numpy as np
import plotly.express as px
from dash import Dash, dcc, html
from dash.dependencies import Input, Output, State
DASH_THEME = dbc.themes.SOLAR
pd.options.plotting.backend = "plotly"

def fig_overview(data):
    n = 30
    fig = px.scatter(x=np.random.random((n,)), y=np.random.random((n,)))
    return fig


def fig_detail(data, task):
    n = 30
    fig = px.scatter(x=np.random.random((n,)), y=np.random.random((n,)))
    return fig

app = Dash('prrdash', external_stylesheets=[dbc.themes.SOLAR])
app.layout = html.Div(children=[
    dcc.Graph(id='fig_overview', figure=fig_overview(None)),
    dcc.Graph(id='fig_detail', figure=fig_detail(None, None))
])

@app.callback(
    Output('fig_detail', 'figure'),
    Input('fig_overview', 'clickData')
)
def select_project(clickData):
    if clickData is not None:
        fig = fig_detail(None, None)
        return fig

app.run(debug=True)

What I have tried so far:

  1. Verifying the task extraction logic: I can print(task) inside select_project() and get the expected result.
  2. Verifying the plot generator fig_detail(): I can even do fig.show() inside the callback and get the expected result.
  3. Vary mode and jupyter_mode of app.run().
  4. Vary the layout. The code already shows the stripped down version of the DOM.
  5. Vary the ids - that shows you how stumped I am. No, changing the names does not change anything.
  6. I do have a work-around: wrapping the diagram in an additional div and replacing its children with a whole new dcc.Graph does work as intended. But I really would love to understand why this simplified approach fails!

Do you have any suggestions for what to try next? Or even just a little pointer on how to debug.

(EDIT: provided full working code example)


Solution

  • I didn't have full code so I had to create own version.
    And I run it with app.run(debug=True) to see problems

    First (in browser) it shows me problem with ['custom']
    but I don't have your data and code so you may not have this problem.

    It seems problem can be what select_project returns.

    If there is no clickData then it (automatically) runs return None
    but if I return fig then code works for me

    def select_project(clickData):
        if clickData is not None:
            print(clickData)
            task = clickData['points'][0]['customdata'][0]
            fig = fig_detail(data, task)
        else:
            fig = fig_detail(data, None)
    
        return fig  # <-- it has to always return figure
    

    Minimal working code with random data directly in code

    from dash import Dash, dcc, html, Input, Output, callback
    import plotly.express as px
    import random
    import pandas as pd
    
    SIZE = 30
    
    random.seed(0)
    data = pd.DataFrame({
        'x': range(SIZE),
        'y': [random.randint(0,100) for _ in range(SIZE)],
        'size': [random.randint(1,5) for _ in range(SIZE)],
        'color': random.choices(['red', 'green', 'blue', 'black', 'white'], k=SIZE),
    })
    
    def fig_overview(data):
        fig = px.scatter(data, x="x", y="y", size="size", color="color")
        return fig
        
    def fig_detail(data, task):
        #print('task:', task)
        
        if task:
            point = task['x']
            temp_data = data[point-1:point+2]
        else:
            temp_data = data
            
        #print('temp_data:', temp_data)
    
        fig = px.scatter(temp_data, x="x", y="y", size="size", color="color")
        return fig
    
    app = Dash(__name__)
    app.layout = html.Div(children=[
        dcc.Graph(id='fig_overview', figure=fig_overview(data)),
        dcc.Graph(id='fig_detail',   figure=fig_detail(data, None))
    ])
    
    
    @app.callback(
        Output('fig_detail', 'figure'),
        Input('fig_overview', 'clickData')
    )
    def select_project(clickData):
        if clickData is not None:
            #print(clickData)
            task = clickData['points'][0]#['customdata'][0]
            fig = fig_detail(data, task)
        else:
            fig = fig_detail(data, None)
            #return None  # <--- if I use it then it stop working but it doesn't show error
    
        return fig  # <-- it has to always return figure
    
    
    if __name__ == '__main__':
        app.run(debug=True)
    

    EDIT:

    Dash has also special method to inform that you don't want to update data - you have to raise error PreventUpdate()

    from dash.exceptions import PreventUpdate
    
    @app.callback(
        Output('fig_detail', 'figure'),
        Input('fig_overview', 'clickData')
    )
    def select_project(clickData):
        if clickData is not None:
            task = clickData['points'][0]#['customdata'][0]
            fig = fig_detail(data, task)
            return fig
    
        raise PreventUpdate()
    

    Doc: Advanced Callbacks | Dash for Python Documentation | Plotly

    Doc also shows that you can use no_update to skip some value in output

    from dash import no_update
    
    @app.callback(
        Output('fig_detail', 'figure'),
        Input('fig_overview', 'clickData')
    )
    def select_project(clickData):
        if clickData is not None:
            task = clickData['points'][0]#['customdata'][0]
            fig = fig_detail(data, task)
            return fig
    
        return no_update
    

    All this because system may execute this function automatically at start (even if you don't select element in first plot).