Skip to content

Commit cc07774

Browse files
jonasvddjvdd
andauthored
Refactor/remove trace updater (#281)
* ✨ removing TraceUpdater from docs * 💨 updating tests * 💪 updating dash apps * 🙏 fix traceupdater bug * 🎄 updating examples * 🙈 formatting * ✨ rerunning basic example * 🙈 make graph div an array * 💪 patch compatible http request parsing * 💨 add no_update edge case to patch method * 🙏 * 🖊️ improving docs * 🕵️ review examples * 🧹 review code * 👀 add type hints * 🙈 fix typo --------- Co-authored-by: Jeroen Van Der Donckt <[email protected]>
1 parent 0a68cb7 commit cc07774

23 files changed

+490
-1006
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ venv*
33
*.ruff*
44
*.DS_Store
55
file_system_store/
6+
file_system_backend/
67

78
# Sphinx documentation
89
*_build/

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ lint:
1414
test:
1515
poetry run pytest --cov-report term-missing --cov=plotly_resampler tests
1616

17+
.PHONY: docs
18+
docs:
19+
poetry run mkdocs build -c -f mkdocs.yml
20+
1721
.PHONY: clean
1822
clean:
1923
rm -rf `find . -name __pycache__`

examples/basic_example.ipynb

Lines changed: 247 additions & 576 deletions
Large diffs are not rendered by default.

examples/dash_apps/01_minimal_global.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
No dynamic graph construction / pattern matching callbacks are needed.
55
66
This example uses a global FigureResampler object, which is considered a bad practice.
7-
source: https://dash.plotly.com/sharing-data-between-callbacks:
7+
source: https://dash.plotly.com/sharing-data-between-callbacks:
88
99
Dash is designed to work in multi-user environments where multiple people view the
1010
application at the same time and have independent sessions.
@@ -16,7 +16,6 @@
1616
import numpy as np
1717
import plotly.graph_objects as go
1818
from dash import Dash, Input, Output, callback_context, dcc, html, no_update
19-
from trace_updater import TraceUpdater
2019

2120
from plotly_resampler import FigureResampler
2221

@@ -38,9 +37,8 @@
3837
html.H1("plotly-resampler global variable", style={"textAlign": "center"}),
3938
html.Button("plot chart", id="plot-button", n_clicks=0),
4039
html.Hr(),
41-
# The graph and it's needed components to update efficiently
40+
# The graph object - which we will empower with plotly-resampler
4241
dcc.Graph(id="graph-id"),
43-
TraceUpdater(id="trace-updater", gdID="graph-id"),
4442
]
4543
)
4644

@@ -67,10 +65,8 @@ def plot_graph(n_clicks):
6765
return no_update
6866

6967

70-
# Register the graph update callbacks to the layout
71-
fig.register_update_graph_callback(
72-
app=app, graph_id="graph-id", trace_updater_id="trace-updater"
73-
)
68+
# The plotly-resampler callback to update the graph after a relayout event (= zoom/pan)
69+
fig.register_update_graph_callback(app=app, graph_id="graph-id")
7470

7571
# --------------------------------- Running the app ---------------------------------
7672
if __name__ == "__main__":

examples/dash_apps/02_minimal_cache.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import plotly.graph_objects as go
1414
from dash import Input, Output, State, callback_context, dcc, html, no_update
1515
from dash_extensions.enrich import DashProxy, Serverside, ServersideOutputTransform
16-
from trace_updater import TraceUpdater
1716

1817
from plotly_resampler import FigureResampler
1918
from plotly_resampler.aggregation import MinMaxLTTB
@@ -30,12 +29,11 @@
3029
html.H1("plotly-resampler + dash-extensions", style={"textAlign": "center"}),
3130
html.Button("plot chart", id="plot-button", n_clicks=0),
3231
html.Hr(),
33-
# The graph and its needed components to serialize and update efficiently
32+
# The graph object - which we will empower with plotly-resampler
33+
dcc.Graph(id="graph-id"),
3434
# Note: we also add a dcc.Store component, which will be used to link the
3535
# server side cached FigureResampler object
36-
dcc.Graph(id="graph-id"),
3736
dcc.Loading(dcc.Store(id="store")),
38-
TraceUpdater(id="trace-updater", gdID="graph-id"),
3937
]
4038
)
4139

