Update graph from a number of buttons

Hi! Could you please share your thoughts on this solution to the duplicate callback outputs problem?

I see that I could just draw all curves in a single plot and then activate/deactivate them but that’s not the point. The point is to draw different and possibly completely unrelated plots to the same dcc.Graph depending on what the user wants to see. Actually - and maybe this is an important difference - the point is to draw the plots to the exact same location on the screen. Like switching TV channels: the location is the same, the content different. But it is fine if under the hood, not the channel is changed but, say, multiple devices get swapped out.

Also, what if the plots are heavy and it is likely that the user wants to jump back and forth between them? Would it make more sense to somehow just hide the dcc.Graph components?

import numpy as np
import plotly.express as px
from dash import Dash, Input, Output, callback_context, dcc, html
from dash.exceptions import PreventUpdate

N = 5

app = Dash(__name__)

app.layout = html.Div([
        html.Div(
                [html.Div(
                        html.Button(
                                f'x^{i}',
                                id=f'btn-{i}'
                        )
                ) for i in range(N)]
        ),
        dcc.Graph(id='graph')
]
)


@app.callback(
        Output('graph', 'figure'),
        [Input(f'btn-{i}', 'n_clicks') for i in range(N)]
)
def update_graph(*_):
    ctx = callback_context
    if not ctx.triggered:
        raise PreventUpdate
    else:
        n = ctx.triggered[0]['prop_id'].split('.')[0].split('-')[1]

    x = np.linspace(0, 8, 81)
    y = x ** int(n)
    fig = px.scatter(x=x, y=y)
    return fig


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

Hi @lambdakappatheta

This is a great question! I think you came up with a nice solution.

There are lots of different ways to do this depending on the type of inputs and the complexity of the figure you want to display

Using Tabs - no callback required:
Here is a simple solution where the figure is in the children property of the tab:


import numpy as np
import plotly.express as px
from dash import Dash, dcc, html

N = 5

app = Dash(__name__)


def make_figure(n):
    x = np.linspace(0, 8, 81)
    y = x ** int(n)
    return dcc.Graph(figure=px.scatter(x=x, y=y))


app.layout = html.Div(
    [
        dcc.Tabs(
            [
                dcc.Tab(label=f"x^{i}", value=f"x^{i}", children=make_figure(i))
                for i in range(N)
            ]
        )
    ]
)

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


Callback as a router:

If you have more complex figures, having everything in one callback can make the callback quite lengthy. Here is an example of defining a function for each figure, then use the callback like a router.

This example uses buttons, but it also works with tabs when the content is from a callback rather than simply children as shown above.

import numpy as np
import plotly.express as px
from dash import Dash, Input, Output, callback_context, dcc, html
from dash.exceptions import PreventUpdate

N = 3

app = Dash(__name__)


def make_figure0(n):
    x = np.linspace(0, 8, 81)
    y = x ** int(n)
    return px.scatter(x=x, y=y)


def make_figure1(n):
    x = np.linspace(0, 8, 81)
    y = x ** int(n)
    return px.scatter(x=x, y=y)


def make_figure2(n):
    x = np.linspace(0, 8, 81)
    y = x ** int(n)
    return px.scatter(x=x, y=y)


app.layout = html.Div(
    [
        html.Div([html.Div(html.Button(f"x^{i}", id=f"btn-{i}")) for i in range(N)]),
        dcc.Graph(id="graph"),
    ]
)


@app.callback(
    Output("graph", "figure"), [Input(f"btn-{i}", "n_clicks") for i in range(N)]
)
def update_graph(*_):
    ctx = callback_context
    if not ctx.triggered:
        raise PreventUpdate
    else:
        n = ctx.triggered[0]["prop_id"].split(".")[0].split("-")[1]
    if n == "0":
        return make_figure0(n)
    if n == "1":
        return make_figure1(n)
    if n == "2":
        return make_figure2(n)


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

Pattern Matching Callabacks
If you have a dynamic number of buttons, you could use pattern matching callbacks.

Multiple Callbacks
Break the callbacks into multiple callbacks - one for each button - and avoid the duplicate callback problem by using the MultiplexerTransform() from the dash-extensions library

other
If you have a variety of inputs - ie not just Buttons, but Dropdowns, Input, DatePickerSingle etc, it can be helpful to organize them with Flexible Callback Signatures.

Maybe coming soon?
If the pull request to improve callback_context is approved, it will be easier to determine which input triggered a callback, handle callbacks with lots if inputs, and access the dict id when using pattern matching.

3 Likes