Skip to content

Commit 8535f8d

Browse files
Convert axhline, axvline, and axline to layout shapes
1 parent d3105d4 commit 8535f8d

2 files changed

Lines changed: 102 additions & 2 deletions

File tree

plotly/matplotlylib/renderer.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import warnings
1111

12+
from matplotlib import transforms
1213
import plotly.graph_objs as go
1314
from plotly.matplotlylib.mplexporter import Renderer
1415
from plotly.matplotlylib import mpltools
@@ -459,8 +460,18 @@ def draw_marked_line(self, **props):
459460
self.plotly_fig.add_trace(marked_line)
460461
self.msg += " Heck yeah, I drew that line\n"
461462
elif props["coordinates"] == "axes":
462-
# dealing with legend graphical elements
463-
self.msg += " Using native legend\n"
463+
if self._processing_legend:
464+
# dealing with legend graphical elements
465+
self.msg += " Using native legend\n"
466+
else:
467+
# horizontal/vertical reference lines (axhline/axvline)
468+
self._draw_axes_line(props)
469+
elif props["coordinates"] == "display" and isinstance(
470+
props["mplobj"].get_transform(), transforms.BlendedGenericTransform
471+
):
472+
# axhline/axvline: blended axes/data transforms are reported as
473+
# display coordinates by the exporter
474+
self._draw_axes_line(props)
464475
else:
465476
self.msg += " Line didn't have 'data' coordinates, not drawing\n"
466477
warnings.warn(
@@ -469,6 +480,37 @@ def draw_marked_line(self, **props):
469480
"coordinates!"
470481
)
471482

483+
def _draw_axes_line(self, props):
484+
"""Draw an axes-coordinate reference line (axhline/axvline) as a
485+
layout shape spanning the line's endpoints in data coordinates."""
486+
ax = self.current_mpl_ax
487+
trans = props["mplobj"].get_transform()
488+
if props["coordinates"] == "display":
489+
px_points = props["data"]
490+
else:
491+
px_points = [trans.transform(pt) for pt in props["data"]]
492+
(x0, y0), (x1, y1) = [ax.transData.inverted().transform(pt) for pt in px_points]
493+
color = mpltools.merge_color_and_opacity(
494+
props["linestyle"]["color"], props["linestyle"]["alpha"]
495+
)
496+
shape = go.layout.Shape(
497+
type="line",
498+
x0=x0,
499+
y0=y0,
500+
x1=x1,
501+
y1=y1,
502+
xref="x{0}".format(self.axis_ct),
503+
yref="y{0}".format(self.axis_ct),
504+
line=go.layout.shape.Line(
505+
color=color,
506+
width=props["linestyle"]["linewidth"],
507+
dash=mpltools.convert_dash(props["linestyle"]["dasharray"]),
508+
),
509+
layer="above",
510+
)
511+
self.plotly_fig["layout"]["shapes"] += (shape,)
512+
self.msg += " Heck yeah, I drew that reference line\n"
513+
472514
def draw_image(self, **props):
473515
"""Draw image.
474516

plotly/matplotlylib/tests/test_renderer.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,64 @@ def test_custom_date_xtickvals_are_converted():
315315
)
316316

317317

318+
def test_axhline_converts():
319+
"""axhline converts to a layout shape spanning the axes width."""
320+
fig, ax = plt.subplots()
321+
ax.axhline(0.5)
322+
323+
plotly_fig = tls.mpl_to_plotly(fig)
324+
325+
assert len(plotly_fig.data) == 0
326+
assert len(plotly_fig.layout.shapes) == 1
327+
shape = plotly_fig.layout.shapes[0]
328+
assert shape.type == "line"
329+
x0, x1 = ax.get_xlim()
330+
assert abs(shape.x0 - x0) < 1e-9
331+
assert abs(shape.x1 - x1) < 1e-9
332+
assert abs(shape.y0 - 0.5) < 1e-9
333+
assert abs(shape.y1 - 0.5) < 1e-9
334+
assert shape.xref == "x"
335+
assert shape.yref == "y"
336+
assert shape.line.color == "rgba(31, 119, 180, 1)"
337+
338+
339+
def test_axvline_converts():
340+
"""axvline converts to a layout shape spanning the axes height."""
341+
fig, ax = plt.subplots()
342+
ax.axvline(0.5)
343+
344+
plotly_fig = tls.mpl_to_plotly(fig)
345+
346+
assert len(plotly_fig.data) == 0
347+
assert len(plotly_fig.layout.shapes) == 1
348+
shape = plotly_fig.layout.shapes[0]
349+
assert shape.type == "line"
350+
y0, y1 = ax.get_ylim()
351+
assert abs(shape.x0 - 0.5) < 1e-9
352+
assert abs(shape.x1 - 0.5) < 1e-9
353+
assert abs(shape.y0 - y0) < 1e-9
354+
assert abs(shape.y1 - y1) < 1e-9
355+
356+
357+
def test_axline_converts():
358+
"""axline converts to a layout shape spanning the whole axes box."""
359+
fig, ax = plt.subplots()
360+
ax.axline((0.5, 0.5), slope=1)
361+
362+
plotly_fig = tls.mpl_to_plotly(fig)
363+
364+
assert len(plotly_fig.data) == 0
365+
assert len(plotly_fig.layout.shapes) == 1
366+
shape = plotly_fig.layout.shapes[0]
367+
assert shape.type == "line"
368+
x0, x1 = ax.get_xlim()
369+
y0, y1 = ax.get_ylim()
370+
assert abs(shape.x0 - x0) < 1e-9
371+
assert abs(shape.x1 - x1) < 1e-9
372+
assert abs(shape.y0 - y0) < 1e-9
373+
assert abs(shape.y1 - y1) < 1e-9
374+
375+
318376
def test_uneven_custom_date_xtickvals_are_converted():
319377
"""Unevenly spaced custom date ticks must be converted to date strings."""
320378
dates = [datetime.datetime(2023, 1, i) for i in range(1, 11)]

0 commit comments

Comments
 (0)