Skip to content

Fix conversion of matplotlib contour lines - #5770

Open
robertoffmoura wants to merge 5 commits into
plotly:mainfrom
robertoffmoura:rm/fix-contour-plot
Open

robertoffmoura wants to merge 5 commits into
plotly:mainfrom
robertoffmoura:rm/fix-contour-plot

Conversation

@robertoffmoura

Copy link
Copy Markdown
Contributor

mpl_to_plotly doesn't render contour lines correctly. Matplotlib packs each contour level into a single path containing several disjoint subpaths, so the converted figure connects them with segments that shouldn't exist, and closed rings are left open because the CLOSEPOLY vertex is dropped on export:

  • separate branches of the same level get joined by diagonals
  • every closed loop misses its final segment

Fix: path collections with no face colors (line collections, which is what ax.contour produces) are now drawn as line traces instead of filled polygons:

  • disjoint subpaths inside a path are separated with None, so plotly does not connect them
  • subpaths ending in a Z code have their first vertex appended to close the ring
  • multi-vertex path codes (C, S) consume the correct number of vertices while parsing
  • degenerate paths (fewer than 2 vertices, e.g. the empty path some contour sets carry) are skipped
  • x-values are converted to date strings on date axes, and per-path edge colors/linewidths are applied in order

Snippet to reproduce:

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import plotly.tools as tls

x = np.linspace(-2, 2, 120)
X, Y = np.meshgrid(x, x)
Z = np.sin(3 * np.sqrt(X**2 + Y**2))

fig, ax = plt.subplots()
ax.contour(X, Y, Z, levels=[-0.75, -0.25, 0.25, 0.75], linewidths=2)
fig.savefig("contour_mpl.png")

p = tls.mpl_to_plotly(fig)
p.write_image("contour_plotly.png")
matplotlib plotly before plotly after
contour_mpl contour_plotly_before contour_plotly_after

@camdecoster camdecoster self-assigned this Sep 23, 2026

@camdecoster camdecoster left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good, but does it handle the case when a plot has a legend enabled?
Never mind, wrong PR window.

@camdecoster camdecoster left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good and seems like a fine solution. I added a few comments/questions about potential improvements.

Could you please add a changelog entry?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please add a test for the None separators as described in the PR description?

Comment on lines +551 to +560
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper function looks like the same one from _draw_filled_path_collection. Could you move it up a level and reuse it in both locations?

Comment on lines +612 to +614
line=go.scatter.Line(
color=_export_color(edgecolor), width=linewidth
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this handle dash styles?

What about turning off legend display? I think that came up in another PR.

"collections linked to 'data' coordinates"
)

def _draw_line_collection(self, props):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function currently draws one trace per path. What do you think of updating it to group consecutive same-style lines together into one trace? That would cut down on the number of traces (and make a legend less noisy).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants