Skip to content

Commit bf7cf9b

Browse files
annotation-like objects axis reference mapping
to a subplot on a new figure. This will allow us to copy the annotations to a new figure with more axes as part of a px.overlay command. This just needs to be generalized to work with shapes and images but the workings can be seen in proto/px_overlay/map_axis_pair_example.py.
1 parent ec2332f commit bf7cf9b

File tree

3 files changed

+182
-0
lines changed

3 files changed

+182
-0
lines changed
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import plotly.graph_objects as go
2+
from plotly.subplots import make_subplots
3+
import px_overlay
4+
import pytest
5+
6+
fig0 = px_overlay.make_subplots_all_secondary_y(3, 4)
7+
fig1 = px_overlay.make_subplots_all_secondary_y(4, 5)
8+
9+
for dims, f in zip([(3, 4), (4, 5)], [fig0, fig1]):
10+
for r, c in px_overlay.multi_index(*dims):
11+
for sy in [False, True]:
12+
f.add_trace(go.Scatter(x=[], y=[]), row=r + 1, col=c + 1, secondary_y=sy)
13+
14+
fig0.add_annotation(row=2, col=3, text="hi", x=0.25, xref="x domain", y=3)
15+
fig0.add_annotation(
16+
row=3, col=4, text="hi", x=0.25, xref="x domain", y=2, secondary_y=True
17+
)
18+
19+
for an in fig0.layout.annotations:
20+
oldaxpair = tuple([an[ref] for ref in ["xref", "yref"]])
21+
newaxpair = px_overlay.map_axis_pair(fig0, fig1, oldaxpair)
22+
newan = go.layout.Annotation(an)
23+
print(oldaxpair)
24+
print(newaxpair)
25+
newan["xref"], newan["yref"] = newaxpair
26+
fig1.add_annotation(newan)
27+
28+
fig0.show()
29+
fig1.show()

proto/px_overlay/px_overlay.py

+113
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import json
1111
from itertools import product, cycle, chain
1212
from functools import reduce
13+
import re
1314

1415

1516
def multi_index(*kwargs):
@@ -49,6 +50,118 @@ def extract_axis_titles(fig):
4950
return (r_titles, c_titles)
5051

5152

