Fix Matplotlib fill, interpolation, errorbar, and stroke mismatches#211
Fix Matplotlib fill, interpolation, errorbar, and stroke mismatches#211sselvakumaran wants to merge 1 commit into
Conversation
Merging this PR will not alter performance
Comparing Footnotes
|
Greptile SummaryThis PR improves static 2D compatibility with Matplotlib. The main changes are:
Confidence Score: 4/5Image interpolation, masked Gouraud rendering, and joined-fill boundary recovery can produce incorrect output and need fixes before merging.
python/xy/pyplot/_axes.py, python/xy/pyplot/_plot_types.py, and python/xy/_paint.py
What T-Rex did
Important Files Changed
Reviews (1): Last reviewed commit: "Fix Matplotlib rendering mismatches" | Re-trigger Greptile |
| 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, |
There was a problem hiding this comment.
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.
- What the screen looked like at this point in the check.
- 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.
| 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)) |
There was a problem hiding this comment.
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.
- What the screen looked like at this point in the check.
| # 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]: |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
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.
- What the screen looked like at this point in the check.
Summary
axis("equal")is selected before adding data, and export filled polygons without triangulation seamsbarh(..., xerr=..., yerr=...)error components on their requested axesWhy these belong together
These are all in-scope static 2D Matplotlib rendering mismatches rather than new chart families:
axis("equal")beforefill(); 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.imshowinterpolation names mostly shared the same bilinear behavior, truecolor images were not interpolated, andinterpolation_stage="rgba"was rejected. The implementation now uses separable NumPy filters already available in the project, including masked-value renormalization.pcolormesh(..., shading="gouraud")was displayed as flat independent triangles; regular rectilinear meshes now use a bounded smooth scalar surface.xerrandyerrwhile forwarding their error bars.match_fillbehavior 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
mainwith the 88-example scoped static-2D gallery corpus:lines_bars_and_markers/fill.py, becomes practically working/similarfill.pyfigure 0: SSIM 0.2257 → 0.6189, RGB MAE 0.3352 → 0.1249, and divergent → similarfill.pyfigure 1: SSIM 0.5052 → 0.7257, RGB MAE 0.1002 → 0.0518pcolormesh_grids.py: SSIM 0.5999 → 0.8581, RGB MAE 0.1515 → 0.0508The 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.pymontage 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 passedmake check-pyplot: 618 passed, 3 skippedcargo test: 102 passedDeliberately 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.