Skip to content

Fix Matplotlib fill, interpolation, errorbar, and stroke mismatches#211

Draft
sselvakumaran wants to merge 1 commit into
mainfrom
agent/fix-matplotlib-rendering-mismatches
Draft

Fix Matplotlib fill, interpolation, errorbar, and stroke mismatches#211
sselvakumaran wants to merge 1 commit into
mainfrom
agent/fix-matplotlib-rendering-mismatches

Conversation

@sselvakumaran

Copy link
Copy Markdown
Contributor

Summary

  • preserve autoscaling when axis("equal") is selected before adding data, and export filled polygons without triangulation seams
  • add dependency-free data-stage/RGBA-stage image interpolation and smooth regular-grid Gouraud shading
  • keep barh(..., xerr=..., yerr=...) error components on their requested axes
  • make per-item bar and triangle-mesh outlines match their corresponding face colors in WebGL, PNG, and SVG

Why these belong together

These are all in-scope static 2D Matplotlib rendering mismatches rather than new chart families:

  • Matplotlib's fill gallery calls axis("equal") before fill(); xy was freezing the empty (0, 1) domains, so the polygon was clipped/misframed. Static exporters also painted each tessellation triangle separately, exposing antialias seams and applying translucent alpha repeatedly.
  • accepted imshow interpolation names mostly shared the same bilinear behavior, truecolor images were not interpolated, and interpolation_stage="rgba" was rejected. The implementation now uses separable NumPy filters already available in the project, including masked-value renormalization.
  • regular pcolormesh(..., shading="gouraud") was displayed as flat independent triangles; regular rectilinear meshes now use a bounded smooth scalar surface.
  • horizontal bars swapped xerr and yerr while forwarding their error bars.
  • buffer-free match_fill behavior existed for scatter, but direct per-item bar/mesh faces could still receive a fallback outline color, with inconsistent results between WebGL and static exporters.

No runtime dependency was added. Matplotlib is used only as the external reference for the comparison run; it is not imported by xy for these features.

Gallery impact

Measured against current main with the 88-example scoped static-2D gallery corpus:

  • execution coverage is unchanged: 52/88 examples render, producing 73 paired figures
  • automatic image classification improves from 29 to 30 similar and 44 to 43 divergent
  • one additional gallery program, lines_bars_and_markers/fill.py, becomes practically working/similar
  • fill.py figure 0: SSIM 0.2257 → 0.6189, RGB MAE 0.3352 → 0.1249, and divergent → similar
  • fill.py figure 1: SSIM 0.5052 → 0.7257, RGB MAE 0.1002 → 0.0518
  • regular Gouraud panel in pcolormesh_grids.py: SSIM 0.5999 → 0.8581, RGB MAE 0.1515 → 0.0508

The selected gallery corpus does not contain the horizontal-bar dual-error case or direct per-item match-fill cases, so those fixes improve correctness without changing its execution count. The broad interpolation_methods.py montage does not change automatic class; this PR adds the missing stage/truecolor/filter behavior without claiming pixel-identical Matplotlib filter kernels.

Validation

  • pytest -q tests/pyplot/test_matplotlib_mismatch_regressions.py: 7 passed
  • make check-pyplot: 618 passed, 3 skipped
  • cargo test: 102 passed
  • Ruff lint and formatting checks pass
  • committed widget and standalone JavaScript bundles are fresh
  • scoped 88-example gallery comparison rerun against current main

Deliberately separate

Arbitrary per-row scatter labels/metadata are still missing and need their own data/interaction design. Text layout and MathText/TeX rendering are also intentionally excluded: xy currently performs a small dependency-free TeX-like Unicode/plain-text conversion, not a LaTeX render. A text-focused change should decide separately whether to extend that dependency-free path or accept a renderer dependency.

@codspeed-hq

codspeed-hq Bot commented Jul 22, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 97 untouched benchmarks
⏩ 1 skipped benchmark1


Comparing agent/fix-matplotlib-rendering-mismatches (462d92d) with main (419f630)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR improves static 2D compatibility with Matplotlib. The main changes are:

  • Preserve autoscaling when equal aspect is selected before adding data.
  • Add image interpolation stages and dependency-free filters.
  • Smooth regular-grid Gouraud meshes.
  • Keep horizontal-bar errors on their requested axes.
  • Match per-item outlines to face colors across renderers.
  • Export joined polygon fills without internal triangle seams.

Confidence Score: 4/5

Image interpolation, masked Gouraud rendering, and joined-fill boundary recovery can produce incorrect output and need fixes before merging.

  • Automatic interpolation-stage selection is not implemented.
  • Transparent RGBA samples can bleed hidden colors into visible pixels.
  • Masked Gouraud samples can expand into broad missing bands.
  • Large coordinate spans can collapse distinct polygon vertices.

python/xy/pyplot/_axes.py, python/xy/pyplot/_plot_types.py, and python/xy/_paint.py

T-Rex T-Rex Logs

What T-Rex did

  • Reproduced and compared auto-stage versus data-stage results for a 2×2 scalar image, finding the auto-stage output (512×512 scalar grid) exactly matches the explicit data-stage result, while the RGBA stage yields a 512×512×4 grid.
  • Executed the pyplot imshow and PNG export path with a masked scalar grid using rgba interpolation, and observed RGBA values contaminating the RGB endpoints with a maximum difference of 0.451863.
  • Generated a six-vertex polygon and ran the triangulation and SVG export flow, which showed a 20-unit tolerance and a key collision that produced a null boundary, with the SVG export returning four independent triangles instead of one joined polygon.
  • Focused pytest path for pcolormesh with Gouraud shading and a single masked vertex, revealing 16,384 invalid values and a 128×128 missing region, while the Chromium-based PNG exporter produced a valid image.
  • Collected static export evidence showing seven passing cases and multi-renderer outputs across native and Chromium exporters, including per-item bars and triangle-mesh visuals and SVG-based face attributes.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
