Skip to content

Commit 8a0df73

Browse files
Reference the axis domain for axes-fraction sides of spans
1 parent d8d4b6d commit 8a0df73

2 files changed

Lines changed: 75 additions & 25 deletions

File tree

‎plotly/matplotlylib/renderer.py‎

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,19 +56,26 @@ def _convert_collection_dash(linestyle):
5656
return ",".join("{0:g}px".format(d) for d in dashes)
5757

5858

59-
def _has_blended_transform(transform):
60-
"""Check whether a matplotlib transform contains a blended transform."""
59+
def _find_blended_transform(transform):
60+
"""Return the first blended transform within a matplotlib transform, or
61+
None when it contains no blended transform."""
6162
if isinstance(
6263
transform, (transforms.BlendedGenericTransform, transforms.BlendedAffine2D)
6364
):
64-
return True
65+
return transform
6566
if hasattr(transform, "_a") and hasattr(transform, "_b"):
66-
return _has_blended_transform(transform._a) or _has_blended_transform(
67-
transform._b
68-
)
67+
blended = _find_blended_transform(transform._a)
68+
if blended is None:
69+
blended = _find_blended_transform(transform._b)
70+
return blended
6971
if hasattr(transform, "_transform"):
70-
return _has_blended_transform(transform._transform)
71-
return False
72+
return _find_blended_transform(transform._transform)
73+
return None
74+
75+
76+
def _has_blended_transform(transform):
77+
"""Check whether a matplotlib transform contains a blended transform."""
78+
return _find_blended_transform(transform) is not None
7279

7380

7481
class PlotlyRenderer(Renderer):
@@ -699,15 +706,29 @@ def _draw_axes_line(self, props):
699706
return True
700707

