diff --git a/CHANGELOG.md b/CHANGELOG.md index ea837e3d0b..7525068fe8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +### Added +- Add support for converting matplotlib fill polygons (`plt.fill`) to filled scatter traces in `mpl_to_plotly` [[#5795](https://github.com/plotly/plotly.py/pull/5795)], with thanks to @robertoffmoura for the contribution! + ### Fixed - Fix concurrent first access to lazily initialized graph object properties, which could raise `ValueError("Invalid value")` [[#5691](https://github.com/plotly/plotly.py/pull/5691)], with thanks to @hb1915 for the contribution! - Fix `mpl_to_plotly` crashing on touching bars (such as `plt.hist`) due to floating-point noise producing negative `bargap` values by clamping `bargap` to `[0, 1]` [[#5696](https://github.com/plotly/plotly.py/pull/5696)], with thanks to @robertoffmoura for the contribution! diff --git a/plotly/matplotlylib/renderer.py b/plotly/matplotlylib/renderer.py index 65bbcfabb1..68ab05e543 100644 --- a/plotly/matplotlylib/renderer.py +++ b/plotly/matplotlylib/renderer.py @@ -9,6 +9,7 @@ import warnings +import matplotlib.patches as mpatches import plotly.graph_objs as go from plotly.matplotlylib.mplexporter import Renderer from plotly.matplotlylib import mpltools @@ -602,6 +603,12 @@ def draw_path(self, **props): is_bar = mpltools.is_bar(self.current_mpl_ax.containers, **props) if is_bar: self.current_bars += [props] + elif ( + isinstance(props["mplobj"], mpatches.Polygon) + and props["coordinates"] == "data" + ): + self.msg += " Drawing a filled polygon\n" + self._draw_filled_polygon(props) else: self.msg += " This path isn't a bar, not drawing\n" warnings.warn( @@ -609,6 +616,34 @@ def draw_path(self, **props): "of a bar chart. Ignoring." ) + def _draw_filled_polygon(self, props): + """Draw a matplotlib Polygon patch as a filled scatter trace.""" + style = props["style"] + verts = props["data"] + facecolor = _export_color(style["facecolor"]) + edgecolor = _export_color(style["edgecolor"]) + mplobj = props.get("mplobj") + label = mplobj.get_label() if mplobj is not None else None + if not label or (isinstance(label, str) and label.startswith("_")): + label = None + showlegend = False + else: + showlegend = None + self.plotly_fig.add_trace( + go.Scatter( + name=label, + showlegend=showlegend, + x=self._convert_x_dates([v[0] for v in verts]), + y=[v[1] for v in verts], + mode="lines", + line=go.scatter.Line(color=edgecolor, width=style["edgewidth"]), + fill="toself", + fillcolor=facecolor, + xaxis="x{0}".format(self.axis_ct), + yaxis="y{0}".format(self.axis_ct), + ) + ) + def draw_text(self, **props): """Create an annotation dict for a text obj. diff --git a/plotly/matplotlylib/tests/test_renderer.py b/plotly/matplotlylib/tests/test_renderer.py index 02dd6cf34a..65b2df4a8c 100644 --- a/plotly/matplotlylib/tests/test_renderer.py +++ b/plotly/matplotlylib/tests/test_renderer.py @@ -348,6 +348,48 @@ def test_uneven_custom_date_xtickvals_are_converted(): ) +def test_fill_converts(): + """plt.fill polygons convert to filled scatter traces.""" + x = np.linspace(0, 2 * np.pi, 50) + fig, ax = plt.subplots() + ax.fill(x, np.sin(x), "g") + + plotly_fig = tls.mpl_to_plotly(fig) + + assert len(plotly_fig.data) == 1 + trace = plotly_fig.data[0] + assert trace.type == "scatter" + assert trace.fill == "toself" + assert np.allclose(trace.x, x) + assert np.allclose(trace.y, np.sin(x)) + assert trace.fillcolor == "#007F00" + assert trace.line.color == "rgba(0,0,0,0)" + + +def test_fill_with_legend_label(): + """plt.fill polygons with labels export their name and show in legend.""" + fig, ax = plt.subplots() + ax.fill([0, 1, 2], [0, 2, 0], "b", label="Shaded Area") + ax.fill([0, 1, 2], [0, 1, 0], "g") + ax.plot([0, 1, 2], [1, 1, 1], "r-", label="Threshold") + ax.legend() + + plotly_fig = tls.mpl_to_plotly(fig) + + assert plotly_fig.layout.showlegend == True + assert len(plotly_fig.data) == 3 + + labeled_fill = next( + t for t in plotly_fig.data if t.fill == "toself" and t.name == "Shaded Area" + ) + assert labeled_fill.showlegend is not False + + unlabeled_fill = next( + t for t in plotly_fig.data if t.fill == "toself" and t.name is None + ) + assert unlabeled_fill.showlegend is False + + def test_custom_date_xtickvals_given_as_numbers_are_converted(): """Custom date ticks given as matplotlib date numbers must be converted to date strings."""