Hi,
I am making a dashboard where I want to visualize a large timeseries dataset. Currently the user can upload a datafile and plots are generated sorted by physical quantity (e.g. plot all temperatures together, plot all pressures together). This works perfectly with the resampler!
Now I want to add the functionality where the x-axis of all plots zoom when the user zooms in one of the plots. I created the following (non-)working example
from uuid import uuid4
from dash import dcc, ctx, ALL, MATCH, no_update
from dash import html
from dash_extensions.enrich import Dash, ServersideOutput, Output, Input, State, Trigger, DashProxy, TriggerTransform, ServersideOutputTransform, MultiplexerTransform
import pandas as pd
import numpy as np
import plotly.io as pio
import plotly.graph_objects as go
from plotly_resampler import FigureResampler
from trace_updater import TraceUpdater
pio.renderers.default='browser'
pd.options.plotting.backend = "plotly"
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = Dash(__name__,external_stylesheets=external_stylesheets)\
app = DashProxy(
__name__,
suppress_callback_exceptions=True,
external_stylesheets=external_stylesheets,
transforms=[ServersideOutputTransform(), TriggerTransform() ,MultiplexerTransform()],
)
app.layout = html.Div([
html.Button("plot", id="btn-plot"),
dcc.Store(id="store-temp"),
html.Div(id='graph-container'),
])
@app.callback(
ServersideOutput("store-temp", "data"),
Input("btn-plot", "n_clicks"),
)
def store_data(click):
print('store data')
n=10000
x = np.arange(n)
df=pd.DataFrame()
y1 = (np.sin(x / 200) * 1 + np.random.randn(n) / 10 * 1 )
y2 = (np.sin(x / 100) * 1 + np.random.randn(n) / 20 * 1 )
df['y1']=y1
df['y2']=y2
return df
@app.callback(
Output("graph-container", "children"),
State("graph-container", "children"),
Input("store-temp", "data"),
prevent_initial_call=True
)
def create_graphs(gc_children,df):
print('creating graphs')
gc_children = [] if gc_children is None else gc_children
uid = str(uuid4())
diff_container = 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.Store(id={"type": "store", "index": uid}),
dcc.Store(id={"type": "store-columns", "index": uid},data=['y1','y2']),
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
),
],
)
gc_children.append(diff_container)
uid = str(uuid4())
diff_container = html.Div(
children=[
dcc.Graph(id={"type": "dynamic-graph", "index": uid}, figure=go.Figure()),
dcc.Store(id={"type": "store", "index": uid}),
dcc.Store(id={"type": "store-columns", "index": uid},data=['y2', 'y1']),
TraceUpdater(id={"type": "dynamic-updater", "index": uid}, gdID=f"{uid}"),
dcc.Interval(
id={"type": "interval", "index": uid}, max_intervals=1, interval=1
),
],
)
gc_children.append(diff_container)
print('store data cb finish')
return gc_children
@app.callback(
ServersideOutput({"type": "store", "index": MATCH}, "data"),
Output({"type": "dynamic-graph", "index": MATCH}, "figure"),
State("store-temp", "data"),
State({"type": "store-columns", "index": MATCH}, "data"),
Trigger({"type": "interval", "index": MATCH}, "n_intervals"),
prevent_initial_call=True,
)
def construct_display_graph(df,columns) -> FigureResampler:
df2=df[columns]
fr = FigureResampler(go.Figure(), verbose=True)
for col in df2.columns:
fr.add_trace(go.Scattergl(name=col, mode='lines'),hf_y=df2[col],hf_x=df2.index)
fr.update_traces(connectgaps=True)
return fr, fr
# @app.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):
# print(fig)
# if fig is not None:
# return fig.construct_update_data(relayoutdata)
# return no_update
@app.callback(
Output({"type": "dynamic-updater", "index": ALL}, "updateData"),
Input({"type": "dynamic-graph", "index": ALL}, "relayoutData"),
State({"type": "dynamic-graph", "index": ALL}, "id"),
State({"type": "store", "index": ALL}, "data"),
prevent_initial_call=True,
memoize=True,
)
def update_fig(relayoutdata: list[dict],ids: list[dict], figs: list[FigureResampler]):
figure_updated = ctx.triggered_id # get the id of the figure that triggered the callback
triggered_index=ids.index(figure_updated) # get the index of the figure in the Input/Output lists of the callback
# print(figure_updated)
# print(relayoutdata)
print(ids)
# print('index : '+ str(triggered_index))
zoomdata=dict(relayoutdata[triggered_index]) # get the relayoutdata of the figure that triggered the callback
new_relayoutdata = []
for i, data in enumerate(relayoutdata): # loop over current relayoutdata
if i == triggered_index:
new_relayoutdata.append(zoomdata) # keep relayoutdata of figure that triggered callback
else:
if 'xaxis.range[0]' in zoomdata:
data = dict(relayoutdata[i])
data['xaxis.range[0]'] = zoomdata ['xaxis.range[0]']
data['xaxis.range[1]'] = zoomdata ['xaxis.range[1]']
data['xaxis.autorange'] = False
new_relayoutdata.append(data)
else:
new_relayoutdata.append(zoomdata)
print(zoomdata)
updatedata = []
print(figs)
for i,fig in enumerate(figs):
if fig is None:
return [no_update, no_update]
else:
updatedata.append(fig.construct_update_data(new_relayoutdata[i]))
return updatedata
if __name__ == '__main__':
app.run_server(debug=True, port=9023)
Like in the 11_sine_generator.py example plots are generated with a unique id. In the update_fig()
callback I want to update all graphs if one of them updates. In the example (commented out in my code) MATCH
is used. To get all FigureResampler object I replaced it with ALL
. However, State({"type": "store", "index": ALL}, "data")
produces a list with hashes(?) like ['b4de3743d91d23d6b85d2bcdd11cb531', '7d9efd7bb10eafd6cfcffe27b9ec566c']
where State({"type": "store-columns", "index": MATCH}, "data")
gives me the single FigureResampler object. With ALL
I would expect a list of FigureResampler objects.
What am I missing here, how can I obtain the FigureResampler objects so I can modify them with construct_update_data()
?
question examples