[Python] Dynamically adding plots to dash

Discussão em 'Python' iniciado por Stack, Agosto 25, 2025.

  1. Stack

    Stack Membro Participativo

    I have managed to do a simple example of plotly and dash where I can develop plotly scripts following a format, and then easily add them to a dash app. For example

    # module_hist.py
    import numpy as np
    import plotly.graph_objects as go


    def plot_hist(n=1000, bins=30, seed=0):
    rng = np.random.default_rng(int(seed))
    data = rng.normal(0, 1, int(n))
    fig = go.Figure(go.Histogram(x=data, nbinsx=int(bins), name="hist"))
    fig.update_layout(margin=dict(l=40, r=20, t=40, b=40))
    return fig

    plot_hist_schema = {
    "n": {"type": "int", "default": 1000, "min": 100, "max": 10000, "step": 100, "help": "Sample size"},
    "bins": {"type": "int", "default": 30, "min": 5, "max": 100, "step": 1, "help": "Histogram bins"},
    "seed": {"type": "int", "default": 0, "help": "Random seed"},
    }

    if __name__ == "__main__":
    import fire

    def cli(n=1000, bins=30, seed=0):
    fig = plot_hist(n=n, bins=bins, seed=seed)
    fig.show()

    fire.Fire(cli)


    module_line.py

    # module_line.py
    import numpy as np
    import plotly.graph_objects as go


    def plot_line(n=50, noise=0.1, seed=0):
    rng = np.random.default_rng(int(seed))
    x = np.arange(n)
    y = np.sin(x / 5) + noise * rng.standard_normal(n)
    fig = go.Figure(go.Scatter(x=x, y=y, mode="lines+markers", name="line"))
    fig.update_layout(margin=dict(l=40, r=20, t=40, b=40))
    return fig


    # Parameter schema for auto-form generation
    plot_line_schema = {
    "n": {"type": "int", "default": 50, "min": 10, "max": 500, "step": 10, "help": "Number of points"},
    "noise": {"type": "float", "default": 0.1, "min": 0, "max": 1, "step": 0.05, "help": "Noise scale"},
    "seed": {"type": "int", "default": 0, "help": "Random seed"},
    }

    if __name__ == "__main__":
    import fire

    # fire.Fire(plot_line) # e.g. python module_line.py --n=200 --noise=0.2 --seed=42
    def cli(n=50, noise=0.1, seed=0):
    fig = plot_line(n=n, noise=noise, seed=seed)
    fig.show() # <--- here, in CLI wrapper only

    fire.Fire(cli)


    and then a dash app

    # app.py
    from dash import Dash, dcc, html, Input, Output
    from module_line import plot_line
    from module_hist import plot_hist

    app = Dash(__name__)
    app.layout = html.Div(
    [
    html.H2("Reusable Plotly Modules in Dash"),
    dcc.Tabs(
    id="tab",
    value="line",
    children=[
    dcc.Tab(
    label="Line",
    value="line",
    children=[
    html.Div(
    [
    html.Label("n"),
    dcc.Slider(
    id="n-line", min=10, max=500, step=10, value=50
    ),
    html.Label("noise"),
    dcc.Slider(
    id="noise", min=0, max=1, step=0.05, value=0.1
    ),
    html.Label("seed"),
    dcc.Input(id="seed-line", type="number", value=0),
    dcc.Graph(id="fig-line"),
    ],
    style={"padding": "1rem"},
    )
    ],
    ),
    dcc.Tab(
    label="Histogram",
    value="hist",
    children=[
    html.Div(
    [
    html.Label("n"),
    dcc.Slider(
    id="n-hist",
    min=100,
    max=10000,
    step=100,
    value=1000,
    ),
    html.Label("bins"),
    dcc.Slider(id="bins", min=5, max=100, step=1, value=30),
    html.Label("seed"),
    dcc.Input(id="seed-hist", type="number", value=0),
    dcc.Graph(id="fig-hist"),
    ],
    style={"padding": "1rem"},
    )
    ],
    ),
    ],
    ),
    ]
    )


    @app.callback(
    Output("fig-line", "figure"),
    Input("n-line", "value"),
    Input("noise", "value"),
    Input("seed-line", "value"),
    )
    def update_line(n, noise, seed):
    return plot_line(n=n, noise=noise, seed=seed)


    @app.callback(
    Output("fig-hist", "figure"),
    Input("n-hist", "value"),
    Input("bins", "value"),
    Input("seed-hist", "value"),
    )
    def update_hist(n, bins, seed):
    return plot_hist(n=n, bins=bins, seed=seed)


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


    It works great. I have a tabbed interface and in each tab the plotly plot.

    I am trying to do it more dynamic, so that I dont have to rewrite the dash if I have a new plotly plot But I havent been able to do it. First I get this weird message (in all my years doing dash I have never seen this kind of error)

    [​IMG]

    and the plots dont get actualized etc. My failing script is this one

    from dash import Dash, dcc, html, Input, Output, State
    import dash

    from module_line import plot_line, plot_line_schema
    from module_hist import plot_hist, plot_hist_schema

    # Registry of available modules
    MODULES = {
    "Line Plot": {"func": plot_line, "schema": plot_line_schema},
    "Histogram": {"func": plot_hist, "schema": plot_hist_schema},
    }

    app = Dash(__name__)

    def make_controls(schema, prefix):
    """Generate Dash controls from schema dict"""
    controls = []
    for name, meta in schema.items():
    cid = f"{prefix}-{name}"
    label = html.Label(f"{name} ({meta.get('help','')})")
    t = meta["type"]
    if t in ("int", "float"):
    controls.append(label)
    controls.append(
    dcc.Slider(
    id=cid,
    min=meta.get("min", 0),
    max=meta.get("max", 100),
    step=meta.get("step", 1),
    value=meta["default"],
    tooltip={"placement": "bottom"}
    )
    )
    else: # fallback to text input
    controls.append(label)
    controls.append(
    dcc.Input(id=cid, type="text", value=str(meta["default"]))
    )
    return html.Div(controls, style={"padding": "1rem"})


    # Layout
    app.layout = html.Div([
    html.H2("Pluggable Plotly Modules"),
    dcc.Dropdown(
    id="module-select",
    options=[{"label": k, "value": k} for k in MODULES],
    value=list(MODULES.keys())[0],
    ),
    html.Div(id="controls-area"),
    html.Button("Update Plot", id="update-btn"),
    dcc.Graph(id="output-figure")
    ])

    @app.callback(
    Output("controls-area", "children"),
    Input("module-select", "value")
    )
    def update_controls(module_name):
    schema = MODULES[module_name]["schema"]
    return make_controls(schema, prefix=module_name)


    @app.callback(
    Output("output-figure", "figure"),
    Input("update-btn", "n_clicks"),
    State("module-select", "value"),
    State("controls-area", "children")
    )
    def update_plot(n_clicks, module_name, controls_state):
    if not n_clicks:
    return dash.no_update
    schema = MODULES[module_name]["schema"]
    func = MODULES[module_name]["func"]

    # Collect parameter values from State dynamically
    ctx = dash.callback_context
    inputs = {}
    # for name in schema:
    # comp_id = f"{module_name}-{name}"
    # val = ctx.states.get(f"{comp_id}.value")
    # # Cast based on schema type
    # t = schema[name]["type"]
    # if t == "int":
    # val = int(val)
    # elif t == "float":
    # val = float(val)
    # inputs[name] = val
    for name, meta in schema.items():
    comp_id = f"{module_name}-{name}"
    val = ctx.states.get(f"{comp_id}.value")

    if val is None:
    val = meta["default"]

    t = meta["type"]
    try:
    if t == "int":
    val = int(val)
    elif t == "float":
    val = float(val)
    # else leave as string
    except Exception:
    val = meta["default"]

    inputs[name] = val

    return func(**inputs)


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


    Can someone tell me

    1. what is wrong? what is that weird error message
    2. Even disregarding my attempt, how can I dinamically add plots in my dash app? (it doesnt necessarily have to be tabs as you see)

    To add complexity I am thinking of adding also lists to the types of entry, but that is besides this question scope. Just letting it be known

    Continue reading...

Compartilhe esta Página