How to deal with conceptually separate views

Hi! I’m trying to implement a layout where the users can select whether they want to see exponential functions or the sine function. If they chose exponential, they can select the exponent. In addition to this, I would also like to add a button for zooming in on the plots.

Now, I have 3 solutions:

  1. reports Duplicate callback outputs error
  2. reports A nonexistent object was used in an Input of a Dash callback error
  3. works but I’m not sure it scales

In particular, the solution that works eliminates the nonexistent object issue by adding a hidden component with the required id to the layout. For this small example, this solution is fine but I would like to extend this design and adding such dummy hidden components everywhere could clutter the layouts quickly.

More generally what I would like to do is something that can be seen on many pages like this: https://developer.mozilla.org/en-US/docs/Learn. Here, there is a sort of main navigation (an accordion on the left) that brings up the main content (an article). Just like there, I would like to make it possible to for the users to jump between different main contents (graphs). However, the graphs are different and need different controls like selection of exponent. Sometimes, a control is shared by multiple main contents like the zooming feature.

Note that the exp / sin dropdown in my example plays the role of the main navigation.

As a bonus, it would be really nice if navigating the dashboard using the main navigation wouldn’t require the recreation / redrawing / recomputation of the selected main content if it has already been selected before.

So what do you think? What design would you recommend as a solution?

As to the example, let me share the code first that works:

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

n = 100
x = np.linspace(0, n, 10 * n)


def graph_exp(exponent):
    return px.line(x=x, y=x ** exponent)


def graph_sin():
    return px.line(x=x, y=np.sin(x))


app = Dash(__name__)

app.layout = html.Div([
    dcc.Dropdown(options=[{'label': i, 'value': i} for i in ('exp', 'sin')], id='dd-main'),
    html.Div(id='main'),
])


@app.callback(
    Output('main', 'children'),
    Input('dd-main', 'value')
)
def update_main(dd_main_value):
    if dd_main_value is None:
        raise PreventUpdate

    ret = html.Div()
    if dd_main_value == 'exp':
        default = 0
        ret = html.Div([
            dcc.Dropdown(options=[{'label': i, 'value': i} for i in range(5)], value=default, id='dd-exp'),
            dcc.Graph(id='graph', figure=graph_exp(default)),
            html.Button('Zoom', id='zoom')
        ]),
    if dd_main_value == 'sin':
        ret = html.Div([
            dcc.Dropdown(id='dd-exp', style={'display': 'none'}),
            dcc.Graph(id='graph', figure=graph_sin()),
            html.Button('Zoom', id='zoom')
        ])

    return ret


@app.callback(
    Output('graph', 'figure'),
    Input('zoom', 'n_clicks'),
    Input('dd-exp', 'value'),
    State('graph', 'figure')
)
def zoom(zoom_n_clicks, dd_exp_value, garph_figure):
    triggered_id = ctx.triggered_id

    if triggered_id == 'dd-exp':
        return graph_exp(dd_exp_value)
    if triggered_id == 'zoom':
        garph_figure['layout']['xaxis'] = {'range': (zoom_n_clicks, 2 * zoom_n_clicks)}
        return garph_figure

    raise PreventUpdate

This is the code that throws the Duplicate callback outputs error:

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

n = 100
x = np.linspace(0, n, 10 * n)


def graph_exp(exponent):
    return px.line(x=x, y=x ** exponent)


def graph_sin():
    return px.line(x=x, y=np.sin(x))


app = Dash(__name__)

app.layout = html.Div([
    dcc.Dropdown(options=[{'label': i, 'value': i} for i in ('exp', 'sin')], id='dd-main'),
    html.Div(id='main'),
])


@app.callback(
    Output('main', 'children'),
    Input('dd-main', 'value')
)
def update_main(dd_main_value):
    if dd_main_value is None:
        raise PreventUpdate

    ret = html.Div()
    if dd_main_value == 'exp':
        default = 0
        ret = html.Div([
            dcc.Dropdown(options=[{'label': i, 'value': i} for i in range(5)], value=default, id='dd-exp'),
            dcc.Graph(id='graph', figure=graph_exp(default)),
            html.Button('Zoom', id='zoom')
        ]),
    if dd_main_value == 'sin':
        ret = html.Div([
            dcc.Graph(id='graph', figure=graph_sin()),
            html.Button('Zoom', id='zoom')
        ])

    return ret


