Skip to content

Commit e8faf50

Browse files
authored
🔥 multiple y-axes support (#244)
* 🔥 multiple y-axes support * 💨 add example * 💪 update changelog
1 parent cd735c3 commit e8faf50

File tree

5 files changed

+688
-0
lines changed

5 files changed

+688
-0
lines changed

CHANGELOG.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,54 @@
11

2+
# v0.9.0
3+
## Major changes:
4+
### Even faster aggregation 🐎
5+
We switched our aggregation backend to [tsdownsample](https://github.com/predict-idlab/tsdownsample), which alleviates the need to compile our C code on non-supported devices, and has parallelization capabilities.
6+
`tsdownsample` leverages the [argminmax](https://github.com/jvdd/argminmax) crate, which has SIMD-optimized instruction to find vertical extrema really fast!
7+
8+
With parallelization enabled, you should clearly see a bump in perfomance when visualizing (multiple) large traces! 🐎
9+
10+
### Versioned docs! :party:
11+
We restyled our [documentation](https://predict-idlab.github.io/plotly-resampler/latest) and added versioning! 🎉
12+
13+
https://predict-idlab.github.io/plotly-resampler/latest/
14+
15+
Go check it out! :point_up:
16+
17+
### Other Features
18+
- Support for **log-scale** axes (and thus log-bin-based aggregators) - check [this pull-request](https://github.com/predict-idlab/plotly-resampler/pull/207)
19+
![](https://cdn.discordapp.com/attachments/372491075153166338/1129004610472906782/image.png)
20+
21+
> The above image shows how the `log` aggregator (row2) will use log-scale bins. This can be seen in the 1-1000 range when comparing both subplots. <br>*Note: the shown data has a fixed delta-x of 1. Hence, here are no exact equally spaced bins for the left part of the LogLTTB.*
22+
23+
- Add a fill-value option to gap handlers
24+
![](https://cdn.discordapp.com/attachments/372491075153166338/1129004638016897045/image.png)
25+
26+
> The above image shows how the `fill_value` option can be used to fill gaps with a specific value.<br> This can be of greate use, when you use the `fill='tozeroy'` option in plotly **and gaps occur in your data**, as this will, combined with `line_shape='vh'`, fill the area between the trace and the x-axis and gaps will be a flat zero-line.
27+
### Bugfixes
28+
- support for pandas2.0 intricacies
29+
30+
## What's Changed (generated)
31+
* fix: handle bool dtype for x in LTTB_core_py by @jvdd in https://github.com/predict-idlab/plotly-resampler/pull/183
32+
* fix: add colors to streamlit example :art: by @jvdd in https://github.com/predict-idlab/plotly-resampler/pull/187
33+
* docs: describe solution in FAQ for slow datetime arrays by @jvdd in https://github.com/predict-idlab/plotly-resampler/pull/184
34+
* Rework aggregator interface by @jvdd in https://github.com/predict-idlab/plotly-resampler/pull/186
35+
* :rocket: integrate with tsdownsample by @jvdd in https://github.com/predict-idlab/plotly-resampler/pull/191
36+
* refactor: use composition for gap handling by @jvdd in https://github.com/predict-idlab/plotly-resampler/pull/199
37+
* ✨ np.array interface implementation by @jonasvdd in https://github.com/predict-idlab/plotly-resampler/pull/154
38+
* 🧹 fix typo in docstring + remove LTTB from MinMaxLTTB + remove interleave_gaps by @jonasvdd in https://github.com/predict-idlab/plotly-resampler/pull/201
39+
* chore: use ruff instead of isort by @jvdd in https://github.com/predict-idlab/plotly-resampler/pull/200
40+
* 🌈 adding marker props by @jonasvdd in https://github.com/predict-idlab/plotly-resampler/pull/148
41+
* Datetime bugfix by @jonasvdd in https://github.com/predict-idlab/plotly-resampler/pull/209
42+
* Fixes #210 by @jonasvdd in https://github.com/predict-idlab/plotly-resampler/pull/211
43+
* Log support by @jonasvdd in https://github.com/predict-idlab/plotly-resampler/pull/207
44+
* Datetime range by @jonasvdd in https://github.com/predict-idlab/plotly-resampler/pull/213
45+
* :sparkles: add fill_value option to gap handlers by @jonasvdd in https://github.com/predict-idlab/plotly-resampler/pull/218
46+
* :sparkles: fix `limit_to_view=True` but no gaps inserted bug by @jonasvdd in https://github.com/predict-idlab/plotly-resampler/pull/220
47+
* :bug: convert trace props to array + check for nan removal by @jvdd in https://github.com/predict-idlab/plotly-resampler/pull/225
48+
* Figurewidget datetime bug by @jonasvdd in https://github.com/predict-idlab/plotly-resampler/pull/232
49+
* ♻️ deprecate JupyterDash in favor for updated Dash version by @NielsPraet in https://github.com/predict-idlab/plotly-resampler/pull/233
50+
* :eyes: comment out reset layout by @jvdd in https://github.com/predict-idlab/plotly-resampler/pull/228
51+
* Docs/versioned docs (#236) by @jonasvdd in https://github.com/predict-idlab/plotly-resampler/pull/237
252

353
# v 0.8.0
454

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Additionally, this notebook also shows some more advanced functionalities, such
2323
* The flexibility of configuring different aggregation-algorithms and number of shown samples per trace
2424
* How plotly-resampler can be used for logarithmic x-axes and an implementation of a logarithmic aggregation algorithm, i.e., [LogLTTB](example_utils/loglttb.py)
2525
* Using different `fill_value` for gap handling of filled area plots.
26+
* Using multiple y-axes in a single subplot (see the [other_examples](other_examples.ipynb))
2627

2728
**Note**: the basic example notebook requires `plotly-resampler>=0.9.0rc3`.
2829

examples/other_examples.ipynb

Lines changed: 394 additions & 0 deletions
Large diffs are not rendered by default.

plotly_resampler/figure_resampler/figure_resampler_interface.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,11 @@ def _check_update_figure_dict(
454454
else:
455455
y_axis = "yaxis" + trace.get("yaxis")[1:]
456456

457+
# Also check for overlaying traces - fixes #242
458+
overlaying = figure["layout"].get(y_axis, {}).get("overlaying")
459+
if overlaying:
460+
y_axis = "yaxis" + overlaying[1:]
461+
457462
# Next to the x-anchor, we also fetch the xaxis which matches the
458463
# current trace (i.e. if this value is not None, the axis shares the
459464
# x-axis with one or more traces).

tests/test_multiple_axes.py

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import numpy as np
2+
import plotly.graph_objects as go
3+
import pytest
4+
from plotly.subplots import make_subplots
5+
6+
from plotly_resampler import FigureResampler, FigureWidgetResampler
7+
from plotly_resampler.aggregation import MinMaxLTTB
8+
9+
10+
@pytest.mark.parametrize("fig_type", [FigureResampler, FigureWidgetResampler])
11+
def test_multiple_axes_figure(fig_type):
12+
# Generate some data
13+
x = np.arange(200_000)
14+
sin = 3 + np.sin(x / 200) + np.random.randn(len(x)) / 30
15+
16+
fig = fig_type(
17+
default_n_shown_samples=2000, default_downsampler=MinMaxLTTB(parallel=True)
18+
)
19+
20+
# all traces will be plotted against the same x-axis
21+
# note: the first added trace its yaxis will be used as reference
22+
fig.add_trace(go.Scatter(name="orig", yaxis="y1", line_width=1), hf_x=x, hf_y=sin)
23+
fig.add_trace(
24+
go.Scatter(name="negative", yaxis="y2", line_width=1), hf_x=x, hf_y=-sin
25+
)
26+
fig.add_trace(
27+
go.Scatter(name="sqrt(orig)", yaxis="y3", line_width=1),
28+
hf_x=x,
29+
hf_y=np.sqrt(sin * 10),
30+
)
31+
fig.add_trace(
32+
go.Scatter(name="orig**2", yaxis="y4", line_width=1),
33+
hf_x=x,
34+
hf_y=(sin - 3) ** 2,
35+
)
36+
37+
# in order for autoshift to work, you need to set x-anchor to free
38+
fig.update_layout(
39+
# NOTE: you can use the domain key to set the x-axis range (if you want to display)
40+
# the legend on the right instead of the top as done here
41+
xaxis=dict(domain=[0, 1]),
42+
# Add a title to the y-axis
43+
yaxis=dict(title="orig"),
44+
# by setting anchor=free, overlaying, and autoshift, the axis will be placed
45+
# automatically, without overlapping any other axes
46+
yaxis2=dict(
47+
title="negative",
48+
anchor="free",
49+
overlaying="y1",
50+
side="left",
51+
autoshift=True,
52+
),
53+
yaxis3=dict(
54+
title="sqrt(orig)",
55+
anchor="free",
56+
overlaying="y1",
57+
side="right",
58+
autoshift=True,
59+
),
60+
yaxis4=dict(
61+
title="orig ** 2",
62+
anchor="free",
63+
overlaying="y1",
64+
side="right",
65+
autoshift=True,
66+
),
67+
)
68+
69+
# Update layout properties
70+
fig.update_layout(
71+
title_text="multiple y-axes example",
72+
height=600,
73+
legend=dict(
74+
orientation="h",
75+
yanchor="bottom",
76+
y=1.02,
77+
xanchor="right",
78+
x=1,
79+
),
80+
template="plotly_white",
81+
)
82+
83+
# Test: check whether a single update triggers all traces to be updated
84+
out = fig.construct_update_data({"xaxis.range[0]": 0, "xaxis.range[1]": 50_000})
85+
assert len(out) == 5
86+
# fig.show_dash
87+
88+
89+
@pytest.mark.parametrize("fig_type", [FigureResampler, FigureWidgetResampler])
90+
def test_multiple_axes_subplot_rows(fig_type):
91+
# Generate some data
92+
x = np.arange(200_000)
93+
sin = 3 + np.sin(x / 200) + np.random.randn(len(x)) / 30
94+
95+
# create a figure with 2 rows and 1 column
96+
# NOTE: instead of the above methods, we don't add the "yaxis" argument to the
97+
# scatter object
98+
fig = fig_type(make_subplots(rows=2, cols=1, shared_xaxes=True))
99+
fig.add_trace(go.Scatter(name="orig"), hf_x=x, hf_y=sin, row=2, col=1)
100+
fig.add_trace(go.Scatter(name="-orig"), hf_x=x, hf_y=-sin, row=2, col=1)
101+
fig.add_trace(go.Scatter(name="sqrt"), hf_x=x, hf_y=np.sqrt(sin * 10), row=2, col=1)
102+
fig.add_trace(go.Scatter(name="orig**2"), hf_x=x, hf_y=(sin - 3) ** 2, row=2, col=1)
103+
104+
# NOTE: because of the row and col specification, the yaxis is automatically set to y2
105+
for i, data in enumerate(fig.data[1:], 3):
106+
data.update(yaxis=f"y{i}")
107+
108+
# add the original signal to the first row subplot
109+
fig.add_trace(go.Scatter(name="<b>orig</b>"), row=1, col=1, hf_x=x, hf_y=sin)
110+
111+
# in order for autoshift to work, you need to set x-anchor to free
112+
fig.update_layout(
113+
xaxis2=dict(domain=[0, 1], anchor="y2"),
114+
yaxis2=dict(title="orig"),
115+
yaxis3=dict(
116+
title="-orig",
117+
anchor="free",
118+
overlaying="y2",
119+
side="left",
120+
autoshift=True,
121+
),
122+
yaxis4=dict(
123+
title="sqrt(orig)",
124+
anchor="free",
125+
overlaying="y2",
126+
side="right",
127+
autoshift=True,
128+
),
129+
yaxis5=dict(
130+
title="orig ** 2",
131+
anchor="free",
132+
overlaying="y2",
133+
side="right",
134+
autoshift=True,
135+
),
136+
)
137+
138+
# Update layout properties
139+
fig.update_layout(
140+
title_text="multiple y-axes example",
141+
height=800,
142+
legend=dict(
143+
orientation="h",
144+
yanchor="bottom",
145+
y=1.02,
146+
xanchor="right",
147+
x=1,
148+
),
149+
template="plotly_white",
150+
)
151+
152+
# Test: check whether a single update triggers all traces to be updated
153+
out = fig.construct_update_data(
154+
{
155+
"xaxis.range[0]": 0,
156+
"xaxis.range[1]": 50_000,
157+
"xaxis2.range[0]": 0,
158+
"xaxis2.range[1]": 50_000,
159+
}
160+
)
161+
assert len(out) == 6
162+
163+
164+
@pytest.mark.parametrize("fig_type", [FigureResampler, FigureWidgetResampler])
165+
def test_multiple_axes_subplot_cols(fig_type):
166+
x = np.arange(200_000)
167+
sin = 3 + np.sin(x / 200) + np.random.randn(len(x)) / 30
168+
169+
# Create a figure with 1 row and 2 columns
170+
fig = fig_type(make_subplots(rows=1, cols=2))
171+
fig.add_trace(go.Scatter(name="orig"), hf_x=x, hf_y=sin, row=1, col=2)
172+
fig.add_trace(go.Scatter(name="-orig"), hf_x=x, hf_y=-sin, row=1, col=2)
173+
fig.add_trace(go.Scatter(name="sqrt"), hf_x=x, hf_y=np.sqrt(sin * 10), row=1, col=2)
174+
fig.add_trace(go.Scatter(name="orig**2"), hf_x=x, hf_y=(sin - 3) ** 2, row=1, col=2)
175+
176+
# NOTE: because of the row & col specification, the yaxis is automatically set to y2
177+
for i, data in enumerate(fig.data[1:], 3):
178+
data.update(yaxis=f"y{i}")
179+
180+
fig.add_trace(go.Scatter(name="<b>orig</b>"), row=1, col=1, hf_x=x, hf_y=sin)
181+
182+
# In order for autoshift to work, you need to set x-anchor to free
183+
fig.update_layout(
184+
xaxis=dict(domain=[0, 0.4]),
185+
xaxis2=dict(domain=[0.56, 1]),
186+
yaxis2=dict(title="orig"),
187+
yaxis3=dict(
188+
title="-orig",
189+
anchor="free",
190+
overlaying="y2",
191+
side="left",
192+
autoshift=True,
193+
),
194+
yaxis4=dict(
195+
title="sqrt(orig)",
196+
anchor="free",
197+
overlaying="y2",
198+
side="right",
199+
autoshift=True,
200+
),
201+
yaxis5=dict(
202+
title="orig ** 2",
203+
anchor="free",
204+
overlaying="y2",
205+
side="right",
206+
autoshift=True,
207+
),
208+
)
209+
210+
# Update layout properties
211+
fig.update_layout(
212+
title_text="multiple y-axes example",
213+
height=300,
214+
legend=dict(
215+
orientation="h",
216+
yanchor="bottom",
217+
y=1.02,
218+
xanchor="right",
219+
x=1,
220+
),
221+
template="plotly_white",
222+
)
223+
224+
out = fig.construct_update_data(
225+
{
226+
"xaxis.range[0]": 0,
227+
"xaxis.range[1]": 50_000,
228+
}
229+
)
230+
assert len(out) == 2
231+
232+
out = fig.construct_update_data(
233+
{
234+
"xaxis2.range[0]": 0,
235+
"xaxis2.range[1]": 50_000,
236+
}
237+
)
238+
assert len(out) == 5

0 commit comments

Comments
 (0)