Skip to content

Commit 7612315

Browse files
jonasvddjvdd
andauthored
Bug/rangeselector (#287)
* ✨ fix for #275 * 🧹 review code * ✨ new example * 🔍 reviewing examples * 🖍️ docs-fix for #275 * 🔍 review --------- Co-authored-by: Jeroen Van Der Donckt <[email protected]>
1 parent 46e629f commit 7612315

File tree

5 files changed

+204
-8
lines changed

5 files changed

+204
-8
lines changed

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ The [dash_apps](dash_apps/) folder contains example dash apps in which `plotly-r
4646
| [runtime graph construction](dash_apps/03_minimal_cache_dynamic.py) | minimal example where graphs are constructed based on user interactions at runtime. [Pattern matching callbacks](https://dash.plotly.com/pattern-matching-callbacks) are used construct these plotly-resampler graphs dynamically. Again, server side caching is performed. |
4747
| [xaxis overview (rangeslider)](dash_apps/04_minimal_cache_overview.py) | minimal example where a linked xaxis overview is shown below the `FigureResampler` figure. This xaxis rangeslider utilizes [clientside callbacks](https://dash.plotly.com/clientside-callbacks) to realize this behavior. |
4848
| [xaxis overview (subplots)](dash_apps/05_cache_overview_subplots.py) | example where a linked xaxis overview is shown below the `FigureResampler` figure (with subplots). |
49+
| [overview range selector button](dash_apps/06_cache_overview_range_buttons.py) | example where (i) a linked xaxis overview is shown below the `FigureResampler` figure, and (ii) a rangeselector along with a reset axis button is utilized to zoom in on specific window sizes. |
4950
| **advanced apps** | |
5051
| [dynamic sine generator](dash_apps/11_sine_generator.py) | exponential sine generator which uses [pattern matching callbacks](https://dash.plotly.com/pattern-matching-callbacks) to remove and construct plotly-resampler graphs dynamically |
5152
| [file visualization](dash_apps/12_file_selector.py) | load and visualize multiple `.parquet` files with plotly-resampler |

examples/dash_apps/04_minimal_cache_overview.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434

3535
# --------------------------------------Globals ---------------------------------------
36-
# NOTE: Remark how the assests folder is passed to the Dash(proxy) application and how
36+
# NOTE: Remark how the assets folder is passed to the Dash(proxy) application and how
3737
# the lodash script is included as an external script.
3838
app = DashProxy(
3939
__name__,
@@ -47,7 +47,7 @@
4747
html.H1("plotly-resampler + dash-extensions", style={"textAlign": "center"}),
4848
html.Button("plot chart", id="plot-button", n_clicks=0),
4949
html.Hr(),
50-
# The graph, overview graph, and servside store for the FigureResampler graph
50+
# The graph, overview graph, and serverside 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)),

examples/dash_apps/05_cache_overview_subplots.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737

3838

3939
# --------------------------------------Globals ---------------------------------------
40-
# NOTE: Remark how the assests folder is passed to the Dash(proxy) application and how
40+
# NOTE: Remark how the assets folder is passed to the Dash(proxy) application and how
4141
# the lodash script is included as an external script.
4242
app = DashProxy(
4343
__name__,
@@ -51,7 +51,7 @@
5151
html.H1("plotly-resampler + dash-extensions", style={"textAlign": "center"}),
5252
html.Button("plot chart", id="plot-button", n_clicks=0),
5353
html.Hr(),
54-
# The graph, overview graph, and servside store for the FigureResampler graph
54+
# The graph, overview graph, and serverside 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)),
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
"""Minimal dash app example.
2+
3+
Click on a button, and see a plotly-resampler graph of an exponential and log curve is
4+
shown. In addition, another graph is shown below, which is an overview of the main
5+
graph. This other graph is bidirectionally linked to the main graph; when you
6+
select a region in the overview graph, the main graph will zoom in on that region and
7+
vice versa.
8+
9+
On the left top of the main graph, you can see a range selector. This range selector
10+
allows to zoom in with a fixed time range.
11+
12+
Lastly, there is a button present to reset the axes of the main graph. This button
13+
replaces the default reset axis button as the default button removes the spikes.
14+
(specifically, the `xaxis.showspikes` and `yaxis.showspikes` are set to False; This is
15+
most likely a bug in plotly-resampler, but I have not yet found out why).
16+
17+
This example uses the dash-extensions its ServersideOutput functionality to cache
18+
the FigureResampler per user/session on the server side. This way, no global figure
19+
variable is used and shows the best practice of using plotly-resampler within dash-apps.
20+
21+
"""
22+
23+
import dash
24+
import numpy as np
25+
import pandas as pd
26+
import plotly.graph_objects as go
27+
from dash import Input, Output, State, callback_context, dcc, html, no_update
28+
from dash_extensions.enrich import DashProxy, Serverside, ServersideOutputTransform
29+
30+
# The overview figure requires clientside callbacks, whose JavaScript code is located
31+
# in the assets folder. We need to tell dash where to find this folder.
32+
from plotly_resampler import ASSETS_FOLDER, FigureResampler
33+
from plotly_resampler.aggregation import MinMaxLTTB
34+
35+
# -------------------------------- Data and constants ---------------------------------
36+
# Data that will be used for the plotly-resampler figures
37+
x = np.arange(2_000_000)
38+
x_time = pd.date_range("2020-01-01", periods=len(x), freq="1min")
39+
noisy_sin = (3 + np.sin(x / 200) + np.random.randn(len(x)) / 10) * x / 1_000
40+
41+
# The ids of the components used in the app (we put them here to avoid typos later on)
42+
GRAPH_ID = "graph-id"
43+
OVERVIEW_GRAPH_ID = "overview-graph"
44+
STORE_ID = "store"
45+
PLOT_BTN_ID = "plot-button"
46+
47+
# --------------------------------------Globals ---------------------------------------
48+
# NOTE: Remark how
49+
# (1) the assets folder is passed to the Dash(proxy) application
50+
# (2) the lodash script is included as an external script.
51+
app = DashProxy(
52+
__name__,
53+
transforms=[ServersideOutputTransform()],
54+
assets_folder=ASSETS_FOLDER,
55+
external_scripts=["https://cdn.jsdelivr.net/npm/lodash/lodash.min.js"],
56+
)
57+
58+
# Construct the app layout
59+
app.layout = html.Div(
60+
[
61+
html.H1("plotly-resampler + dash-extensions", style={"textAlign": "center"}),
62+
html.Button("plot chart", id=PLOT_BTN_ID, n_clicks=0),
63+
html.Hr(),
64+
# The graph, overview graph, and serverside store for the FigureResampler graph
65+
dcc.Graph(
66+
id=GRAPH_ID,
67+
# NOTE: we remove the reset scale button as it removes the spikes and
68+
# we provide our own reset-axis button upon graph construction
69+
config={"modeBarButtonsToRemove": ["resetscale"]},
70+
),
71+
dcc.Graph(id=OVERVIEW_GRAPH_ID, config={"displayModeBar": False}),
72+
dcc.Loading(dcc.Store(id=STORE_ID)),
73+
]
74+
)
75+
76+
77+
# ------------------------------------ DASH logic -------------------------------------
78+
# --- construct and store the FigureResampler on the serverside ---
79+
@app.callback(
80+
[
81+
Output(GRAPH_ID, "figure"),
82+
Output(OVERVIEW_GRAPH_ID, "figure"),
83+
Output(STORE_ID, "data"),
84+
],
85+
Input(PLOT_BTN_ID, "n_clicks"),
86+
prevent_initial_call=True,
87+
)
88+
def plot_graph(_):
89+
ctx = callback_context
90+
if not len(ctx.triggered) or PLOT_BTN_ID not in ctx.triggered[0]["prop_id"]:
91+
return no_update
92+
93+
# 1. Create the figure and add data
94+
fig = FigureResampler(
95+
# fmt: off
96+
go.Figure(layout=dict(
97+
# dragmode="pan",
98+
hovermode="x unified",
99+
xaxis=dict(rangeselector=dict(buttons=list([
100+
dict(count=7, label="1 week", step="day", stepmode="backward"),
101+
dict(count=1, label="1 month", step="month", stepmode="backward"),
102+
dict(count=2, label="2 months", step="month", stepmode="backward"),
103+
dict(count=1, label="1 year", step="year", stepmode="backward"),
104+
]))),
105+
)),
106+
# fmt: on
107+
default_downsampler=MinMaxLTTB(parallel=True),
108+
create_overview=True,
109+
)
110+
111+
# Figure construction logic
112+
log = noisy_sin * 0.9999995**x
113+
exp = noisy_sin * 1.000002**x
114+
fig.add_trace(go.Scattergl(name="log"), hf_x=x_time, hf_y=log)
115+
fig.add_trace(go.Scattergl(name="exp"), hf_x=x_time, hf_y=exp)
116+
117+
fig.update_layout(
118+
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
119+
)
120+
fig.update_layout(
121+
margin=dict(b=10),
122+
template="plotly_white",
123+
height=650, # , hovermode="x unified",
124+
# https://plotly.com/python/custom-buttons/
125+
updatemenus=[
126+
dict(
127+
type="buttons",
128+
x=0.45,
129+
xanchor="left",
130+
y=1.09,
131+
yanchor="top",
132+
buttons=[
133+
dict(
134+
label="reset axes",
135+
method="relayout",
136+
args=[
137+
{
138+
"xaxis.autorange": True,
139+
"yaxis.autorange": True,
140+
"xaxis.showspikes": True,
141+
"yaxis.showspikes": False,
142+
}
143+
],
144+
),
145+
],
146+
)
147+
],
148+
)
149+
# fig.update_traces(xaxis="x")
150+
# fig.update_xaxes(showspikes=True, spikemode="across", spikesnap="cursor")
151+
152+
coarse_fig = fig._create_overview_figure()
153+
return fig, coarse_fig, Serverside(fig)
154+
155+
156+
# --- Clientside callbacks used to bidirectionally link the overview and main graph ---
157+
app.clientside_callback(
158+
dash.ClientsideFunction(namespace="clientside", function_name="main_to_coarse"),
159+
dash.Output(
160+
OVERVIEW_GRAPH_ID, "id", allow_duplicate=True
161+
), # TODO -> look for clean output
162+
dash.Input(GRAPH_ID, "relayoutData"),
163+
[dash.State(OVERVIEW_GRAPH_ID, "id"), dash.State(GRAPH_ID, "id")],
164+
prevent_initial_call=True,
165+
)
166+
167+
app.clientside_callback(
168+
dash.ClientsideFunction(namespace="clientside", function_name="coarse_to_main"),
169+
dash.Output(GRAPH_ID, "id", allow_duplicate=True),
170+
dash.Input(OVERVIEW_GRAPH_ID, "selectedData"),
171+
[dash.State(GRAPH_ID, "id"), dash.State(OVERVIEW_GRAPH_ID, "id")],
172+
prevent_initial_call=True,
173+
)
174+
175+
176+
# --- FigureResampler update callback ---
177+
# The plotly-resampler callback to update the graph after a relayout event (= zoom/pan)
178+
# As we use the figure again as output, we need to set: allow_duplicate=True
179+
@app.callback(
180+
Output(GRAPH_ID, "figure", allow_duplicate=True),
181+
Input(GRAPH_ID, "relayoutData"),
182+
State(STORE_ID, "data"), # The server side cached FigureResampler per session
183+
prevent_initial_call=True,
184+
)
185+
def update_fig(relayoutdata, fig: FigureResampler):
186+
if fig is None:
187+
return no_update
188+
return fig.construct_update_data_patch(relayoutdata)
189+
190+
191+
if __name__ == "__main__":
192+
# Start the app
193+
app.run(debug=True, host="localhost", port=8055, use_reloader=False)

plotly_resampler/registering.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,12 @@ def register_plotly_resampler(mode="auto", **aggregator_kwargs):
8888
We advise to use mode= ``widget`` when working in an IPython based environment
8989
as this will just behave as a ``go.FigureWidget``, but with dynamic aggregation.
9090
When using mode= ``auto`` or ``figure``; most figures will be wrapped as
91-
[`FigureResampler`][figure_resampler.FigureResampler],
92-
on which
93-
[`show_dash`][figure_resampler.FigureResampler.show_dash]
94-
needs to be called.
91+
[`FigureResampler`][figure_resampler.FigureResampler], on which
92+
[`show_dash`][figure_resampler.FigureResampler.show_dash] needs to be called.
93+
94+
!!! note
95+
This function is mostly useful for notebooks. For dash-apps, we advise to look
96+
at the dash app examples on [GitHub](https://github.com/predict-idlab/plotly-resampler/tree/main/examples#2-dash-apps)
9597
9698
Parameters
9799
----------

0 commit comments

Comments
 (0)