53+
def make_subplots_all_secondary_y(rows, cols):
54+
"""
55+
Get subplots like make_subplots but all also have secondary y-axes.
56+
"""
57+
grid_ref_shape = [rows, cols]
58+
specs = [
59+
[dict(secondary_y=True) for __ in range(grid_ref_shape[1])]
60+
for _ in range(grid_ref_shape[0])
61+
]
62+
fig = make_subplots(*grid_ref_shape, specs=specs)
63+
return fig
64+
65+
66+
def parse_axis_ref(ax):
67+
""" Find the axis letter, optional number, and domain of axis. """
68+
# TODO: can this be obtained via codegen?
69+
pat = re.compile("([xy])(axis)?([0-9]*)( domain)?")
70+
matches = pat.match(ax)
71+
if matches is None:
72+
raise ValueError('Axis "%s" cannot be parsed.' % (ax,))
73+
return (matches[1], matches[3], matches[4])
74+
75+
76+
def norm_axis_ref(ax):
77+
""" normalize ax so it is in the format: yaxis, yaxis2, xaxis7 etc. """
78+
al, an, _ = parse_axis_ref(ax)
79+
return al + "axis" + an
80+
81+
82+
def axis_pair_to_row_col(fig, axpair):
83+
"""
84+
returns the row and column of the subplot having the axis pair and whether it is a
85+
secondary y
86+
"""
87+
if "paper" in axpair:
88+
raise ValueError('Cannot find row and column of "paper" axis reference.')
89+
naxpair = tuple([norm_axis_ref(ax) for ax in axpair])
90+
nrows, ncols = fig_grid_ref_shape(fig)
91+
row = None
92+
col = None
93+
for r, c in multi_index(nrows, ncols):
94+
for sp in fig._grid_ref[r][c]:
95+
if naxpair == sp.layout_keys:
96+
row = r + 1
97+
col = c + 1
98+
if row is None or col is None:
99+
raise ValueError("Could not find subplot containing axes (%s,%s)." % nax)
100+
secondary_y = False
101+
yax = naxpair[1]
102+
if fig.layout[yax]["side"] == "right":
103+
secondary_y = True
104+
return (row, col, secondary_y)
105+
106+
107+
def find_subplot_axes(fig, row, col, secondary_y=False):
108+
"""
109+
Returns 2-tuple containing (xaxis,yaxis) at specified row, col and secondary y-axis.
110+
"""
111+
nrows, ncols = fig_grid_ref_shape(fig)
112+
try:
113+
sps = fig._grid_ref[row - 1][col - 1]
114+
except IndexError:
115+
raise IndexError(
116+
"Figure does not have a subplot at the requested row or column."
117+
)
118+
119+
def _check_is_secondary_y(sp):
120+
xax, yax = sp.layout_keys
121+
# TODO: It may not be totally accurate to assume if an y-axis' "side" is
122+
# "right" than it is a secondary y axis...
123+
return fig.layout[yax]["side"] == "right"
124+
125+
# find the secondary y axis
126+
err_msg = (
127+
"Could not find a y-axis " "at the subplot in the requested row or column."
128+
)
129+
filter_fun = lambda sp: not _check_is_secondary_y(sp)
130+
if secondary_y:
131+
err_msg = (
132+
"Could not find a secondary y-axis "
133+
"at the subplot in the requested row or column."
134+
)
135+
filter_fun = _check_is_secondary_y
136+
try:
137+
sp = list(filter(filter_fun, sps))[0]
138+
except (IndexError, TypeError):
139+
# Catch IndexError if the list is empty, catch TypeError if sps isn't
140+
# iterable (e.g., is None)
141+
raise IndexError(err_msg)
142+
return sp.layout_keys
143+
144+
145+
def map_axis_pair(old_fig, new_fig, axpair, make_axis_ref=True):
146+
"""
147+
Find the axes on the new figure that will give the same subplot and
148+
possibly secondary y axis as on the old figure. This can only
149+
work if the axis pair is ("paper","paper") or the axis pair corresponds to a
150+
subplot on the old figure the new figure has corresponding rows,
151+
columns and secondary y-axes.
152+
if make_axis_ref is True, axis is removed from the resulting strings, e.g., xaxis2 -> x2
153+
"""
154+
if axpair == ("paper", "paper"):
155+
return ax
156+
row, col, secondary_y = axis_pair_to_row_col(old_fig, axpair)
157+
newaxpair = find_subplot_axes(new_fig, row, col, secondary_y)
158+
axpair_extras = [" domain" if ax.endswith("domain") else "" for ax in axpair]
159+
newaxpair = tuple(ax + extra for ax, extra in zip(newaxpair, axpair_extras))
160+
if make_axis_ref:
161+
newaxpair = tuple(ax.replace("axis", "") for ax in newaxpair)
162+
return newaxpair
163+
164+
52165
def px_simple_combine(fig0, fig1, fig1_secondary_y=False):
53166
"""
54167
Combines two figures by just using the layout of the first figure and
+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from plotly.subplots import make_subplots
2+
import px_overlay
3+
import pytest
4+
5+
fig = px_overlay.make_subplots_all_secondary_y(3, 4)
6+
fig_no_sy = px_overlay.make_subplots(3, 4)
7+
fig_custom = make_subplots(
8+
rows=2,
9+
cols=2,
10+
specs=[[{}, {}], [{"colspan": 2}, None]],
11+
subplot_titles=("First Subplot", "Second Subplot", "Third Subplot"),
12+
)
13+
14+
15+
def test_bad_row_col():
16+
with pytest.raises(
17+
IndexError,
18+
match=r"^Figure does not have a subplot at the requested row or column\.$",
19+
):
20+
px_overlay.find_subplot_axes(fig, 4, 2, secondary_y=False)
21+
with pytest.raises(
22+
IndexError,
23+
match=r"^Figure does not have a subplot at the requested row or column\.$",
24+
):
25+
px_overlay.find_subplot_axes(fig, 4, 2, secondary_y=True)
26+
27+
28+
def test_no_secondary_y():
29+
with pytest.raises(
30+
IndexError,
31+
match=r"^Could not find a secondary y-axis at the subplot in the requested row or column\.$",
32+
):
33+
px_overlay.find_subplot_axes(fig_no_sy, 2, 2, secondary_y=True)
34+
with pytest.raises(
35+
IndexError,
36+
match=r"^Could not find a y-axis at the subplot in the requested row or column\.$",
37+
):
38+
px_overlay.find_subplot_axes(fig_custom, 2, 2, secondary_y=False)
39+
axes = px_overlay.find_subplot_axes(fig_custom, 1, 2, secondary_y=False)
40+
assert axes == ("xaxis2", "yaxis2")

0 commit comments

Comments
 (0)