-
-
Notifications
You must be signed in to change notification settings - Fork 73
/
Copy path03_minimal_cache_dynamic.py
118 lines (99 loc) · 4.49 KB
/
03_minimal_cache_dynamic.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
"""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
from dash import MATCH, Input, Output, State, dcc, html, no_update
from dash_extensions.enrich import (
DashProxy,
Serverside,
ServersideOutputTransform,
Trigger,
TriggerTransform,
)
from plotly_resampler import FigureResampler
from plotly_resampler.aggregation import MinMaxLTTB
# 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
# --------------------------------------Globals ---------------------------------------
app = DashProxy(__name__, transforms=[ServersideOutputTransform(), TriggerTransform()])
app.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.
@app.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=[
dcc.Graph(id={"type": "dynamic-graph", "index": uid}, figure=go.Figure()),
# Note: we also add a dcc.Store component, which will be used to link the
# server side cached FigureResampler object
dcc.Loading(dcc.Store(id={"type": "store", "index": 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
@app.callback(
Output({"type": "dynamic-graph", "index": MATCH}, "figure"),
Output({"type": "store", "index": MATCH}, "data"),
State("add-chart", "n_clicks"),
Trigger({"type": "interval", "index": MATCH}, "n_intervals"),
prevent_initial_call=True,
)
def construct_display_graph(n_clicks) -> FigureResampler:
fig = FigureResampler(
go.Figure(),
default_n_shown_samples=2_000,
default_downsampler=MinMaxLTTB(parallel=True),
)
# 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, Serverside(fig)
# The plotly-resampler callback to update the graph after a relayout event (= zoom/pan)
# As we use the figure again as output, we need to set: allow_duplicate=True
@app.callback(
Output({"type": "dynamic-graph", "index": MATCH}, "figure", allow_duplicate=True),
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_patch(relayoutdata)
return no_update
# --------------------------------- Running the app ---------------------------------
if __name__ == "__main__":
app.run_server(debug=True, port=9023)