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
84 changes: 82 additions & 2 deletions plotly/matplotlylib/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,8 +526,13 @@ def draw_path_collection(self, **props):
self.msg += " Drawing path collection as markers\n"
self.draw_marked_line(**scatter_props)
elif props["path_coordinates"] == "data":
self.msg += " Drawing path collection as filled polygons\n"
self._draw_filled_path_collection(props)
if len(props["styles"]["facecolor"]) == 0:
# no face colors: a line collection (e.g. contour lines)
self.msg += " Drawing path collection as lines\n"
self._draw_line_collection(props)
else:
self.msg += " Drawing path collection as filled polygons\n"
self._draw_filled_path_collection(props)
else:
self.msg += " Path collection not linked to 'data', not drawing\n"
warnings.warn(
Expand All @@ -537,6 +542,81 @@ def draw_path_collection(self, **props):
"collections linked to 'data' coordinates"
)

def _draw_line_collection(self, 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.

This function currently draws one trace per path. What do you think of updating it to group consecutive same-style lines together into one trace? That would cut down on the number of traces (and make a legend less noisy).

"""Draw a path collection without face colors (e.g. contour lines)
as plain lines."""
edgecolors = mpltools.convert_rgba_array(props["styles"]["edgecolor"])
linewidths = mpltools.convert_linewidth_array(props["styles"]["linewidth"])

def per_path(colors, i, default):
if isinstance(colors, str):
return colors
if colors is None:
return default
try:
n = len(colors)
except TypeError:
return colors
return colors[i % n] if n else default
Comment on lines +551 to +560

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 helper function looks like the same one from _draw_filled_path_collection. Could you move it up a level and reuse it in both locations?


for i, (verts, codes) in enumerate(props["paths"]):
edgecolor = per_path(edgecolors, i, "rgba(0,0,0,0)")
linewidth = per_path(linewidths, i, 0)
# a path may contain several disjoint lines (e.g. contour lines
# of the same level); separate disjoint subpaths with None so
# plotly does not connect them.
# In SVG paths, codes carry different numbers of vertices:
# M/L: 1, C: 3 (cubic curve), S: 2 (smooth/quad curve), Z: 0.
code_steps = {"M": 1, "L": 1, "C": 3, "S": 2, "Z": 0}
subpaths = []
current = []
closed = False
vi = 0
for c in codes:
step = code_steps.get(c, 1)
if c == "M":
if current:
subpaths.append((current, closed))
current = [verts[vi]]
closed = False
vi += 1
elif c == "Z":
closed = True
else:
current.extend(verts[vi : vi + step])
vi += step
if current:
subpaths.append((current, closed))
x_combined = []
y_combined = []
for sub, closed in subpaths:
if len(sub) < 2:
continue
# a closed subpath (Z code) must be closed explicitly since
# plotly's lines mode does not close the loop
if closed:
sub = sub + [sub[0]]
sub_x = self._convert_x_dates([v[0] for v in sub])
sub_y = [v[1] for v in sub]
if x_combined:
x_combined.append(None)
y_combined.append(None)
x_combined.extend(sub_x)
y_combined.extend(sub_y)
if x_combined:
self.plotly_fig.add_trace(
go.Scatter(
x=x_combined,
y=y_combined,
mode="lines",
line=go.scatter.Line(
color=_export_color(edgecolor), width=linewidth
),
Comment on lines +612 to +614

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.

Will this handle dash styles?

What about turning off legend display? I think that came up in another PR.

xaxis="x{0}".format(self.axis_ct),
yaxis="y{0}".format(self.axis_ct),
)
)

def _draw_filled_path_collection(self, props):
"""Draw a path collection (e.g. violin plot bodies) as filled polygons."""
facecolors = mpltools.convert_rgba_array(props["styles"]["facecolor"])
Expand Down
38 changes: 37 additions & 1 deletion plotly/matplotlylib/tests/test_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.

Could you please add a test for the None separators as described in the PR description?

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

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import plotly.tools as tls


Expand Down Expand Up @@ -167,13 +168,15 @@ def test_stem_plot_renders():


def test_contour_lines_convert():
"""Contour lines used to crash with an ndarray line width."""
"""Contour lines must render as lines, not filled polygons."""
x = np.linspace(-3, 3, 30)
X, Y = np.meshgrid(x, x)
fig, ax = plt.subplots()
ax.contour(X, Y, np.sin(X) * np.cos(Y), 10)
plotly_fig = tls.mpl_to_plotly(fig)
assert len(plotly_fig.data) > 0
assert all(t.fill is None for t in plotly_fig.data)
assert all(t.mode == "lines" for t in plotly_fig.data)


def test_contourf_bands_render():
Expand Down Expand Up @@ -351,3 +354,36 @@ def test_custom_date_xtickvals_given_as_numbers_are_converted():
"2023-01-07 00:00:00",
"2023-01-10 00:00:00",
)


def test_contour_rings_are_closed():
"""Closed contour loops (Z codes) must close in plotly, not leave a gap."""
x = np.linspace(-3, 3, 50)
X, Y = np.meshgrid(x, x)
fig, ax = plt.subplots()
ax.contour(X, Y, X**2 + Y**2, levels=[1, 4])
plotly_fig = tls.mpl_to_plotly(fig)

assert len(plotly_fig.data) == 2
assert plotly_fig.data[0].x[0] == plotly_fig.data[0].x[-1]
assert plotly_fig.data[0].y[0] == plotly_fig.data[0].y[-1]
assert plotly_fig.data[1].x[0] == plotly_fig.data[1].x[-1]
assert plotly_fig.data[1].y[0] == plotly_fig.data[1].y[-1]


def test_line_collection_date_xaxis():
"""Line collections with date x-values must export date strings,
not raw matplotlib date numbers."""
dates = [
datetime.datetime(2023, 1, 1) + datetime.timedelta(days=i) for i in range(10)
]
y = np.linspace(0, 10, 10)
X, Y = np.meshgrid(mdates.date2num(dates), y)
fig, ax = plt.subplots()
ax.xaxis_date()
ax.contour(X, Y, np.sin(X) * np.cos(Y), 5)
plotly_fig = tls.mpl_to_plotly(fig)
lines = [t for t in plotly_fig.data if t.mode == "lines"]
assert len(lines) >= 1
assert any(isinstance(x, str) for t in lines for x in t.x)
assert all(x is None or isinstance(x, str) for t in lines for x in t.x)
Loading