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
59 changes: 59 additions & 0 deletions plotly/matplotlylib/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,24 @@
from plotly.matplotlylib import mpltools


def _export_color(color):
"""Export a matplotlib color for use as a plotly color.

matplotlib uses "none" for fully transparent colors, which plotly does not
accept, so transparent colors are exported as transparent black.
Colors already exported by the mplexporter (hex or rgba strings) are
passed through unchanged.
"""
if isinstance(color, str):
return "rgba(0,0,0,0)" if color == "none" else color
if isinstance(color, (list, tuple)) and all(
isinstance(c, str) for c in color
):
return [_export_color(c) for c in color]
bgcolor = export_color(color)
return "rgba(0,0,0,0)" if bgcolor == "none" else bgcolor


class PlotlyRenderer(Renderer):
"""A renderer class inheriting from base for rendering mpl plots in plotly.

Expand Down Expand Up @@ -513,6 +531,9 @@ 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)
else:
self.msg += " Path collection not linked to 'data', not drawing\n"
warnings.warn(
Expand All @@ -522,6 +543,44 @@ def draw_path_collection(self, **props):
"collections linked to 'data' coordinates"
)

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"])
edgecolors = mpltools.convert_rgba_array(props["styles"]["edgecolor"])
linewidths = mpltools.convert_linewidth_array(props["styles"]["linewidth"])
alpha = props["styles"]["alpha"]

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[min(i, n - 1)] if n else default

for i, (verts, codes) in enumerate(props["paths"]):
facecolor = per_path(facecolors, i, "rgba(0,0,0,0)")
edgecolor = per_path(edgecolors, i, "rgba(0,0,0,0)")
linewidth = per_path(linewidths, i, 0)
self.plotly_fig.add_trace(
go.Scatter(
x=[v[0] for v in verts],
y=[v[1] for v in verts],
mode="lines",
line=go.scatter.Line(
color=_export_color(edgecolor), width=linewidth
),
fill="toself",
fillcolor=_export_color(facecolor),
opacity=alpha,
xaxis="x{0}".format(self.axis_ct),
yaxis="y{0}".format(self.axis_ct),
)
)

def draw_path(self, **props):
"""Draw path, currently only attempts to draw bar charts.

Expand Down
71 changes: 71 additions & 0 deletions plotly/matplotlylib/tests/test_renderer.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import numpy as np
import matplotlib.pyplot as plt
import plotly.tools as tls

Expand Down Expand Up @@ -84,3 +85,73 @@ def test_multiple_traces_native_legend():
assert plotly_fig.data[0].mode == "lines"
assert plotly_fig.data[1].mode == "markers"
assert plotly_fig.data[2].mode == "lines+markers"


def test_violinplot_bodies_are_filled_polygons():
fig, ax = plt.subplots()
ax.violinplot(np.random.randn(100, 3))
plotly_fig = tls.mpl_to_plotly(fig)
bodies = [t for t in plotly_fig.data if t.fill == "toself" and len(t.x) > 100]
assert len(bodies) >= 3


def test_pcolor_rectangles_render():
x = np.linspace(-3, 3, 10)
X, Y = np.meshgrid(x, x)
fig, ax = plt.subplots()
ax.pcolor(X, Y, np.sin(X) * np.cos(Y))
plotly_fig = tls.mpl_to_plotly(fig)
assert len(plotly_fig.data) == 100
assert all(len(t.x) >= 4 for t in plotly_fig.data)


def test_eventplot_segments_render():
fig, ax = plt.subplots()
ax.eventplot([np.random.randn(20) for _ in range(5)])
plotly_fig = tls.mpl_to_plotly(fig)
assert len(plotly_fig.data) == 100


def test_stackplot_areas_render():
x = np.arange(10)
fig, ax = plt.subplots()
ax.stackplot(x, np.random.rand(10), np.random.rand(10), np.random.rand(10))
plotly_fig = tls.mpl_to_plotly(fig)
assert len(plotly_fig.data) >= 3


def test_fill_between_renders():
x = np.linspace(0, 2 * np.pi, 50)
fig, ax = plt.subplots()
ax.fill_between(x, np.sin(x), np.cos(x))
plotly_fig = tls.mpl_to_plotly(fig)
assert len(plotly_fig.data) >= 1


def test_stem_plot_renders():
x = np.linspace(0, 2 * np.pi, 20)
fig, ax = plt.subplots()
ax.stem(x, np.sin(x))
plotly_fig = tls.mpl_to_plotly(fig)
assert len(plotly_fig.data) >= 20


def test_contour_lines_convert():
"""Contour lines used to crash with an ndarray line width."""
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


def test_contourf_bands_render():
"""Contourf bands (multi-subpath collections) must render as fills."""
x = np.linspace(-3, 3, 30)
X, Y = np.meshgrid(x, x)
fig, ax = plt.subplots()
ax.contourf(X, Y, np.sin(X) * np.cos(Y), 10)
plotly_fig = tls.mpl_to_plotly(fig)
filled = [t for t in plotly_fig.data if t.fill == "toself"]
assert len(filled) > 0