python/xy/pyplot/_axes.py Adds equal-axis autoscaling, errorbar routing, and image resampling; automatic-stage and transparent-edge interpolation remain incorrect.
python/xy/pyplot/_plot_types.py Adds joined fills and regular Gouraud heatmaps, but masked Gouraud values can spread across the output.
python/xy/_paint.py Adds tessellated-polygon boundary recovery with a global tolerance that can merge distinct vertices.
python/xy/_raster.py Adds joined-fill raster output and face-matched strokes; joined-fill output depends on boundary recovery.
python/xy/_svg.py Adds joined SVG polygons and face-matched strokes; joined-fill output depends on boundary recovery.
python/xy/marks.py Adds joined-fill state and match-fill channels for per-item bar and mesh colors.
js/src/40_gl.ts Adds a shader mode that sources outline color from resolved face paint.
js/src/50_chartview.ts Selects the new face-matched outline mode for rectangle and mesh traces.

Reviews (1): Last reviewed commit: "Fix Matplotlib rendering mismatches" | Re-trigger Greptile

Comment thread python/xy/pyplot/_axes.py
Comment on lines 2049 to +2054
not truecolor
and interpolation not in (None, "none", "nearest")
and min(grid.shape) >= 2
and interpolation_stage == "rgba"
and effective_interpolation not in ("none", "nearest")
):
grid = _scalar_grid_rgba(
grid,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Auto Stage Always Uses Data

interpolation_stage="auto" is accepted here, but only the literal "rgba" selects RGBA-stage conversion. Scalar images using "auto" therefore always interpolate values before applying the colormap, which produces incorrect colors whenever automatic selection should use RGBA interpolation with a nonlinear colormap.

Artifacts

Repro: focused scalar-image auto-stage regression test

  • Evidence file captured while the check ran.

Repro: verbose failing pytest output with stage shapes and center samples

  • The full command output behind this check.

Transparent Pixel Bleed

  • What the screen looked like at this point in the check.

Repro Masked Gouraud Spread

  • What the screen looked like at this point in the check.

Global Tolerance Thin Feature Observed

  • What the screen looked like at this point in the check.

View artifacts

T-Rex Ran code and verified through T-Rex

Comment thread python/xy/pyplot/_axes.py
Comment on lines +5359 to +5363
rgb = _lut(resolve_cmap(cmap), np.nan_to_num(normalized, nan=0.0).reshape(-1)).reshape(
values.shape + (3,)
)
alpha = np.where(np.isfinite(values), 1.0, 0.0)
return np.dstack((rgb / 255.0, alpha))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Transparent Pixels Bleed Hidden Color

Masked samples receive the colormap's zero-value RGB and zero alpha, then _resample_grid interpolates RGB and alpha independently. With interpolation_stage="rgba", that hidden RGB contributes to neighboring visible pixels and creates colored fringes around masked regions instead of transparent-edge interpolation.

Artifacts

Repro: executable pyplot imshow and PNG export harness

  • Evidence file captured while the check ran.

Error trace from the failing run

  • The full error output from the failing run.

Transparent Pixel Bleed

  • What the screen looked like at this point in the check.

View artifacts

T-Rex Ran code and verified through T-Rex

Comment thread python/xy/_paint.py
Comment on lines +31 to +35
# x0/x1/x2. The joined-fill flag is only used for one simple polygon; a
# generous relative bucket is still far below meaningful edge spacing.
tolerance = max(span * 2e-5, 1e-12)

def vertex_key(point: tuple[float, float]) -> tuple[int, int]:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Global Tolerance Collapses Thin Features

Vertex identity uses 2e-5 of the polygon's full coordinate span. A large-span polygon with nearby but distinct vertices can therefore map those vertices to one key—for example, a span of 1e6 permits buckets around 20 units wide—causing the recovered boundary to drop edges, connect the wrong points, or fall back to visible triangle seams.

Artifacts

Repro: executable mesh and SVG-export harness

  • Evidence file captured while the check ran.

Repro: captured key collision, null boundary, and SVG fallback output

  • The full command output behind this check.

Global Tolerance Thin Feature Observed

  • What the screen looked like at this point in the check.

View artifacts

T-Rex Ran code and verified through T-Rex

if gouraud_axes is not None and no_edges:
width = max(2, min(512, max(256, z.shape[1] * 32)))
height = max(2, min(512, max(256, z.shape[0] * 32)))
smooth = _bilinear_grid(z, width, height)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Masked Gouraud Values Spread Outward

Regular Gouraud meshes pass masked or NaN values through _bilinear_grid, whose two np.interp passes do not renormalize around invalid samples. One masked vertex can become a horizontal NaN interval and then a broad vertical band, so a local mask produces a much larger missing region in the generated heatmap.

Artifacts

Repro: focused regular-Gouraud pytest and PNG export harness

  • Evidence file captured while the check ran.

Repro: verbose pytest output showing the expanded invalid-mask dimensions and successful render

  • The full command output behind this check.

Repro Masked Gouraud Spread

  • What the screen looked like at this point in the check.

View artifacts

T-Rex Ran code and verified through T-Rex

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.

1 participant