Skip to content

Commit 1ef0296

Browse files
committed
🔥 multiple y-axes support
1 parent cd735c3 commit 1ef0296

File tree

2 files changed

+243
-0
lines changed

2 files changed

+243
-0
lines changed

plotly_resampler/figure_resampler/figure_resampler_interface.py

+5
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

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