Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Fix `mpl_to_plotly` crashing on figures with fully transparent or alpha-carrying colors, such as boxplot outlier markers (`facecolor="none"`), rgba line colors, and `color="none"` text and axis labels; all matplotlib colors are now exported through a single helper that maps `"none"` to transparent rgba and keeps or overrides alpha as matplotlib does [[#5700](https://github.com/plotly/plotly.py/pull/5700)], with thanks to @robertoffmoura for the contribution!
- Export tick label colors to the plotly tickfont in `mpl_to_plotly` [[#5716](https://github.com/plotly/plotly.py/pull/5716)], with thanks to @robertoffmoura for the contribution!
- Fix `px.sunburst`, `px.treemap` and `px.icicle` listing sectors in a different order on every run when `path` is used with a Polars DataFrame; sectors now follow their order of first appearance for all dataframe backends [[#5766](https://github.com/plotly/plotly.py/pull/5766)], with thanks to @Irahan2 for the contribution!
- Fix `mpl_to_plotly` conversion of matplotlib contour lines and line collections: close contour rings ending with `Z` codes, support dash styles, convert date x-axes, separate disjoint subpaths with `None` separators, hide line collection traces from the legend by default, and group consecutive same-style lines into single traces [[#5770](https://github.com/plotly/plotly.py/pull/5770)], with thanks to @robertoffmoura for the contribution!

## [7.1.0] - 2026-09-15

Expand Down
1 change: 1 addition & 0 deletions plotly/matplotlylib/mplexporter/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ def draw_collection(
"linewidth": collection.get_linewidths(),
"facecolor": collection.get_facecolors(),
"edgecolor": collection.get_edgecolors(),
"linestyle": collection.get_linestyle(),
"alpha": collection._alpha,
"zorder": collection.get_zorder(),
}
Expand Down
153 changes: 137 additions & 16 deletions plotly/matplotlylib/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,39 @@
from plotly.matplotlylib.mpltools import _export_color


def _per_path(values, i, default):
"""Return the style value for the i-th path of a path collection.

A scalar (or string) value applies to every path, a sequence is cycled
through like matplotlib does, and None or an empty sequence yields
`default`.
"""
if isinstance(values, str):
return values
if values is None:
return default
try:
n = len(values)
except TypeError:
return values
return values[i % n] if n else default


def _convert_collection_dash(linestyle):
"""Convert a matplotlib collection line style to a plotly dash string.

Collections report line styles as (offset, dashes) tuples, with dashes
in points and already scaled by line width (None for solid lines). Line
widths are exported with their point values used as px, so the dashes
are exported the same way, which keeps matplotlib's dash-to-width ratio.
plotly has no dash offset, so the offset is dropped.
"""
dashes = linestyle[1] if linestyle is not None else None
if not dashes:
return "solid"
return ",".join("{0:g}px".format(d) for d in dashes)


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

Expand Down Expand Up @@ -562,6 +595,7 @@ def draw_path_collection(self, **props):
'linewidth', (one or more linewidths)
'facecolor', (one or more facecolors for path)
'edgecolor', (one or more edgecolors for path)
'linestyle', (one or more (offset, dashes) line styles)
'alpha', (one or more opacites for path)
'zorder', (precedence when stacked)
]
Expand All @@ -580,8 +614,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 @@ -591,27 +630,109 @@ def draw_path_collection(self, **props):
"collections linked to 'data' coordinates"
)

def _draw_line_collection(self, props):
Comment thread
robertoffmoura marked this conversation as resolved.
"""Draw a path collection without face colors (e.g. contour lines)
as plain lines, grouping consecutive same-style paths into single traces."""
edgecolors = mpltools.convert_rgba_array(props["styles"]["edgecolor"])
linewidths = mpltools.convert_linewidth_array(props["styles"]["linewidth"])
linestyles = props["styles"].get("linestyle")

current_style = None
grouped_x = []
grouped_y = []

def flush():
if current_style is not None and grouped_x:
ec, lw, d = current_style
self.plotly_fig.add_trace(
go.Scatter(
x=grouped_x,
y=grouped_y,
mode="lines",
showlegend=False,
line=go.scatter.Line(
color=_export_color(ec),
width=lw,
dash=d,
),
xaxis="x{0}".format(self.axis_ct),
yaxis="y{0}".format(self.axis_ct),
)
)

for i, (verts, codes) in enumerate(props["paths"]):
# 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))
path_x = []
path_y = []
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 path_x:
path_x.append(None)
path_y.append(None)
path_x.extend(sub_x)
path_y.extend(sub_y)
if not path_x:
continue

edgecolor = _per_path(edgecolors, i, "rgba(0,0,0,0)")
linewidth = _per_path(linewidths, i, 0)
dash = _convert_collection_dash(_per_path(linestyles, i, None))
style = (edgecolor, linewidth, dash)

if style != current_style:
flush()
current_style = style
grouped_x = list(path_x)
grouped_y = list(path_y)
else:
grouped_x.append(None)
grouped_y.append(None)
grouped_x.extend(path_x)
grouped_y.extend(path_y)

flush()

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"])

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

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)
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=self._convert_x_dates([v[0] for v in verts]),
Expand Down
Loading
Loading