@@ -63,17 +61,19 @@ def plot_graph(n_clicks):
6361
return no_update
6462

6563

64+
# The plotly-resampler callback to update the graph after a relayout event (= zoom/pan)
65+
# As we use the figure again as output, we need to set: allow_duplicate=True
6666
@app.callback(
67-
Output("trace-updater", "updateData"),
67+
Output("graph-id", "figure", allow_duplicate=True),
6868
Input("graph-id", "relayoutData"),
6969
State("store", "data"), # The server side cached FigureResampler per session
7070
prevent_initial_call=True,
7171
memoize=True,
7272
)
73-
def update_fig(relayoutdata, fig):
73+
def update_fig(relayoutdata: dict, fig: FigureResampler):
7474
if fig is None:
7575
return no_update
76-
return fig.construct_update_data(relayoutdata)
76+
return fig.construct_update_data_patch(relayoutdata)
7777

7878

7979
# --------------------------------- Running the app ---------------------------------

examples/dash_apps/03_minimal_cache_dynamic.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
Trigger,
2727
TriggerTransform,
2828
)
29-
from trace_updater import TraceUpdater
3029

3130
from plotly_resampler import FigureResampler
3231
from plotly_resampler.aggregation import MinMaxLTTB
@@ -59,12 +58,10 @@ def add_graph_div(n_clicks: int, div_children: List[html.Div]):
5958
uid = str(uuid4())
6059
new_child = html.Div(
6160
children=[
62-
# The graph and its needed components to serialize and update efficiently
61+
dcc.Graph(id={"type": "dynamic-graph", "index": uid}, figure=go.Figure()),
6362
# Note: we also add a dcc.Store component, which will be used to link the
6463
# server side cached FigureResampler object
65-
dcc.Graph(id={"type": "dynamic-graph", "index": uid}, figure=go.Figure()),
6664
dcc.Loading(dcc.Store(id={"type": "store", "index": uid})),
67-
TraceUpdater(id={"type": "dynamic-updater", "index": uid}, gdID=f"{uid}"),
6865
# This dcc.Interval components makes sure that the `construct_display_graph`
6966
# callback is fired once after these components are added to the session
7067
# its front-end
@@ -101,16 +98,18 @@ def construct_display_graph(n_clicks) -> FigureResampler:
10198
return fig, Serverside(fig)
10299

103100

101+
# The plotly-resampler callback to update the graph after a relayout event (= zoom/pan)
102+
# As we use the figure again as output, we need to set: allow_duplicate=True
104103
@app.callback(
105-
Output({"type": "dynamic-updater", "index": MATCH}, "updateData"),
104+
Output({"type": "dynamic-graph", "index": MATCH}, "figure", allow_duplicate=True),
106105
Input({"type": "dynamic-graph", "index": MATCH}, "relayoutData"),
107106
State({"type": "store", "index": MATCH}, "data"),
108107
prevent_initial_call=True,
109108
memoize=True,
110109
)
111110
def update_fig(relayoutdata: dict, fig: FigureResampler):
112111
if fig is not None:
113-
return fig.construct_update_data(relayoutdata)
112+
return fig.construct_update_data_patch(relayoutdata)
114113
return no_update
115114

116115

examples/dash_apps/04_minimal_cache_overview.py

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,15 @@
1111
1212
"""
1313

14+
import dash
1415
import numpy as np
1516
import plotly.graph_objects as go
16-
import dash
1717
from dash import Input, Output, State, callback_context, dcc, html, no_update
1818
from dash_extensions.enrich import DashProxy, Serverside, ServersideOutputTransform
19-
from trace_updater import TraceUpdater
2019

2120
# The overview figure requires clientside callbacks, whose JavaScript code is located
2221
# in the assets folder. We need to tell dash where to find this folder.
23-
from plotly_resampler import FigureResampler, ASSETS_FOLDER
22+
from plotly_resampler import ASSETS_FOLDER, FigureResampler
2423

2524
# -------------------------------- Data and constants ---------------------------------
2625
# Data that will be used for the plotly-resampler figures
@@ -31,27 +30,27 @@
3130
GRAPH_ID = "graph-id"
3231
OVERVIEW_GRAPH_ID = "overview-graph"
3332
STORE_ID = "store"
34-
TRACEUPDATER_ID = "traceupdater"
3533

3634

3735
# --------------------------------------Globals ---------------------------------------
38-
# Remark how the assests folder is passed to the Dash(proxy) application
36+
# NOTE: Remark how the assests folder is passed to the Dash(proxy) application and how
37+
# the lodash script is included as an external script.
3938
app = DashProxy(
40-
__name__, transforms=[ServersideOutputTransform()], assets_folder=ASSETS_FOLDER
39+
__name__,
40+
transforms=[ServersideOutputTransform()],
41+
assets_folder=ASSETS_FOLDER,
42+
external_scripts=["https://cdn.jsdelivr.net/npm/lodash/lodash.min.js"],
4143
)
4244

4345
app.layout = html.Div(
4446
[
4547
html.H1("plotly-resampler + dash-extensions", style={"textAlign": "center"}),
4648
html.Button("plot chart", id="plot-button", n_clicks=0),
4749
html.Hr(),
48-
# The graph and its needed components to serialize and update efficiently
49-
# Note: we also add a dcc.Store component, which will be used to link the
50-
# server side cached FigureResampler object
50+
# The graph, overview graph, and servside store for the FigureResampler graph
5151
dcc.Graph(id=GRAPH_ID),
5252
dcc.Graph(id=OVERVIEW_GRAPH_ID),
5353
dcc.Loading(dcc.Store(id=STORE_ID)),
54-
TraceUpdater(id=TRACEUPDATER_ID, gdID=GRAPH_ID),
5554
]
5655
)
5756

@@ -104,19 +103,22 @@ def plot_graph(_):
104103
)
105104

106105

107-
# --- FigureResampler update logic ---
106+
# --- FigureResampler update callback ---
107+
108+
# The plotly-resampler callback to update the graph after a relayout event (= zoom/pan)
109+
# As we use the figure again as output, we need to set: allow_duplicate=True
108110
@app.callback(
109-
Output(TRACEUPDATER_ID, "updateData"),
111+
Output(GRAPH_ID, "figure", allow_duplicate=True),
110112
Input(GRAPH_ID, "relayoutData"),
111113
State(STORE_ID, "data"), # The server side cached FigureResampler per session
112114
prevent_initial_call=True,
113115
)
114-
def update_fig(relayoutdata, fig):
116+
def update_fig(relayoutdata: dict, fig: FigureResampler):
115117
if fig is None:
116118
return no_update
117-
return fig.construct_update_data(relayoutdata)
119+
return fig.construct_update_data_patch(relayoutdata)
118120

119121

120122
# --------------------------------- Running the app ---------------------------------
121123
if __name__ == "__main__":
122-
app.run_server(debug=False, port=9023, use_reloader=False)
124+
app.run_server(debug=True, port=9023, use_reloader=False)

examples/dash_apps/05_cache_overview_subplots.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
from dash import Input, Output, State, callback_context, dcc, html, no_update
2020
from dash_extensions.enrich import DashProxy, Serverside, ServersideOutputTransform
2121
from plotly.subplots import make_subplots
22-
from trace_updater import TraceUpdater
2322

2423
# The overview figure requires clientside callbacks, whose JavaScript code is located
2524
# in the assets folder. We need to tell dash where to find this folder.
@@ -35,27 +34,27 @@
3534
GRAPH_ID = "graph-id"
3635
OVERVIEW_GRAPH_ID = "overview-graph"
3736
STORE_ID = "store"
38-
TRACEUPDATER_ID = "traceupdater"
3937

4038

4139
# --------------------------------------Globals ---------------------------------------
42-
# Remark how the assests folder is passed to the Dash(proxy) application
40+
# NOTE: Remark how the assests folder is passed to the Dash(proxy) application and how
41+
# the lodash script is included as an external script.
4342
app = DashProxy(
44-
__name__, transforms=[ServersideOutputTransform()], assets_folder=ASSETS_FOLDER
43+
__name__,
44+
transforms=[ServersideOutputTransform()],
45+
assets_folder=ASSETS_FOLDER,
46+
external_scripts=["https://cdn.jsdelivr.net/npm/lodash/lodash.min.js"],
4547
)
4648

4749
app.layout = html.Div(
4850
[
4951
html.H1("plotly-resampler + dash-extensions", style={"textAlign": "center"}),
5052
html.Button("plot chart", id="plot-button", n_clicks=0),
5153
html.Hr(),
52-
# The graph and its needed components to serialize and update efficiently
53-
# Note: we also add a dcc.Store component, which will be used to link the
54-
# server side cached FigureResampler object
54+
# The graph, overview graph, and servside store for the FigureResampler graph
5555
dcc.Graph(id=GRAPH_ID),
5656
dcc.Graph(id=OVERVIEW_GRAPH_ID),
5757
dcc.Loading(dcc.Store(id=STORE_ID)),
58-
TraceUpdater(id=TRACEUPDATER_ID, gdID=GRAPH_ID),
5958
]
6059
)
6160

@@ -139,17 +138,20 @@ def plot_graph(_):
139138
)
140139

141140

142-
# --- FigureResampler update logic ---
141+
# --- FigureResampler update callback ---
142+
143+
# The plotly-resampler callback to update the graph after a relayout event (= zoom/pan)
144+
# As we use the figure again as output, we need to set: allow_duplicate=True
143145
@app.callback(
144-
Output(TRACEUPDATER_ID, "updateData"),
146+
Output(GRAPH_ID, "figure", allow_duplicate=True),
145147
Input(GRAPH_ID, "relayoutData"),
146148
State(STORE_ID, "data"), # The server side cached FigureResampler per session
147149
prevent_initial_call=True,
148150
)
149-
def update_fig(relayoutdata, fig):
151+
def update_fig(relayoutdata, fig: FigureResampler):
150152
if fig is None:
151153
return no_update
152-
return fig.construct_update_data(relayoutdata)
154+
return fig.construct_update_data_patch(relayoutdata)
153155

154156

155157
# --------------------------------- Running the app ---------------------------------

examples/dash_apps/11_sine_generator.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
Trigger,
2626
TriggerTransform,
2727
)
28-
from trace_updater import TraceUpdater
2928

3029
from plotly_resampler import FigureResampler
3130

@@ -132,12 +131,10 @@ def add_or_remove_graph(add_graph, remove_graph, n, exp, gc_children):
132131
uid = str(uuid4())
133132
new_child = html.Div(
134133
children=[
135-
# The graph and its needed components to serialize and update efficiently
136134
# Note: we also add a dcc.Store component, which will be used to link the
137135
# server side cached FigureResampler object
138136
dcc.Graph(id={"type": "dynamic-graph", "index": uid}, figure=go.Figure()),
139137
dcc.Loading(dcc.Store(id={"type": "store", "index": uid})),
140-
TraceUpdater(id={"type": "dynamic-updater", "index": uid}, gdID=f"{uid}"),
141138
# This dcc.Interval components makes sure that the `construct_display_graph`
142139
# callback is fired once after these components are added to the session
143140
# its front-end
@@ -183,19 +180,23 @@ def construct_display_graph(n, exp, n_added_graphs) -> FigureResampler:
183180
return fr, Serverside(fr)
184181

185182

183+
# --- FigureResampler update callback ---
184+
185+
# The plotly-resampler callback to update the graph after a relayout event (= zoom/pan)
186+
# As we use the figure again as output, we need to set: allow_duplicate=True
186187
@app.callback(
187-
Output({"type": "dynamic-updater", "index": MATCH}, "updateData"),
188+
Output({"type": "dynamic-graph", "index": MATCH}, "figure", allow_duplicate=True),
188189
Input({"type": "dynamic-graph", "index": MATCH}, "relayoutData"),
189190
State({"type": "store", "index": MATCH}, "data"),
190191
prevent_initial_call=True,
191192
memoize=True,
192193
)
193194
def update_fig(relayoutdata: dict, fig: FigureResampler):
194195
if fig is not None:
195-
return fig.construct_update_data(relayoutdata)
196+
return fig.construct_update_data_patch(relayoutdata)
196197
return no_update
197198

198199

199200
# --------------------------------- Running the app ---------------------------------
200201
if __name__ == "__main__":
201-
app.run_server(debug=True, port=9023)
202+
app.run_server(debug=True, port=9023, use_reloader=False)

0 commit comments

Comments
 (0)