Skip to content

Commit faab770

Browse files
Convert line collection dash styles for contour plots
1 parent 52fa598 commit faab770

3 files changed

Lines changed: 62 additions & 1 deletion

File tree

‎plotly/matplotlylib/mplexporter/exporter.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ def draw_collection(
282282
"linewidth": collection.get_linewidths(),
283283
"facecolor": collection.get_facecolors(),
284284
"edgecolor": collection.get_edgecolors(),
285+
"linestyle": collection.get_linestyle(),
285286
"alpha": collection._alpha,
286287
"zorder": collection.get_zorder(),
287288
}

‎plotly/matplotlylib/renderer.py‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,21 @@ def _per_path(values, i, default):
4545
return values[i % n] if n else default
4646

4747

48+
def _convert_collection_dash(linestyle):
49+
"""Convert a matplotlib collection line style to a plotly dash string.
50+
51+
Collections report line styles as (offset, dashes) tuples, with dashes
52+
in points and already scaled by line width (None for solid lines). Line
53+
widths are exported with their point values used as px, so the dashes
54+
are exported the same way, which keeps matplotlib's dash-to-width ratio.
55+
plotly has no dash offset, so the offset is dropped.
56+
"""
57+
dashes = linestyle[1] if linestyle is not None else None
58+
if not dashes:
59+
return "solid"
60+
return ",".join("{0:g}px".format(d) for d in dashes)
61+
62+
4863
class PlotlyRenderer(Renderer):
4964
"""A renderer class inheriting from base for rendering mpl plots in plotly.
5065
@@ -526,6 +541,7 @@ def draw_path_collection(self, **props):
526541
'linewidth', (one or more linewidths)
527542
'facecolor', (one or more facecolors for path)
528543
'edgecolor', (one or more edgecolors for path)
544+
'linestyle', (one or more (offset, dashes) line styles)
529545
'alpha', (one or more opacites for path)
530546
'zorder', (precedence when stacked)
531547
]
@@ -565,10 +581,12 @@ def _draw_line_collection(self, props):
565581
as plain lines."""
566582
edgecolors = mpltools.convert_rgba_array(props["styles"]["edgecolor"])
567583
linewidths = mpltools.convert_linewidth_array(props["styles"]["linewidth"])
584+
linestyles = props["styles"].get("linestyle")
568585

569586
for i, (verts, codes) in enumerate(props["paths"]):
570587
edgecolor = _per_path(edgecolors, i, "rgba(0,0,0,0)")
571588
linewidth = _per_path(linewidths, i, 0)
589+
dash = _convert_collection_dash(_per_path(linestyles, i, None))
572590
# a path may contain several disjoint lines (e.g. contour lines
573591
# of the same level); separate disjoint subpaths with None so
574592
# plotly does not connect them.
@@ -617,7 +635,9 @@ def _draw_line_collection(self, props):
617635
y=y_combined,
618636
mode="lines",
619637
line=go.scatter.Line(
620-
color=_export_color(edgecolor), width=linewidth
638+
color=_export_color(edgecolor),
639+
width=linewidth,
640+
dash=dash,
621641
),
622642
xaxis="x{0}".format(self.axis_ct),
623643
yaxis="y{0}".format(self.axis_ct),

‎plotly/matplotlylib/tests/test_renderer.py‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,3 +419,43 @@ def test_line_collection_date_xaxis():
419419
assert len(lines) >= 1
420420
assert any(isinstance(x, str) for t in lines for x in t.x)
421421
assert all(x is None or isinstance(x, str) for t in lines for x in t.x)
422+
423+
424+
def test_contour_line_dash_styles():
425+
"""Each contour level keeps its matplotlib dash pattern, exported as a
426+
px dash list (matplotlib's pattern is already scaled by line width)."""
427+
x = np.linspace(-3, 3, 30)
428+
X, Y = np.meshgrid(x, x)
429+
fig, ax = plt.subplots()
430+
ax.contour(
431+
X,
432+
Y,
433+
np.sin(X) * np.cos(Y),
434+
levels=[-0.5, -0.25, 0.25, 0.5],
435+
colors="k",
436+
linewidths=1.5,
437+
linestyles=["dashed", "solid", "dotted", (0, (5, 2, 1, 2))],
438+
)
439+
plotly_fig = tls.mpl_to_plotly(fig)
440+
441+
assert [t.line.dash for t in plotly_fig.data] == [
442+
"5.55px,2.4px",
443+
"solid",
444+
"1.5px,2.475px",
445+
"7.5px,3px,1.5px,3px",
446+
]
447+
448+
449+
def test_contour_line_dash_scales_with_linewidth():
450+
"""matplotlib scales dash patterns by line width; the export follows."""
451+
x = np.linspace(-3, 3, 30)
452+
X, Y = np.meshgrid(x, x)
453+
fig, ax = plt.subplots()
454+
ax.contour(
455+
X, Y, np.sin(X) * np.cos(Y), levels=[0.5], linewidths=3, linestyles="dashed"
456+
)
457+
plotly_fig = tls.mpl_to_plotly(fig)
458+
459+
assert len(plotly_fig.data) == 1
460+
assert plotly_fig.data[0].line.width == 3
461+
assert plotly_fig.data[0].line.dash == "11.1px,4.8px"

0 commit comments

Comments
 (0)