701708
def _draw_axes_span(self, props):
702-
"""Draw a reference span (axhspan/axvspan) as a layout shape spanning
703-
the region in data coordinates."""
709+
"""Draw a reference span (axhspan/axvspan) as a layout shape.
710+
711+
Sides given in axes fractions (e.g. axhspan xmin/xmax) map onto the
712+
axis domain, so they stay fixed to the axes on pan/zoom; sides given
713+
in data coordinates map onto the data axis."""
704714
ax = self.current_mpl_ax
705715
px_points = props["data"]
706-
data_points = [ax.transData.inverted().transform(pt) for pt in px_points]
707-
xs = [pt[0] for pt in data_points]
708-
ys = [pt[1] for pt in data_points]
716+
blended = _find_blended_transform(props["mplobj"].get_transform())
717+
x_in_axes = blended._x == ax.transAxes
718+
y_in_axes = blended._y == ax.transAxes
719+
axes_points = ax.transAxes.inverted().transform(px_points)
720+
data_points = ax.transData.inverted().transform(px_points)
721+
xs = (axes_points if x_in_axes else data_points)[:, 0]
722+
ys = (axes_points if y_in_axes else data_points)[:, 1]
709723
x0, x1 = float(min(xs)), float(max(xs))
710724
y0, y1 = float(min(ys)), float(max(ys))
725+
axis_suffix = str(self.axis_ct) if self.axis_ct > 1 else ""
726+
xref = "x{0}".format(axis_suffix)
727+
yref = "y{0}".format(axis_suffix)
728+
if x_in_axes:
729+
xref += " domain"
730+
if y_in_axes:
731+
yref += " domain"
711732
style = props["style"]
712733
fillcolor = _export_color(style["facecolor"])
713734
edgecolor = _export_color(style["edgecolor"])
@@ -718,8 +739,8 @@ def _draw_axes_span(self, props):
718739
y0=y0,
719740
x1=x1,
720741
y1=y1,
721-
xref="x{0}".format(self.axis_ct),
722-
yref="y{0}".format(self.axis_ct),
742+
xref=xref,
743+
yref=yref,
723744
fillcolor=fillcolor,
724745
line=go.layout.shape.Line(
725746
color=edgecolor,

‎plotly/matplotlylib/tests/test_renderer.py‎

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1395,7 +1395,7 @@ def test_mixed_style_lines_group_consecutive_matches():
13951395

13961396

13971397
def test_axhspan_converts():
1398-
"""axhspan converts to a layout shape spanning the axes width."""
1398+
"""axhspan converts to a layout shape spanning the x axis domain."""
13991399
fig, ax = plt.subplots()
14001400
ax.axhspan(0.25, 0.75)
14011401

@@ -1405,19 +1405,18 @@ def test_axhspan_converts():
14051405
assert len(plotly_fig.layout.shapes) == 1
14061406
shape = plotly_fig.layout.shapes[0]
14071407
assert shape.type == "rect"
1408-
x0, x1 = ax.get_xlim()
1409-
assert abs(shape.x0 - x0) < 1e-9
1410-
assert abs(shape.x1 - x1) < 1e-9
1408+
assert abs(shape.x0 - 0) < 1e-9
1409+
assert abs(shape.x1 - 1) < 1e-9
14111410
assert abs(shape.y0 - 0.25) < 1e-9
14121411
assert abs(shape.y1 - 0.75) < 1e-9
1413-
assert shape.xref == "x"
1412+
assert shape.xref == "x domain"
14141413
assert shape.yref == "y"
14151414
assert shape.fillcolor == "#1F77B4"
14161415
assert shape.layer == "below"
14171416

14181417

14191418
def test_axvspan_converts():
1420-
"""axvspan converts to a layout shape spanning the axes height."""
1419+
"""axvspan converts to a layout shape spanning the y axis domain."""
14211420
fig, ax = plt.subplots()
14221421
ax.axvspan(0.25, 0.75)
14231422

@@ -1427,17 +1426,47 @@ def test_axvspan_converts():
14271426
assert len(plotly_fig.layout.shapes) == 1
14281427
shape = plotly_fig.layout.shapes[0]
14291428
assert shape.type == "rect"
1430-
y0, y1 = ax.get_ylim()
14311429
assert abs(shape.x0 - 0.25) < 1e-9
14321430
assert abs(shape.x1 - 0.75) < 1e-9
1433-
assert abs(shape.y0 - y0) < 1e-9
1434-
assert abs(shape.y1 - y1) < 1e-9
1431+
assert abs(shape.y0 - 0) < 1e-9
1432+
assert abs(shape.y1 - 1) < 1e-9
14351433
assert shape.xref == "x"
1436-
assert shape.yref == "y"
1434+
assert shape.yref == "y domain"
14371435
assert shape.fillcolor == "#1F77B4"
14381436
assert shape.layer == "below"
14391437

14401438

1439+
def test_axhspan_axes_fractions_convert():
1440+
"""axhspan xmin/xmax convert to x axis domain fractions and axvspan
1441+
ymin/ymax convert to y axis domain fractions."""
1442+
fig, ax = plt.subplots()
1443+
ax.plot([0, 10], [0, 10])
1444+
ax.axhspan(2, 4, xmin=0.2, xmax=0.6)
1445+
ax.axvspan(6, 8, ymin=0.1, ymax=0.5)
1446+
1447+
plotly_fig = tls.mpl_to_plotly(fig)
1448+
1449+
hspan, vspan = plotly_fig.layout.shapes
1450+
assert hspan.xref == "x domain"
1451+
assert hspan.yref == "y"
1452+
assert np.allclose([hspan.x0, hspan.x1, hspan.y0, hspan.y1], [0.2, 0.6, 2, 4])
1453+
assert vspan.xref == "x"
1454+
assert vspan.yref == "y domain"
1455+
assert np.allclose([vspan.x0, vspan.x1, vspan.y0, vspan.y1], [6, 8, 0.1, 0.5])
1456+
1457+
1458+
def test_axhspan_on_second_subplot_references_its_domain():
1459+
"""Spans on a second subplot reference that subplot's axis domain."""
1460+
fig, (ax1, ax2) = plt.subplots(1, 2)
1461+
ax2.axhspan(2, 4)
1462+
1463+
plotly_fig = tls.mpl_to_plotly(fig)
1464+
1465+
(shape,) = plotly_fig.layout.shapes
1466+
assert shape.xref == "x2 domain"
1467+
assert shape.yref == "y2"
1468+
1469+
14411470
def test_axhspan_with_custom_style_converts():
14421471
"""axhspan with custom colors and alpha converts to a styled layout shape."""
14431472
fig, ax = plt.subplots()

0 commit comments

Comments
 (0)