Skip to content

Commit 513e432

Browse files
Draw data-coordinate path collections as filled polygons
1 parent c0740bf commit 513e432

2 files changed

Lines changed: 130 additions & 0 deletions

File tree

plotly/matplotlylib/renderer.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,24 @@
1414
from plotly.matplotlylib import mpltools
1515

1616

17+
def _export_color(color):
18+
"""Export a matplotlib color for use as a plotly color.
19+
20+
matplotlib uses "none" for fully transparent colors, which plotly does not
21+
accept, so transparent colors are exported as transparent black.
22+
Colors already exported by the mplexporter (hex or rgba strings) are
23+
passed through unchanged.
24+
"""
25+
if isinstance(color, str):
26+
return "rgba(0,0,0,0)" if color == "none" else color
27+
if isinstance(color, (list, tuple)) and all(
28+
isinstance(c, str) for c in color
29+
):
30+
return [_export_color(c) for c in color]
31+
bgcolor = export_color(color)
32+
return "rgba(0,0,0,0)" if bgcolor == "none" else bgcolor
33+
34+
1735
class PlotlyRenderer(Renderer):
1836
"""A renderer class inheriting from base for rendering mpl plots in plotly.
1937
@@ -513,6 +531,9 @@ def draw_path_collection(self, **props):
513531
}
514532
self.msg += " Drawing path collection as markers\n"
515533
self.draw_marked_line(**scatter_props)
534+
elif props["path_coordinates"] == "data":
535+
self.msg += " Drawing path collection as filled polygons\n"
536+
self._draw_filled_path_collection(props)
516537
else:
517538
self.msg += " Path collection not linked to 'data', not drawing\n"
518539
warnings.warn(
@@ -522,6 +543,44 @@ def draw_path_collection(self, **props):
522543
"collections linked to 'data' coordinates"
523544
)
524545

546+
def _draw_filled_path_collection(self, props):
547+
"""Draw a path collection (e.g. violin plot bodies) as filled polygons."""
548+
facecolors = mpltools.convert_rgba_array(props["styles"]["facecolor"])
549+
edgecolors = mpltools.convert_rgba_array(props["styles"]["edgecolor"])
550+
linewidths = mpltools.convert_linewidth_array(props["styles"]["linewidth"])
551+
alpha = props["styles"]["alpha"]
552+
553+
def per_path(colors, i, default):
554+
if isinstance(colors, str):
555+
return colors
556+
if colors is None:
557+
return default
558+
try:
559+
n = len(colors)
560+
except TypeError:
561+
return colors
562+
return colors[min(i, n - 1)] if n else default
563+
564+
for i, (verts, codes) in enumerate(props["paths"]):
565+
facecolor = per_path(facecolors, i, "rgba(0,0,0,0)")
566+
edgecolor = per_path(edgecolors, i, "rgba(0,0,0,0)")
567+
linewidth = per_path(linewidths, i, 0)
568+
self.plotly_fig.add_trace(
569+
go.Scatter(
570+
x=[v[0] for v in verts],
571+
y=[v[1] for v in verts],
572+
mode="lines",
573+
line=go.scatter.Line(
574+
color=_export_color(edgecolor), width=linewidth
575+
),
576+
fill="toself",
577+
fillcolor=_export_color(facecolor),
578+
opacity=alpha,
579+
xaxis="x{0}".format(self.axis_ct),
580+
yaxis="y{0}".format(self.axis_ct),
581+
)
582+
)
583+
525584
def draw_path(self, **props):
526585
"""Draw path, currently only attempts to draw bar charts.
527586

plotly/matplotlylib/tests/test_renderer.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import numpy as np
12
import matplotlib.pyplot as plt
23
import plotly.tools as tls
34

@@ -84,3 +85,73 @@ def test_multiple_traces_native_legend():
8485
assert plotly_fig.data[0].mode == "lines"
8586
assert plotly_fig.data[1].mode == "markers"
8687
assert plotly_fig.data[2].mode == "lines+markers"
88+
89+
90+
def test_violinplot_bodies_are_filled_polygons():
91+
fig, ax = plt.subplots()
92+
ax.violinplot(np.random.randn(100, 3))
93+
plotly_fig = tls.mpl_to_plotly(fig)
94+
bodies = [t for t in plotly_fig.data if t.fill == "toself" and len(t.x) > 100]
95+
assert len(bodies) >= 3
96+
97+
98+
def test_pcolor_rectangles_render():
99+
x = np.linspace(-3, 3, 10)
100+
X, Y = np.meshgrid(x, x)
101+
fig, ax = plt.subplots()
102+
ax.pcolor(X, Y, np.sin(X) * np.cos(Y))
103+
plotly_fig = tls.mpl_to_plotly(fig)
104+
assert len(plotly_fig.data) == 100
105+
assert all(len(t.x) >= 4 for t in plotly_fig.data)
106+
107+
108+
def test_eventplot_segments_render():
109+
fig, ax = plt.subplots()
110+
ax.eventplot([np.random.randn(20) for _ in range(5)])
111+
plotly_fig = tls.mpl_to_plotly(fig)
112+
assert len(plotly_fig.data) == 100
113+
114+
115+
def test_stackplot_areas_render():
116+
x = np.arange(10)
117+
fig, ax = plt.subplots()
118+
ax.stackplot(x, np.random.rand(10), np.random.rand(10), np.random.rand(10))
119+
plotly_fig = tls.mpl_to_plotly(fig)
120+
assert len(plotly_fig.data) >= 3
121+
122+
123+
def test_fill_between_renders():
124+
x = np.linspace(0, 2 * np.pi, 50)
125+
fig, ax = plt.subplots()
126+
ax.fill_between(x, np.sin(x), np.cos(x))
127+
plotly_fig = tls.mpl_to_plotly(fig)
128+
assert len(plotly_fig.data) >= 1
129+
130+
131+
def test_stem_plot_renders():
132+
x = np.linspace(0, 2 * np.pi, 20)
133+
fig, ax = plt.subplots()
134+
ax.stem(x, np.sin(x))
135+
plotly_fig = tls.mpl_to_plotly(fig)
136+
assert len(plotly_fig.data) >= 20
137+
138+
139+
def test_contour_lines_convert():
140+
"""Contour lines used to crash with an ndarray line width."""
141+
x = np.linspace(-3, 3, 30)
142+
X, Y = np.meshgrid(x, x)
143+
fig, ax = plt.subplots()
144+
ax.contour(X, Y, np.sin(X) * np.cos(Y), 10)
145+
plotly_fig = tls.mpl_to_plotly(fig)
146+
assert len(plotly_fig.data) > 0
147+
148+
149+
def test_contourf_bands_render():
150+
"""Contourf bands (multi-subpath collections) must render as fills."""
151+
x = np.linspace(-3, 3, 30)
152+
X, Y = np.meshgrid(x, x)
153+
fig, ax = plt.subplots()
154+
ax.contourf(X, Y, np.sin(X) * np.cos(Y), 10)
155+
plotly_fig = tls.mpl_to_plotly(fig)
156+
filled = [t for t in plotly_fig.data if t.fill == "toself"]
157+
assert len(filled) > 0

0 commit comments

Comments
 (0)