1
- # Prototype for px.combine
1
+ # Prototype for px.overlay
2
2
# Combine 2 figures containing subplots
3
3
# Run as
4
- # python px_combine .py
4
+ # python px_overlay .py
5
5
6
6
import plotly .express as px
7
7
import plotly .graph_objects as go
@@ -111,7 +111,9 @@ def find_subplot_axes(fig, row, col, secondary_y=False):
111
111
nrows , ncols = fig_grid_ref_shape (fig )
112
112
try :
113
113
sps = fig ._grid_ref [row - 1 ][col - 1 ]
114
- except IndexError :
114
+ except (IndexError , TypeError ):
115
+ # IndexError if fig has _grid_ref but not requested row or column,
116
+ # TypeError if fig has no _grid_ref (it is None)
115
117
raise IndexError (
116
118
"Figure does not have a subplot at the requested row or column."
117
119
)
@@ -142,7 +144,15 @@ def _check_is_secondary_y(sp):
142
144
return sp .layout_keys
143
145
144
146
145
- def map_axis_pair (old_fig , new_fig , axpair , make_axis_ref = True ):
147
+ def map_axis_pair (
148
+ old_fig ,
149
+ new_fig ,
150
+ axpair ,
151
+ new_row = None ,
152
+ new_col = None ,
153
+ new_secondary_y = None ,
154
+ make_axis_ref = True ,
155
+ ):
146
156
"""
147
157
Find the axes on the new figure that will give the same subplot and
148
158
possibly secondary y axis as on the old figure. This can only
@@ -151,9 +161,14 @@ def map_axis_pair(old_fig, new_fig, axpair, make_axis_ref=True):
151
161
columns and secondary y-axes.
152
162
if make_axis_ref is True, axis is removed from the resulting strings, e.g., xaxis2 -> x2
153
163
"""
164
+ if None in axpair :
165
+ raise ValueError ("Cannot map axis whose value is None." )
154
166
if axpair == ("paper" , "paper" ):
155
- return ax
167
+ return axpair
156
168
row , col , secondary_y = axis_pair_to_row_col (old_fig , axpair )
169
+ row = new_row if new_row is not None else row
170
+ col = new_col if new_col is not None else col
171
+ secondary_y = new_secondary_y if new_secondary_y is not None else secondary_y
157
172
newaxpair = find_subplot_axes (new_fig , row , col , secondary_y )
158
173
axpair_extras = [" domain" if ax .endswith ("domain" ) else "" for ax in axpair ]
159
174
newaxpair = tuple (ax + extra for ax , extra in zip (newaxpair , axpair_extras ))
@@ -162,7 +177,27 @@ def map_axis_pair(old_fig, new_fig, axpair, make_axis_ref=True):
162
177
return newaxpair
163
178
164
179
165
- def px_simple_combine (fig0 , fig1 , fig1_secondary_y = False ):
180
+ def map_annotation_like_obj_axis (oldfig , newfig , an , force_secondary_y = False ):
181
+ """
182
+ Take an annotation-like object with xref and yref referring to axes in oldfig
183
+ and map them to axes in newfig. This makes it possible to map an annotation
184
+ to the same subplot row, column or secondary y in a new plot even if they do
185
+ not have matching subplots.
186
+ If force_secondary_y is True, attempt is made to map the annotation to a
187
+ secondary y axis in the new figure.
188
+ Returns the new annotation. Note that it has not been added to newfig, the
189
+ caller must then do this if it wants it added to newfig.
190
+ """
191
+ oldaxpair = tuple ([an [ref ] for ref in ["xref" , "yref" ]])
192
+ newaxpair = map_axis_pair (
193
+ oldfig , newfig , oldaxpair , new_secondary_y = force_secondary_y
194
+ )
195
+ newan = an .__class__ (an )
196
+ newan ["xref" ], newan ["yref" ] = newaxpair
197
+ return newan
198
+
199
+
200
+ def px_simple_overlay (fig0 , fig1 , fig1_secondary_y = False ):
166
201
"""
167
202
Combines two figures by just using the layout of the first figure and
168
203
appending the data of the second figure.
@@ -177,7 +212,7 @@ def px_simple_combine(fig0, fig1, fig1_secondary_y=False):
177
212
grid_ref_shape = fig_grid_ref_shape (fig0 )
178
213
if grid_ref_shape != fig_grid_ref_shape (fig1 ):
179
214
raise ValueError (
180
- "Only two figures with the same subplot geometry can be combined ."
215
+ "Only two figures with the same subplot geometry can be overlayd ."
181
216
)
182
217
# reflow the colors
183
218
colorway = fig0 .layout .template .layout .colorway
@@ -209,7 +244,28 @@ def px_simple_combine(fig0, fig1, fig1_secondary_y=False):
209
244
tr , row = r + 1 , col = c + 1 , secondary_y = (fig1_secondary_y and (f == fig1 ))
210
245
)
211
246
# TODO: How to preserve axis sizes when adding secondary y?
212
- # TODO: How to put annotations on the correct subplot when using secondary y?
247
+
248
+ # Map the axes of the annotation-like objects to the new figure. Map the
249
+ # fig1 objects to the secondary-y if requested.
250
+ selectors = product (
251
+ [fig0 , fig1 ],
252
+ [
253
+ go .Figure .select_annotations ,
254
+ go .Figure .select_shapes ,
255
+ go .Figure .select_layout_images ,
256
+ ],
257
+ )
258
+ adders = product (
259
+ [(fig , False ), (fig , fig1_secondary_y )],
260
+ [go .Figure .add_annotation , go .Figure .add_shape , go .Figure .add_layout_image ],
261
+ )
262
+ for (oldfig , selector ), ((newfig , secy ), adder ) in zip (selectors , adders ):
263
+ for ann in selector (oldfig ):
264
+ newann = map_annotation_like_obj_axis (
265
+ oldfig , newfig , ann , force_secondary_y = secy
266
+ )
267
+ adder (newfig , newann )
268
+
213
269
# fig.update_layout(fig0.layout)
214
270
# title will be wrong
215
271
fig .layout .title = None
@@ -270,17 +326,17 @@ def get_first_set_barmode(figs):
270
326
df = test_data .aug_tips ()
271
327
272
328
273
- def simple_combine_example ():
329
+ def simple_overlay_example ():
274
330
fig0 = px .scatter (df , x = "total_bill" , y = "tip" , facet_row = "sex" , facet_col = "smoker" )
275
331
fig1 = px .histogram (
276
332
df , x = "total_bill" , y = "tip" , facet_row = "sex" , facet_col = "smoker"
277
333
)
278
334
fig1 .update_traces (marker_color = "red" )
279
- fig = px_simple_combine (fig0 , fig1 )
335
+ fig = px_simple_overlay (fig0 , fig1 )
280
336
fig .update_layout (title = "Simple figure combination" )
281
337
return fig
282
338
283
339
284
340
if __name__ == "__main__" :
285
- fig_simple = simple_combine_example ()
341
+ fig_simple = simple_overlay_example ()
286
342
fig_simple .show ()
0 commit comments