@app.callback(
    Output('graph', 'figure'),
    Input('dd-exp', 'value')
)
def update_exp(dd_exp_value):
    return graph_exp(dd_exp_value)


@app.callback(
    Output('graph', 'figure'),
    Input('zoom', 'n_clicks'),
    State('graph', 'figure')
)
def zoom(zoom_n_clicks, fig):
    if zoom_n_clicks is None:
        raise PreventUpdate

    fig['layout']['xaxis'] = {'range': (zoom_n_clicks, 2 * zoom_n_clicks)}
    return fig

And here is the code that triggers the nonexistent object error:

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

n = 100
x = np.linspace(0, n, 10 * n)


def graph_exp(exponent):
    return px.line(x=x, y=x ** exponent)


def graph_sin():
    return px.line(x=x, y=np.sin(x))


app = Dash(__name__)

app.layout = html.Div([
    dcc.Dropdown(options=[{'label': i, 'value': i} for i in ('exp', 'sin')], id='dd-main'),
    html.Div(id='main'),
])


@app.callback(
    Output('main', 'children'),
    Input('dd-main', 'value')
)
def update_main(dd_main_value):
    if dd_main_value is None:
        raise PreventUpdate

    ret = html.Div()
    if dd_main_value == 'exp':
        default = 0
        ret = html.Div([
            dcc.Dropdown(options=[{'label': i, 'value': i} for i in range(5)], value=default, id='dd-exp'),
            dcc.Graph(id='graph', figure=graph_exp(default)),
            html.Button('Zoom', id='zoom')
        ]),
    if dd_main_value == 'sin':
        ret = html.Div([
            dcc.Graph(id='graph', figure=graph_sin()),
            html.Button('Zoom', id='zoom')
        ])

    return ret


@app.callback(
    Output('graph', 'figure'),
    Input('zoom', 'n_clicks'),
    Input('dd-exp', 'value'),
    State('graph', 'figure')
)
def zoom(zoom_n_clicks, dd_exp_value, garph_figure):
    if zoom_n_clicks is not None:
        garph_figure['layout']['xaxis'] = {'range': (zoom_n_clicks, 2 * zoom_n_clicks)}
        return garph_figure

    if dd_exp_value is not None:
        return graph_exp(dd_exp_value)

    raise PreventUpdate

Thank you very much!

I guess it is a matter of taste. But I think I like this design the best so far,

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

n = 100
x = np.linspace(0, n, 10 * n)


def graph_exp(exponent):
    return px.line(x=x, y=x ** exponent)


def graph_sin():
    return px.line(x=x, y=np.sin(x))


app = Dash(__name__)
app.layout = html.Div([
    dcc.Dropdown(options=[{'label': i, 'value': i} for i in ('exp', 'sin')], id='dd-main'),
    html.Div(id='main'),
])


@app.callback(
    Output('main', 'children'),
    Input('dd-main', 'value')
)
def update_main(dd_main_value):
    if dd_main_value is None:
        raise PreventUpdate
    if dd_main_value == 'exp':
        default = 0
        return html.Div([
            dcc.Dropdown(options=[{'label': i, 'value': i} for i in range(5)], value=default, id='dd-exp'),
            dcc.Graph(id='graph', figure=graph_exp(default)),
            html.Button('Zoom', id='zoom')
        ]),
    if dd_main_value == 'sin':
        return html.Div([
            dcc.Graph(id='graph', figure=graph_sin()),
            html.Button('Zoom', id='zoom')
        ])
    return html.Div()


@app.callback(
    Output('graph', 'figure'),
    Input('dd-exp', 'value')
)
def update_exp(dd_exp_value):
    return graph_exp(dd_exp_value)


@app.callback(
    Output('graph', 'figure'),
    Input('zoom', 'n_clicks'),
    State('graph', 'figure')
)
def zoom(zoom_n_clicks, fig):
    if zoom_n_clicks is None:
        raise PreventUpdate

    fig['layout']['xaxis'] = {'range': (zoom_n_clicks, 2 * zoom_n_clicks)}
    return fig


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

Conceptionally, I also like that best :slight_smile: But it doesn’t work because of the Duplicate callback outputs error :frowning:

It should work - please try to run the code (I used dash-extensions to avoid that error) :slight_smile:

Ups, I missed the dash-extensions… now I see that it works great. Thank you!