-
-
Notifications
You must be signed in to change notification settings - Fork 73
/
Copy path11_sine_generator.py
201 lines (180 loc) · 7.52 KB
/
11_sine_generator.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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
"""Dash runtime sine generator app example.
In this example, users can configure parameters of a sine wave and then generate the
sine-wave graph at runtime using the create-new-graph button. There is also an option
to remove the graph.
This app uses server side caching of the FigureResampler object. As it uses the same
concepts of the 03_minimal_cache_dynamic.py example, the runtime graph construction
callback is again split up into two callbacks: (1) the callback used to construct the
necessary components and send them to the front-end and (2) the callback used to
construct the plotly-resampler figure and cache it on the server side.
"""
from uuid import uuid4
import dash_bootstrap_components as dbc
import numpy as np
import plotly.graph_objects as go
from dash import MATCH, Input, Output, State, callback_context, dcc, html, no_update
from dash_extensions.enrich import (
DashProxy,
ServersideOutput,
ServersideOutputTransform,
Trigger,
TriggerTransform,
)
from trace_updater import TraceUpdater
from plotly_resampler import FigureResampler
# --------------------------------------Globals ---------------------------------------
app = DashProxy(
__name__,
suppress_callback_exceptions=True,
external_stylesheets=[dbc.themes.LUX],
transforms=[ServersideOutputTransform(), TriggerTransform()],
)
# -------- Construct the app layout --------
app.layout = html.Div(
[
html.Div(html.H1("Exponential sine generator"), style={"textAlign": "center"}),
html.Hr(),
dbc.Row(
[
dbc.Col(
dbc.Form(
[
dbc.Label("#datapoints:", style={"margin-left": "10px"}),
html.Br(),
dcc.Input(
id="nbr-datapoints",
placeholder="n",
type="number",
style={"margin-left": "10px"},
),
*([html.Br()] * 2),
dbc.Label("exponent:", style={"margin-left": "10px"}),
html.Br(),
dcc.Input(
id="expansion-factor",
placeholder="pow",
type="number",
min=0.95,
max=1.00001,
style={"margin-left": "10px"},
),
*([html.Br()] * 2),
dbc.Button(
"Create new graph",
id="add-graph-btn",
color="primary",
style={
"textalign": "center",
"width": "max-content",
"margin-left": "10px",
},
),
*([html.Br()] * 2),
dbc.Button(
"Remove last graph",
id="remove-graph-btn",
color="danger",
style={
"textalign": "center",
"width": "max-content",
"margin-left": "10px",
},
),
],
),
style={"align": "top"},
md=2,
),
dbc.Col(html.Div(id="graph-container"), md=10),
],
),
]
)
# ------------------------------------ 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("graph-container", "children"),
Input("add-graph-btn", "n_clicks"),
Input("remove-graph-btn", "n_clicks"),
[
State("nbr-datapoints", "value"),
State("expansion-factor", "value"),
State("graph-container", "children"),
],
prevent_initial_call=True,
)
def add_or_remove_graph(add_graph, remove_graph, n, exp, gc_children):
if (add_graph is None or n is None or exp is None) and (remove_graph is None):
return no_update
# Transform the graph data to a figure
gc_children = [] if gc_children is None else gc_children
# Check if we need to remove a graph
clicked_btns = [p["prop_id"] for p in callback_context.triggered]
if any("remove-graph" in btn_name for btn_name in clicked_btns):
if not len(gc_children):
return no_update
return gc_children[:-1]
# No graph needs to be removed -> create a new graph
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
),
],
)
gc_children.append(new_child)
return gc_children
# This method constructs the FigureResampler graph and caches it on the server side
@app.callback(
ServersideOutput({"type": "store", "index": MATCH}, "data"),
Output({"type": "dynamic-graph", "index": MATCH}, "figure"),
State("nbr-datapoints", "value"),
State("expansion-factor", "value"),
State("add-graph-btn", "n_clicks"),
Trigger({"type": "interval", "index": MATCH}, "n_intervals"),
prevent_initial_call=True,
)
def construct_display_graph(n, exp, n_added_graphs) -> FigureResampler:
# Figure construction logic based on state variables
x = np.arange(n)
expansion_scaling = exp**x
y = (
np.sin(x / 200) * expansion_scaling
+ np.random.randn(n) / 10 * expansion_scaling
)
fr = FigureResampler(go.Figure(), verbose=True)
fr.add_trace(go.Scattergl(name="sin"), hf_x=x, hf_y=y)
fr.update_layout(
height=350,
showlegend=True,
legend=dict(orientation="h", y=1.12, xanchor="right", x=1),
template="plotly_white",
title=f"graph {n_added_graphs} - n={n:,} pow={exp}",
title_x=0.5,
)
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):
if fig is not None:
return fig.construct_update_data(relayoutdata)
return no_update
# --------------------------------- Running the app ---------------------------------
if __name__ == "__main__":
app.run_server(debug=True, port=9023)