Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions plotly/matplotlylib/renderer.py

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your changes have surfaced a bug! Dotted lines can now throw an error because of an issue here. "circle" isn't a valid dash option. Could you please make the following update?

"2,2": "dot",

It also looks like convert_dash can return invalid values. While you're fixing this issue, you might want to address that one. Valid values are the following: 'solid', 'dot', 'dash', 'longdash', 'dashdot', 'longdashdot'

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import warnings

from matplotlib import transforms
import plotly.graph_objs as go
from plotly.matplotlylib.mplexporter import Renderer
from plotly.matplotlylib import mpltools
Expand Down Expand Up @@ -459,8 +460,18 @@ def draw_marked_line(self, **props):
self.plotly_fig.add_trace(marked_line)
self.msg += " Heck yeah, I drew that line\n"
elif props["coordinates"] == "axes":
# dealing with legend graphical elements
self.msg += " Using native legend\n"
if self._processing_legend:
# dealing with legend graphical elements
self.msg += " Using native legend\n"
else:
# horizontal/vertical reference lines (axhline/axvline)
self._draw_axes_line(props)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sending lines to _draw_axes_line that don't have exactly two points will throw an error (the tuple unpacking fails). Additionally, lines with two points but markers only will also throw an error (because props["linestyle"] = None, so no color can be read).

elif props["coordinates"] == "display" and isinstance(
props["mplobj"].get_transform(), transforms.BlendedGenericTransform
):
# axhline/axvline: blended axes/data transforms are reported as
# display coordinates by the exporter
self._draw_axes_line(props)
else:
self.msg += " Line didn't have 'data' coordinates, not drawing\n"
warnings.warn(
Expand All @@ -469,6 +480,37 @@ def draw_marked_line(self, **props):
"coordinates!"
)

def _draw_axes_line(self, props):
"""Draw an axes-coordinate reference line (axhline/axvline) as a
layout shape spanning the line's endpoints in data coordinates."""
ax = self.current_mpl_ax
trans = props["mplobj"].get_transform()
if props["coordinates"] == "display":
px_points = props["data"]
else:
px_points = [trans.transform(pt) for pt in props["data"]]
(x0, y0), (x1, y1) = [ax.transData.inverted().transform(pt) for pt in px_points]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll need to add a check to see if these are date values. mpl uses a different date format that plotly, so you'll need to convert the values if they're dates. You could do something like this:

if self.x_is_mpl_date:
    x0, x1 = self._convert_x_dates([x0, x1])

color = mpltools.merge_color_and_opacity(
props["linestyle"]["color"], props["linestyle"]["alpha"]
)
shape = go.layout.Shape(
type="line",
x0=x0,
y0=y0,
x1=x1,
y1=y1,
xref="x{0}".format(self.axis_ct),
yref="y{0}".format(self.axis_ct),
Comment on lines +502 to +503

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works as long as the user doesn't zoom/pan the chart. Once they do, the lines no longer span the chart. You could update these to use "x domain" and "y domain" for the proper lines.

Image

line=go.layout.shape.Line(
color=color,
width=props["linestyle"]["linewidth"],
dash=mpltools.convert_dash(props["linestyle"]["dasharray"]),
),
layer="above",
)
self.plotly_fig["layout"]["shapes"] += (shape,)
self.msg += " Heck yeah, I drew that reference line\n"

def draw_image(self, **props):
"""Draw image.

Expand Down
58 changes: 58 additions & 0 deletions plotly/matplotlylib/tests/test_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,64 @@ def test_custom_date_xtickvals_are_converted():
)


def test_axhline_converts():
"""axhline converts to a layout shape spanning the axes width."""
fig, ax = plt.subplots()
ax.axhline(0.5)

plotly_fig = tls.mpl_to_plotly(fig)

assert len(plotly_fig.data) == 0
assert len(plotly_fig.layout.shapes) == 1
shape = plotly_fig.layout.shapes[0]
assert shape.type == "line"
x0, x1 = ax.get_xlim()
assert abs(shape.x0 - x0) < 1e-9
assert abs(shape.x1 - x1) < 1e-9
assert abs(shape.y0 - 0.5) < 1e-9
assert abs(shape.y1 - 0.5) < 1e-9
assert shape.xref == "x"
assert shape.yref == "y"
assert shape.line.color == "rgba(31, 119, 180, 1)"


def test_axvline_converts():
"""axvline converts to a layout shape spanning the axes height."""
fig, ax = plt.subplots()
ax.axvline(0.5)

plotly_fig = tls.mpl_to_plotly(fig)

assert len(plotly_fig.data) == 0
assert len(plotly_fig.layout.shapes) == 1
shape = plotly_fig.layout.shapes[0]
assert shape.type == "line"
y0, y1 = ax.get_ylim()
assert abs(shape.x0 - 0.5) < 1e-9
assert abs(shape.x1 - 0.5) < 1e-9
assert abs(shape.y0 - y0) < 1e-9
assert abs(shape.y1 - y1) < 1e-9


def test_axline_converts():
"""axline converts to a layout shape spanning the whole axes box."""
fig, ax = plt.subplots()
ax.axline((0.5, 0.5), slope=1)

plotly_fig = tls.mpl_to_plotly(fig)

assert len(plotly_fig.data) == 0
assert len(plotly_fig.layout.shapes) == 1
shape = plotly_fig.layout.shapes[0]
assert shape.type == "line"
x0, x1 = ax.get_xlim()
y0, y1 = ax.get_ylim()
assert abs(shape.x0 - x0) < 1e-9
assert abs(shape.x1 - x1) < 1e-9
assert abs(shape.y0 - y0) < 1e-9
assert abs(shape.y1 - y1) < 1e-9


def test_uneven_custom_date_xtickvals_are_converted():
"""Unevenly spaced custom date ticks must be converted to date strings."""
dates = [datetime.datetime(2023, 1, i) for i in range(1, 11)]
Expand Down
Loading