I am attempting to use the “plotly-resampler” library for some timeseries plots. If I run one of their example scripts as a stand-alone dashboard it works just fine. However, if I convert the same python file into a page in a python dashboard with the “use_pages” option, the behaviour of the callback that handles the “FigureResampler” update gives the incorrect output. Specifically, In the example code I included below, the “update_fig” function should receive a “FigureResampler” object from State storage. I get the correct input in the single page example but in the multi-page implementation what the callback receives as input appears to be the contents of the “FIgureResampler” object rather than the object itself. My best guess is that there is something different about the single/multi page implementations in the way that callbacks get serialized/de-serialized and this breaks the implementation of the resampler. Unfortunately, this specific library relies on a few libraries (i.e. Plotly Dash, plotly-resampler, trace-updater and dash-extensions) so I’m struggling to track down where exactly the change is happening. I’ve included a basic example of the modified example as a separate page (e.g. example.py which is called as a page from a main script called something like app.py). Am I just making some basic mistake about how dash subpages work or is there a workaround for this kind of problem? I’ve included a minimum example below if you can help me debug this. Cheers!
The original working code: plotly-resampler/examples/dash_apps/03_minimal_cache_dynamic.py at main · predict-idlab/plotly-resampler · GitHub
My attempt at modifying it to work in “use_pages” dashboard below:
app.py
import dash
from dash import html
from dash.exceptions import PreventUpdate
from dash_extensions.enrich import (
DashProxy,
ServersideOutputTransform,
)
app = DashProxy(__name__, use_pages=True, transforms=[ServersideOutputTransform()])
app.title = "Example"
app.layout = html.Div(children=[dash.page_container])
if __name__ == "__main__":
app.run(port=8050, host="0.0.0.0", debug=True)
pages/example.py
"""Minimal dynamic dash app example.
Click on a button, and draw a new plotly-resampler graph of two noisy sinusoids.
This example uses pattern-matching callbacks to update dynamically constructed graphs.
The plotly-resampler graphs themselves are cached on the server side.
The main difference between this example and 02_minimal_cache.py is that here, we want
to cache using a dcc.Store that is not yet available on the client side. As a result we
split up our logic into two callbacks: (1) the callback used to construct the necessary
components and send them to the client-side, and (2) the callback used to construct the
actual plotly-resampler graph and cache it on the server side. These two callbacks are
chained together using the dcc.Interval component.
"""
from typing import List
from uuid import uuid4
import numpy as np
import plotly.graph_objects as go
import dash
from dash import MATCH, Input, Output, State, dcc, html, no_update, callback
from dash_extensions.enrich import (
DashProxy,
ServersideOutput,
ServersideOutputTransform,
Trigger,
TriggerTransform,
)
from trace_updater import TraceUpdater
from plotly_resampler import FigureResampler
dash.register_page(__name__)
# Data that will be used for the plotly-resampler figures
x = np.arange(2_000_000)
noisy_sin = (3 + np.sin(x / 200) + np.random.randn(len(x)) / 10) * x / 1_000
layout = html.Div(
[
html.Div(children=[html.Button("Add Chart", id="add-chart", n_clicks=0)]),
html.Div(id="container", children=[]),
]
)
# ------------------------------------ DASH logic -------------------------------------
# This method adds the needed components to the front-end, but does not yet contain the
# FigureResampler graph construction logic.
@callback(
Output("container", "children"),
Input("add-chart", "n_clicks"),
State("container", "children"),
prevent_initial_call=True,
)
def add_graph_div(n_clicks: int, div_children: List[html.Div]):
uid = str(uuid4())
new_child = html.Div(
children=[
# The graph and its needed components to serialize and update efficiently
# Note: we also add a dcc.Store component, which will be used to link the
# server side cached FigureResampler object
dcc.Graph(id={"type": "dynamic-graph", "index": uid}, figure=go.Figure()),
dcc.Loading(dcc.Store(id={"type": "store", "index": uid})),
TraceUpdater(id={"type": "dynamic-updater", "index": uid}, gdID=f"{uid}"),
# This dcc.Interval components makes sure that the `construct_display_graph`
# callback is fired once after these components are added to the session
# its front-end
dcc.Interval(
id={"type": "interval", "index": uid}, max_intervals=1, interval=1
),
],
)
div_children.append(new_child)
return div_children
# This method constructs the FigureResampler graph and caches it on the server side
@callback(
ServersideOutput({"type": "store", "index": MATCH}, "data"),
Output({"type": "dynamic-graph", "index": MATCH}, "figure"),
State("add-chart", "n_clicks"),
Trigger({"type": "interval", "index": MATCH}, "n_intervals"),
prevent_initial_call=True,
)
def construct_display_graph(n_clicks, n_trigger) -> FigureResampler:
fig = FigureResampler(go.Figure(), default_n_shown_samples=2_000)
# Figure construction logic based on a state variable, in our case n_clicks
sigma = n_clicks * 1e-6
fig.add_trace(dict(name="log"), hf_x=x, hf_y=noisy_sin * (1 - sigma) ** x)
fig.add_trace(dict(name="exp"), hf_x=x, hf_y=noisy_sin * (1 + sigma) ** x)
fig.update_layout(title=f"<b>graph - {n_clicks}</b>", title_x=0.5)
return fig, fig
@callback(
Output({"type": "dynamic-updater", "index": MATCH}, "updateData"),
Input({"type": "dynamic-graph", "index": MATCH}, "relayoutData"),
State({"type": "store", "index": MATCH}, "data"),
prevent_initial_call=True,
memoize=True,
)
def update_fig(relayoutdata: dict, fig: FigureResampler):
if fig is not None:
return fig.construct_update_data(relayoutdata)
return no_update
pyproject.toml
[tool.poetry]
name = "example"
version = "0.1.0"
description = ""
authors = ["Author"]
[tool.poetry.dependencies]
python = "^3.10"
plotly = "^5.15.0"
dash = "^2.11.0"
dash-iconify = "^0.1.2"
dash-mantine-components = "^0.12.1"
trace-updater = "^0.0.9.1"
dash-extensions = "<1.0.0"
gunicorn = "^20.1.0"
plotly-resampler = "^0.8.3.2"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"