Skip to content

Commit d8d4b6d

Browse files
Convert axhspan and axvspan to layout shapes
1 parent 63895f5 commit d8d4b6d

2 files changed

Lines changed: 119 additions & 0 deletions

File tree

‎plotly/matplotlylib/renderer.py‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,21 @@ 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."""
61+
if isinstance(
62+
transform, (transforms.BlendedGenericTransform, transforms.BlendedAffine2D)
63+
):
64+
return True
65+
if hasattr(transform, "_a") and hasattr(transform, "_b"):
66+
return _has_blended_transform(transform._a) or _has_blended_transform(
67+
transform._b
68+
)
69+
if hasattr(transform, "_transform"):
70+
return _has_blended_transform(transform._transform)
71+
return False
72+
73+
5974
class PlotlyRenderer(Renderer):
6075
"""A renderer class inheriting from base for rendering mpl plots in plotly.
6176
@@ -683,6 +698,39 @@ def _draw_axes_line(self, props):
683698
self.msg += " Heck yeah, I drew that reference line\n"
684699
return True
685700

701+
def _draw_axes_span(self, props):
702+
"""Draw a reference span (axhspan/axvspan) as a layout shape spanning
703+
the region in data coordinates."""
704+
ax = self.current_mpl_ax
705+
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]
709+
x0, x1 = float(min(xs)), float(max(xs))
710+
y0, y1 = float(min(ys)), float(max(ys))
711+
style = props["style"]
712+
fillcolor = _export_color(style["facecolor"])
713+
edgecolor = _export_color(style["edgecolor"])
714+
linewidth = style["edgewidth"] if style["edgecolor"] != "none" else 0
715+
shape = go.layout.Shape(
716+
type="rect",
717+
x0=x0,
718+
y0=y0,
719+
x1=x1,
720+
y1=y1,
721+
xref="x{0}".format(self.axis_ct),
722+
yref="y{0}".format(self.axis_ct),
723+
fillcolor=fillcolor,
724+
line=go.layout.shape.Line(
725+
color=edgecolor,
726+
width=linewidth,
727+
dash=mpltools.convert_dash(style["dasharray"]),
728+
),
729+
layer="below" if style.get("zorder", 1) < 2 else "above",
730+
)
731+
self.plotly_fig["layout"]["shapes"] += (shape,)
732+
self.msg += " Heck yeah, I drew that reference span\n"
733+
686734
def draw_image(self, **props):
687735
"""Draw image.
688736
@@ -908,6 +956,16 @@ def draw_path(self, **props):
908956
elif isinstance(props["mplobj"], mpatches.StepPatch):
909957
self.msg += " Drawing a step path\n"
910958
self._draw_step_path(props)
959+
elif (
960+
isinstance(props["mplobj"], (mpatches.Rectangle, mpatches.Polygon))
961+
and props["coordinates"] == "display"
962+
and _has_blended_transform(props["mplobj"].get_transform())
963+
):
964+
if self._processing_legend:
965+
self.msg += " Using native legend\n"
966+
else:
967+
self.msg += " Drawing an axes span\n"
968+
self._draw_axes_span(props)
911969
else:
912970
self.msg += " This path isn't a bar, not drawing\n"
913971
warnings.warn(

‎plotly/matplotlylib/tests/test_renderer.py‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1392,3 +1392,64 @@ def test_mixed_style_lines_group_consecutive_matches():
13921392
else:
13931393
# older matplotlib emits one collection per level
13941394
assert dashes == ["5.55px,2.4px"] * 2 + ["solid"] * 2
1395+
1396+
1397+
def test_axhspan_converts():
1398+
"""axhspan converts to a layout shape spanning the axes width."""
1399+
fig, ax = plt.subplots()
1400+
ax.axhspan(0.25, 0.75)
1401+
1402+
plotly_fig = tls.mpl_to_plotly(fig)
1403+
1404+
assert len(plotly_fig.data) == 0
1405+
assert len(plotly_fig.layout.shapes) == 1
1406+
shape = plotly_fig.layout.shapes[0]
1407+
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
1411+
assert abs(shape.y0 - 0.25) < 1e-9
1412+
assert abs(shape.y1 - 0.75) < 1e-9
1413+
assert shape.xref == "x"
1414+
assert shape.yref == "y"
1415+
assert shape.fillcolor == "#1F77B4"
1416+
assert shape.layer == "below"
1417+
1418+
1419+
def test_axvspan_converts():
1420+
"""axvspan converts to a layout shape spanning the axes height."""
1421+
fig, ax = plt.subplots()
1422+
ax.axvspan(0.25, 0.75)
1423+
1424+
plotly_fig = tls.mpl_to_plotly(fig)
1425+
1426+
assert len(plotly_fig.data) == 0
1427+
assert len(plotly_fig.layout.shapes) == 1
1428+
shape = plotly_fig.layout.shapes[0]
1429+
assert shape.type == "rect"
1430+
y0, y1 = ax.get_ylim()
1431+
assert abs(shape.x0 - 0.25) < 1e-9
1432+
assert abs(shape.x1 - 0.75) < 1e-9
1433+
assert abs(shape.y0 - y0) < 1e-9
1434+
assert abs(shape.y1 - y1) < 1e-9
1435+
assert shape.xref == "x"
1436+
assert shape.yref == "y"
1437+
assert shape.fillcolor == "#1F77B4"
1438+
assert shape.layer == "below"
1439+
1440+
1441+
def test_axhspan_with_custom_style_converts():
1442+
"""axhspan with custom colors and alpha converts to a styled layout shape."""
1443+
fig, ax = plt.subplots()
1444+
ax.axhspan(0.25, 0.75, facecolor="red", edgecolor="blue", alpha=0.5, linestyle="--")
1445+
1446+
plotly_fig = tls.mpl_to_plotly(fig)
1447+
1448+
assert len(plotly_fig.data) == 0
1449+
assert len(plotly_fig.layout.shapes) == 1
1450+
shape = plotly_fig.layout.shapes[0]
1451+
assert shape.type == "rect"
1452+
assert shape.fillcolor == "rgba(255, 0, 0, 0.5)"
1453+
assert shape.line.color == "rgba(0, 0, 255, 0.5)"
1454+
assert shape.line.dash == "dash"
1455+
assert shape.line.width == 1.0

0 commit comments

Comments
 (0)