Skip to content

Consolidate Matplotlib gallery rendering repairs [mpl compatibility]#280

Draft
sselvakumaran wants to merge 12 commits into
mainfrom
agent/gallery-compat-stack-a
Draft

Consolidate Matplotlib gallery rendering repairs [mpl compatibility]#280
sselvakumaran wants to merge 12 commits into
mainfrom
agent/gallery-compat-stack-a

Conversation

@sselvakumaran

@sselvakumaran sselvakumaran commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Summary

This is the base of a two-PR shipping stack for the existing Matplotlib compatibility work. It replaces the review surface of the many small gallery drafts with one integrated snapshot on current main; #281 adds the final acceptance repairs on top.

The stack combines:

  • autoscale, sticky extents, bar labels, annotations, and axis margins
  • measured tick/title/axis-label gutters and frame geometry
  • explicit and loc="best" legend layout
  • line, stem, histogram, errorbar, contour, colorbar, colormap, text, and tick APIs
  • browser, SVG, raster, and static-export geometry

If this stack is accepted, it supersedes #206, #211, #240#248, #252#256, and #268#274. Several older implementations were intentionally replaced by corrected integrated versions; they should not be replayed mechanically. The component drafts remain open for provenance for now. This PR does not merge or close them.

Exact visual evidence

The commit labels and SHA-256 hashes are recorded in pr-assets/matplotlib-gallery-consolidation/provenance.json.

Bar-label bounds

On main, labels from the penguin stacked bars spill outside the axes and the third category is missing. Stack A keeps labels centered within their segments and restores the complete chart.

Matplotlib, main, and Stack A bar-label comparison

Annotation geometry

Stack A brings the arrow position, stroke, and text placement back toward the Matplotlib reference without clipping the frame.

Matplotlib, main, and Stack A annotation comparison

Full survey legend labels

The integrated audit found that the old #256 result still ellipsized all five legend labels. The current stack preserves the complete labels while retaining the measured left gutter and bounded export.

Matplotlib, main, and current Stack A horizontal-bar comparison

Verification

  • Exact Matplotlib 3.11 gallery execution at render-code commit 10b22b9: 77/88 paired examples, up from 52/88 on main, with 133 paired figures.
  • Automatic image grouping reports 75 near, 26 similar, and 32 divergent paired figures. These are triage buckets, not blanket pixel-equivalence claims; readability and technical correctness remain the acceptance criteria.
  • The final code adjustment restores Matplotlib's sparse-scatter legend(loc="best") choice while retaining the 512-vertex budget for connected paths; its exact regression and focused legend suite pass 33/33.
  • Exact-head CI: 2,521 Python tests passed with 69 skipped and 122 Rust tests passed. Clippy with warnings denied, TypeScript, JS bundle build, Ruff, formatting, provenance JSON, and git diff --check also pass.
  • All current GitHub checks pass, including Matplotlib 3.11, docs, browser/accessibility, Python 3.11 floor, CodeQL, cross-library and methodology benchmarks, source distribution, aggregate Rust/Python/JS, CodSpeed, and all three wheel jobs.
  • The current PR head differs from 10b22b9 only by the evidence/provenance commit; it contains no later render-code change.

Performance

The exact-head CodSpeed comparison at c196c12 is green:

  • 100 benchmarks unchanged
  • 2 improved
  • 2 skipped
  • 0 regressions

The two measured improvements are pyplot PNG export (2.3×) and first-payload large error bars (18.71%). The sparse-scatter repair adds at most 3,584 vectorized offset checks only when loc="best" scores a scatter and does not register a CodSpeed regression.

Known deferred work

The stack deliberately freezes scope rather than claiming complete Matplotlib parity. #281 documents the seven scoped gallery examples left for later.

Four older #211 review findings are also still deferred: automatic RGBA-stage selection, hidden-RGB bleed through masked RGBA interpolation, thin-feature collisions in triangle-boundary bucketing, and mask/NaN spreading in regular Gouraud interpolation. A log-singleton public/render-domain mismatch from #240 remains deferred as well. They are not regressions introduced by consolidation, but they are real remaining compatibility work.

Stack

  1. Consolidate Matplotlib gallery rendering repairs [mpl compatibility] #280 — this consolidation base
  2. Finish Matplotlib gallery acceptance repairs [mpl compatibility] #281 — final acceptance delta, targeting this branch

Summary by CodeRabbit

  • New Features

    • Added configurable axis margins, anchored legends, colorbar shrinking, anchors, and minor ticks.
    • Expanded Matplotlib compatibility for contours, bars, histograms, streamplots, barbs, quivers, and layout options.
    • Added support for additional colormaps, hatching, styled text, math-text italics, and improved marker rendering.
    • Improved autoscaling, tick placement, legend layout, annotation boxes, and subplot positioning.
  • Bug Fixes

    • Improved SVG and PNG parity, color/alpha handling, font defaults, text clipping, colorbar labels, and mesh autoscaling.
    • Added corner masking for contours and more accurate raster composition.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 21099ae0-5dbe-44a9-a741-d1d10fcbc961

📥 Commits

Reviewing files that changed from the base of the PR and between 85e3686 and c196c12.

⛔ Files ignored due to path filters (3)
  • pr-assets/matplotlib-gallery-consolidation/annotation-basic.png is excluded by !**/*.png
  • pr-assets/matplotlib-gallery-consolidation/bar-label-demo.png is excluded by !**/*.png
  • pr-assets/matplotlib-gallery-consolidation/horizontal-barchart-legend.png is excluded by !**/*.png
📒 Files selected for processing (41)
  • js/src/50_chartview.ts
  • pr-assets/matplotlib-gallery-consolidation/provenance.json
  • python/xy/_figure.py
  • python/xy/_paint.py
  • python/xy/_svg.py
  • python/xy/components.py
  • python/xy/marks.py
  • python/xy/pyplot/__init__.py
  • python/xy/pyplot/_artists.py
  • python/xy/pyplot/_axes.py
  • python/xy/pyplot/_grid.py
  • python/xy/pyplot/_mathtext.py
  • python/xy/pyplot/_plot_types.py
  • python/xy/pyplot/_ticker.py
  • python/xy/styles.py
  • spec/api/styling.md
  • spec/matplotlib/compat.md
  • spec/matplotlib/shim-todo.md
  • src/kernels.rs
  • src/raster.rs
  • tests/pyplot/test_adversarial_parity_fixes.py
  • tests/pyplot/test_axes_layout.py
  • tests/pyplot/test_best_legend_placement.py
  • tests/pyplot/test_color_pipeline_fixes.py
  • tests/pyplot/test_gallery_auto_ticks_compat.py
  • tests/pyplot/test_gallery_hist_errorbar_compat.py
  • tests/pyplot/test_gallery_text_pie_compat.py
  • tests/pyplot/test_layout_text_parity_fixes.py
  • tests/pyplot/test_line_legend_gallery_compat.py
  • tests/pyplot/test_matplotlib_mismatch_regressions.py
  • tests/pyplot/test_p3_option_contracts.py
  • tests/pyplot/test_pdsh_gap_features.py
  • tests/svg_test_utils.py
  • tests/test_components.py
  • tests/test_figure.py
  • tests/test_plot_families.py
  • tests/test_svg_export.py
  • tests/test_svg_text_metrics.py
  • tests/test_text_weight_defaults.py
  • tests/test_ui_issue_regressions.py
  • tests/test_width_validation_regressions.py
🚧 Files skipped from review as they are similar to previous changes (25)
  • pr-assets/matplotlib-gallery-consolidation/provenance.json
  • tests/pyplot/test_color_pipeline_fixes.py
  • tests/test_ui_issue_regressions.py
  • python/xy/_paint.py
  • tests/pyplot/test_layout_text_parity_fixes.py
  • tests/pyplot/test_pdsh_gap_features.py
  • python/xy/_figure.py
  • python/xy/pyplot/_grid.py
  • python/xy/styles.py
  • tests/pyplot/test_gallery_hist_errorbar_compat.py
  • src/kernels.rs
  • tests/test_svg_export.py
  • spec/api/styling.md
  • python/xy/components.py
  • tests/test_text_weight_defaults.py
  • tests/pyplot/test_best_legend_placement.py
  • src/raster.rs
  • python/xy/pyplot/_artists.py
  • spec/matplotlib/compat.md
  • tests/pyplot/test_p3_option_contracts.py
  • python/xy/marks.py
  • python/xy/pyplot/_plot_types.py
  • js/src/50_chartview.ts
  • python/xy/_svg.py
  • python/xy/pyplot/_axes.py

📝 Walkthrough

Walkthrough

This PR expands Matplotlib compatibility across axis margins, autoscaling, legends, colorbars, contouring, vector fields, text, layout, SVG/PNG/WebGL rendering, native kernels, colormaps, and regression coverage.

Changes

Compatibility and rendering

Layer / File(s) Summary
API contracts and autoscaling
python/xy/components.py, python/xy/_figure.py, python/xy/marks.py, python/xy/pyplot/_axes.py, python/xy/pyplot/_ticker.py
Adds axis margins, sticky-edge autoscaling, round-number limits, contour masking, per-item bar widths, expanded colormap support, and related API wiring.
Renderer and native behavior
python/xy/_svg.py, python/xy/_raster.py, js/src/*, src/*
Updates text styling, marker geometry, legends, colorbars, styled native text, alpha compositing, triangle fills, and marching-squares corner masking.
Pyplot plotting semantics
python/xy/pyplot/_axes.py, python/xy/pyplot/_plot_types.py, python/xy/pyplot/_artists.py
Expands support for legends, histograms, contours, images, pie charts, error bars, streamplots, barbs, quiver keys, stems, and annotations.
Layout and export composition
python/xy/pyplot/_mplfig.py, python/xy/pyplot/_grid.py
Reworks subplot geometry, tight/constrained layout, notebook gutters, PNG sizing, and RGBA panel composition.
Compatibility contracts and regression coverage
spec/*, tests/*
Documents and tests the updated API, renderer parity, geometry, text, legend, colorbar, autoscale, and plotting behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested reviewers: farhanaliraza

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the PR’s broad focus on consolidating Matplotlib gallery compatibility and rendering fixes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/gallery-compat-stack-a

Comment @coderabbitai help to get the list of available commands.

@codspeed-hq

codspeed-hq Bot commented Jul 25, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 64.01%

⚡ 2 improved benchmarks
✅ 100 untouched benchmarks
⏩ 2 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
test_png_export_line_pyplot 73.9 ms 32.6 ms ×2.3
test_first_payload_errorbar_large 613.1 ms 516.5 ms +18.7%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing agent/gallery-compat-stack-a (c196c12) with main (077c53f)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@sselvakumaran
sselvakumaran force-pushed the agent/gallery-compat-stack-a branch from 1df83ef to ba93136 Compare July 25, 2026 03:08
@sselvakumaran

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 19

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
python/xy/components.py (1)

237-248: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

anchor inserted mid-order breaks Legend's documented positional contract.

Lines 245-246 state new fields must append after render because positional construction over the released field order must keep binding. Inserting anchor at position 3 silently rebinds existing Legend(True, "upper right", 2, "Title") calls (2 now lands on anchor and trips the new validation or, worse, ncols falls to the default).

🔧 Move the new field to the documented append point
 class Legend(Component):
     """Legend chrome; ``render`` remains opaque for Reflex adapters."""
 
     show: bool = True
     loc: Optional[str] = None
-    anchor: Optional[tuple[float, ...]] = None
     ncols: int = 1
     title: Optional[str] = None
     class_name: Optional[str] = None
     style: dict[str, StyleValue] = field(default_factory=dict)
     render: Any = None
     # New fields append after ``render``: Legend is public and positional
     # construction over the released field order must keep binding.
     highlight: bool = True
     toggle: bool = True
+    anchor: Optional[tuple[float, ...]] = None

Note the same ordering question applies to Axis.margin at Line 218, which is inserted between domain and bounds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/xy/components.py` around lines 237 - 248, Preserve released positional
field ordering in the dataclass definitions: move the new Legend.anchor field to
append after render, and likewise move Axis.margin after all existing released
fields. Keep existing fields such as ncols, title, domain, and bounds in their
original positions so positional construction continues binding unchanged.
spec/api/styling.md (1)

1-1: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Mismatched section name between the two files. spec/api/styling.md introduces the heading "Measured left gutter and the rotated y-axis title", but spec/matplotlib/compat.md points to it by a different name, "Chrome gutters and the rotated y-axis title". Pick one name and use it in both places.

  • spec/api/styling.md#L206-206: keep this heading as the canonical name (or rename it to match compat.md's phrasing, whichever is intended going forward).
  • spec/matplotlib/compat.md#L67-70: update the cross-reference text to say "Measured left gutter and the rotated y-axis title" so it actually resolves to the section it points to.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/api/styling.md` at line 1, Align the cross-reference in the matplotlib
compatibility documentation with the canonical heading in the styling
specification. Keep “Measured left gutter and the rotated y-axis title” as the
section name in spec/api/styling.md and update the corresponding reference text
in spec/matplotlib/compat.md to use that exact wording.

Source: Coding guidelines

🧹 Nitpick comments (15)
python/xy/marks.py (2)

2316-2327: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

np.isscalar misses 0-d arrays and NumPy-scalar-like inputs.

np.isscalar(np.array(1.1)) is False, so a 0-d width array falls into _as_1d_float and fails with "contour width must be 1-D". np.ndim(width) == 0 covers both cases.

♻️ Proposed tweak
-    if np.isscalar(width):
+    if np.ndim(width) == 0:
         width = self._positive_scalar(width, "contour width")
         width_values = None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/xy/marks.py` around lines 2316 - 2327, Update the width branching
logic around the scalar validation to use np.ndim(width) == 0 instead of
np.isscalar(width), ensuring Python scalars, NumPy scalars, and 0-D arrays use
_positive_scalar while preserving the existing 1-D array validation path.

645-661: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Non-numeric width escapes with a raw NumPy message.

np.asarray(width, dtype=np.float64) on e.g. width="wide" raises NumPy's own ValueError with no {kind} width label, unlike every other validator in this function.

♻️ Label the conversion failure
-    width_array = np.asarray(width, dtype=np.float64)
+    try:
+        width_array = np.asarray(width, dtype=np.float64)
+    except (TypeError, ValueError) as exc:
+        raise ValueError(f"{kind} width must be scalar or contain numeric values") from exc
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/xy/marks.py` around lines 645 - 661, Update the width conversion logic
in the validation block to catch failures from np.asarray(width,
dtype=np.float64) and re-raise a ValueError labeled with “{kind} width” and the
existing scalar-or-numeric-values message, suppressing the raw NumPy exception.
python/xy/_figure.py (1)

1047-1069: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse of margin for two meanings makes the log branch easy to break.

margin is overwritten with the 0.03 default at Line 1060, so Lines 1061 and 1075 must re-read opts.get("margin") to recover "was it configured?". A separate name makes the intent explicit and prevents a future edit from collapsing the two checks.

♻️ Suggested clarification
-        margin = opts.get("margin")
-        if lo == hi and margin is None:
+        configured_margin = opts.get("margin")
+        if lo == hi and configured_margin is None:
@@
-        if margin is None:
-            margin = 0.03
-        if scale == "log" and opts.get("margin") is not None:
+        margin = 0.03 if configured_margin is None else configured_margin
+        if scale == "log" and configured_margin is not None:
@@
-        if scale == "log" and opts.get("margin") is None:
+        if scale == "log" and configured_margin is None:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/xy/_figure.py` around lines 1047 - 1069, In the extent calculation
around the margin handling, preserve whether the caller explicitly configured a
margin in a separate variable before assigning the default value. Update the
logarithmic branch and any corresponding configured-margin checks to use that
explicitness variable, while continuing to use the resolved numeric margin for
padding calculations.
js/src/50_chartview.ts (1)

5130-5139: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

fontRoom here is absolute px; the Python twin takes a font-size multiple.

_svg._axis_tick_label_offset(axis, unstyled, font_room) returns pad + font_size * font_room (called with 0.2/0.8), while this returns pad + fontRoom (called with size * 1.2). The values are correct for their respective anchors (DOM top edge vs baseline), but the comment claims the helpers mirror each other, so a future edit is likely to copy one signature into the other. Naming the argument (e.g. fontRoomPx) would pin the difference down.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/src/50_chartview.ts` around lines 5130 - 5139, Rename the absolute-pixel
fontRoom parameter in tickLabelOffset to fontRoomPx, preserving the existing pad
+ fontRoomPx calculation and call behavior. Keep this JavaScript helper’s
pixel-based contract distinct from the Python font-size-multiple API.
python/xy/styles.py (1)

160-172: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

_signed_px is _px minus the sign check — consider one parser.

The parse/error-message path is duplicated verbatim; a signed=True flag on _px keeps the two in step if the px grammar ever changes.

♻️ Fold into `_px`
-def _px(value: StyleValue, label: str, *, positive: bool = False) -> float:
+def _px(value: StyleValue, label: str, *, positive: bool = False, signed: bool = False) -> float:
     if isinstance(value, (int, float)) and not isinstance(value, bool):
         number = float(value)
     elif isinstance(value, str):
         match = _PX_RE.match(value)
         if match is None:
             raise ValueError(f"{label} must be a finite CSS px length")
         number = float(match.group(1))
     else:  # pragma: no cover - normalize_css_style rejects this first
         raise ValueError(f"{label} must be a finite CSS px length")
+    if signed:
+        if not np.isfinite(number):
+            raise ValueError(f"{label} must be a finite CSS px length")
+        return number
     if not np.isfinite(number) or number < 0 or (positive and number <= 0):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/xy/styles.py` around lines 160 - 172, Refactor _signed_px to reuse the
existing _px parser by adding a signed=True option to _px and routing signed
values through it. Preserve _signed_px’s acceptance of negative lengths while
keeping the shared px grammar, finite-number validation, and error-message
behavior in one implementation.
python/xy/_svg.py (3)

2410-2436: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Measure the bbox with _fontmetrics.advance instead of the em table.

The docstring's premise ("neither Python emitter has a browser text measurer") no longer holds — _legend_text_width in this same module measures real DejaVu advances from _fontmetrics, which is also the face the rasterizer blits. Reusing it here would make text(bbox=) boxes fit exactly instead of approximately, and drop the second width model.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/xy/_svg.py` around lines 2410 - 2436, Update _estimated_text_width to
measure each line using the existing _fontmetrics.advance path, matching
_legend_text_width and the rasterizer’s DejaVu face. Remove the hand-written
glyph advance table and revise the docstring to reflect the real font-metrics
measurement, while preserving the maximum-line-width and empty-input behavior.

2320-2340: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Sort and clamp the parsed math ranges before slicing.

ranges is consumed in wire order; an unsorted or overlapping pair makes cursor run past the next start, and line[start:end] then re-emits characters already written — duplicated glyphs in the exported label. Producers are internal today, but the guard is two lines.

🛡️ Normalize the ranges
     if not ranges:
         return escape(line)
+    ranges.sort()
     out: list[str] = []
     cursor = 0
     for start, end in ranges:
+        start = max(start, cursor)
+        if start >= end:
+            continue
         if cursor < start:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/xy/_svg.py` around lines 2320 - 2340, Normalize the parsed ranges in
_svg_mathtext_spans before rendering: sort them by start position and clamp each
range against the current cursor so overlapping or out-of-order ranges cannot
re-emit already written characters. Preserve the existing escaping and italic
tspan output for non-overlapping ranges.

1272-1302: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

_y_tick_label_room runs twice per axis here.

_y_title_baseline (Line 1241) measures the tick labels internally, and Line 1291 measures them again — every label goes through _fontmetrics.advance twice on the shared layout() path used by both SVG and PNG export. A small "has an outside title" predicate would avoid the duplicate pass.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/xy/_svg.py` around lines 1272 - 1302, The _y_tick_label_room
calculation is duplicated in _y_axis_left_room because _y_title_baseline already
measures tick labels. Add a lightweight outside-title predicate or equivalent
result reuse, then only call _y_tick_label_room once per axis while preserving
the existing gutter calculations for titled and untitled left y axes.
python/xy/pyplot/_axes.py (2)

1823-1852: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

n_bars is computed twice.

Line 1844 recomputes the value already bound at line 1823; vals is not rebound in between.

♻️ Drop the duplicate
         check_unsupported(kwargs, "bar()/barh()")
-        n_bars = int(np.asarray(vals).size)
         patch_labels: Optional[list[str]] = None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/xy/pyplot/_axes.py` around lines 1823 - 1852, Remove the redundant
second assignment to n_bars in the bar-label handling flow; reuse the value
computed before thickness validation, leaving the existing label-length
validation unchanged.

165-176: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant flag assignment in the vertical-boundary branch.

hit is already True when that block runs; the inner if hit: after the ady loop only needs to break.

♻️ Simplify the break chain
             if ady:
                 for boundary in (y_lo, y_hi):
                     ratio = (boundary - ay0) / ady
                     if 0.0 <= ratio <= 1.0 and x_lo <= ax0 + ratio * adx <= x_hi:
                         hit = True
                         break
-                if hit:
-                    hit = True
-                    break
+                if hit:
+                    break
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/xy/pyplot/_axes.py` around lines 165 - 176, In the vertical-boundary
handling within the surrounding intersection loop, remove the redundant `hit =
True` assignment inside the `if hit:` block after iterating over `(y_lo, y_hi)`.
Keep the conditional break so the loop exits once the boundary intersection has
set `hit`.
tests/test_ui_issue_regressions.py (1)

361-361: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Name the 0.4 × label_size gutter factor. The same magic ratio is asserted in two tests without stating where it comes from (the renderer's title-to-tick pad rule); a module constant plus a one-line comment makes a future drift diagnosable.

Also applies to: 402-402

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_ui_issue_regressions.py` at line 361, Name the shared 0.4 ×
label_size gutter factor used by the assertions near result["gap"] in both
tests. Define a module-level constant representing the renderer’s title-to-tick
pad rule, add a concise comment identifying its source, and replace both inline
0.4 multipliers with that constant.
python/xy/pyplot/__init__.py (1)

303-328: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the new layout keyword in the docstring.

subplots() now consumes layout, but the parameter list still routes readers to "remaining keywords are forwarded to figure", which is no longer true for layout. Same for subplot_mosaic()'s docstring at Line 342.

📝 Suggested docstring addition
     subplot_kw : dict, optional
         Properties applied to every created axes via ``Axes.set``.
+    layout : {"none", "tight", "constrained", "compressed"}, optional
+        Layout engine applied after the grid exists; the rounded/
+        constrained engines map onto the shim's tight-layout pass.
     **kwargs
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/xy/pyplot/__init__.py` around lines 303 - 328, Update the docstrings
for both subplots() and subplot_mosaic() to document the consumed layout keyword
explicitly, including its accepted behavior and effect on figure layout. Ensure
the remaining-keywords description no longer implies that layout is forwarded to
figure().
tests/test_text_weight_defaults.py (1)

78-93: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

_raster_text_bold only decodes styled records with zero ranges. A styled record carrying mathtext ranges (ranges > 0) falls through to the plain-opcode assertion and fails with a misleading "neither a plain nor a styled text record". Deriving the flags offset from the range count keeps the helper honest as soon as a styled label is added.

♻️ Suggested offset derivation
-    styled = body - _STYLED_HEADER
-    if styled >= 0 and stream[styled] == _raster._STYLED_TEXT:
-        (ranges,) = struct.unpack("<I", stream[body - 12 : body - 8])
-        if ranges == 0:
-            return bool(stream[body - 13] & _raster._TEXT_BOLD)
+    for ranges in range(0, 8):
+        styled = body - _STYLED_HEADER - 8 * ranges
+        if styled < 0 or stream[styled] != _raster._STYLED_TEXT:
+            continue
+        (declared,) = struct.unpack("<I", stream[body - 12 - 8 * ranges : body - 8 - 8 * ranges])
+        if declared == ranges:
+            return bool(stream[body - 13 - 8 * ranges] & _raster._TEXT_BOLD)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_text_weight_defaults.py` around lines 78 - 93, Update
_raster_text_bold to recognize styled records regardless of their range count.
Derive the emphasis-flags offset from the encoded ranges value and inspect
_TEXT_BOLD there, while retaining the plain-opcode fallback only for genuinely
plain records.
tests/pyplot/test_layout_text_parity_fixes.py (1)

248-259: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The comment describes a title that neither figure has. Both figures here differ only by set_ylabel; rewording ("the same axes without the y-label") would keep the intent readable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/pyplot/test_layout_text_parity_fixes.py` around lines 248 - 259, Update
the comments in the test around the left_band and top_band assertions to
describe the figures as differing by set_ylabel, not by a title; refer to the
comparison as the same axes without the y-label while preserving the existing
assertion logic.
tests/test_svg_export.py (1)

898-903: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate _tick_label_positions helper across two new test files. Both files independently define the same SVG tick-label-parsing regex/helper; the shared root cause is the lack of a common test utility for this.

  • tests/test_svg_export.py#L898-L903: keep this as the canonical implementation (or move it into a shared test helper module such as tests/conftest.py).
  • tests/pyplot/test_axes_layout.py#L204-L210: drop the local duplicate and import the shared helper instead (adapting the chart.to_svg() call at the call site if the shared version takes a raw svg string).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_svg_export.py` around lines 898 - 903, Centralize the duplicate
_tick_label_positions helper by keeping tests/test_svg_export.py:898-903 as the
canonical implementation, or moving it to a shared test utility. In
tests/pyplot/test_axes_layout.py:204-210, remove the local definition and import
the shared helper, adapting the chart.to_svg() call if the helper accepts raw
SVG text.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@js/src/50_chartview.ts`:
- Around line 5242-5281: Update the y-axis label flow around _axisLabelCss and
attachYTitleToTicks so tick attachment runs only for fallback-CSS placements,
not object label_position placements that provide raw positioning. Preserve the
author’s structured placement unchanged. In attachYTitleToTicks, eliminate the
second post-write getBoundingClientRect layout read by calculating the canvas
correction from the already captured title geometry and intended position delta.

In `@python/xy/_figure.py`:
- Around line 1087-1092: Update the trace-kind allowlist in the loop over
self.traces within the sticky-zero handling method to include "column" alongside
the existing rectangle kinds. Verify the exact kind values emitted by
marks.column via _bar_like and by histogram creation, preserving the exclusion
of segment-based marks such as stem and errorbar.

In `@python/xy/_paint.py`:
- Around line 27-51: Reject triangles containing non-finite coordinates before
projection or serialization in both SVG and raster joined-fill callers. Update
the boundary-reconstruction helper containing vertex_key to return None when any
triangle coordinate is non-finite, and ensure callers skip those triangles so
NaN or infinity never reaches vertex buffers.

In `@python/xy/components.py`:
- Line 1265: Update the width annotations and documentation for the column and
violin components to match their validation behavior: describe column width as
accepting array-like values via _bar_like, while restrict violin width from Any
to the scalar type accepted by self._positive_scalar(...).

In `@python/xy/pyplot/_artists.py`:
- Around line 1142-1149: Update the extended-band label branches in the
formatter loop around lowers, uppers, and n_bands to remove the literal trailing
“s” after lower_text and upper_text, while preserving the existing interior-band
formatting and boundary conditions.
- Around line 520-534: Update the branching in PathCollection.legend_elements so
num=None follows the existing all-unique-values path alongside num=="auto" when
the unique-value condition applies, assigning chosen=unique and
label_values=transformed without converting None through np.asarray.

In `@python/xy/pyplot/_axes.py`:
- Around line 2602-2613: Bound non-nearest resampling targets in the call site
near the image-grid preparation and the corresponding path around the later
image handling block, keeping dimensions between display resolution and a fixed
upper limit without exceeding the source size. Update _resample_grid and
_interpolation_weights to gather only each kernel’s in-support taps (or use the
existing separable interpolation path) instead of constructing dense
target-by-source weight matrices, while preserving interpolation results and
non-finite handling.
- Line 2054: Update the label handling in the per-dataset plotting flow to pass
sequence labels through the existing dataset_values() validation, while
preserving scalar labels as repeated values for len(datasets). Ensure mismatched
label sequences raise the same named length error as other per-dataset options
instead of reaching the loop and causing IndexError.

In `@python/xy/pyplot/_grid.py`:
- Around line 437-441: Update the suptitle compositing block around
kernels.rasterize to blend and write the canvas alpha channel in addition to
RGB. Preserve the existing alpha-weighted overlay compositing, and ensure the
change applies to both absolute and grid rendering paths so transparent figures
retain visible suptitles.

In `@python/xy/pyplot/_mathtext.py`:
- Around line 216-225: The italic-range scan in _convert_math must use
source-span metadata rather than classifying alphabetic tokens from the
flattened rendered text. Preserve each source token’s provenance through
replacements, ensuring commands such as \sum, \prod, \Sigma, and \Gamma are
excluded from italic ranges while ordinary source words retain the existing
_UPRIGHT_WORDS behavior.

In `@python/xy/pyplot/_ticker.py`:
- Around line 180-192: Update tick_values to apply the axes.autolimit_mode
round-number behavior when calling _raw_ticks, ensuring displayed ticks use the
same widened step as view_limits. In view_limits, mirror the upstream
nonsingular validation for the returned tick pair rather than relying only on
len(ticks) < 2, while preserving the existing non-round-number path.

In `@spec/api/styling.md`:
- Line 206: Update the section heading in spec/api/styling.md to exactly match
the cross-reference name in compat.md: “Chrome gutters and the rotated y-axis
title.”
- Around line 211-216: Add the `text` language tag to the fenced code block
containing the left-inset layout formula in the styling documentation,
preserving its existing content and formatting.

In `@spec/matplotlib/compat.md`:
- Around line 67-70: Update the cross-reference in the
xlabel/ylabel/title/suptitle compatibility entry to use the actual “Measured
left gutter and the rotated y-axis title” heading from spec/api/styling.md,
preserving the existing link target and surrounding documentation.

In `@spec/matplotlib/shim-todo.md`:
- Around line 361-370: Remove the obsolete “Barbs glyph” and “Streamplot uses a
fixed-step integrator” bullets from the “Accepted approximations (documented,
still divergent)” section, leaving the updated checklist entries as the
authoritative status. Do not alter unrelated approximation entries.

In `@src/raster.rs`:
- Around line 824-832: Update symbol_extent to apply the r * SQRT_2 extent to
all marker symbols whose geometry reaches diagonal corners: square (1), snapped
pixel (13), diamond (2, 14), and triangle variants (3, 8, 9, 10). Preserve the
existing r extent for other symbols so every caller receives sufficient bounds
without changing unrelated marker sizing.
- Around line 2170-2196: Bound the wire-driven allocation for italic_ranges in
the OP_STYLED_TEXT reader using the existing Reader::bounded_capacity safeguard
with the appropriate element size before reading ranges. Preserve the existing
parsing behavior while ensuring malformed short commands cannot trigger an
unbounded allocation; add a regression case to
malformed_buffer_is_rejected_not_panicked for a huge range_count with
insufficient range data.

In `@tests/pyplot/test_p3_option_contracts.py`:
- Around line 492-512: Update the contour artist’s set_linewidth path to apply
the same internal pixel-scale conversion used during contour construction, while
preserving array cycling and subsequent artist updates. Ensure get_linewidth and
generated contour trace width values use consistent units regardless of whether
linewidths were supplied at construction or through set_linewidth.

In `@tests/pyplot/test_pdsh_gap_features.py`:
- Around line 326-337: Update the record_text spy around _raster._Cmd.text to
accept arbitrary positional and keyword arguments, record the value without
rejecting styled calls, and forward all arguments to original_text. Replace the
manual assignment and try/finally restoration with monkeypatch.setattr for
automatic cleanup.

---

Outside diff comments:
In `@python/xy/components.py`:
- Around line 237-248: Preserve released positional field ordering in the
dataclass definitions: move the new Legend.anchor field to append after render,
and likewise move Axis.margin after all existing released fields. Keep existing
fields such as ncols, title, domain, and bounds in their original positions so
positional construction continues binding unchanged.

In `@spec/api/styling.md`:
- Line 1: Align the cross-reference in the matplotlib compatibility
documentation with the canonical heading in the styling specification. Keep
“Measured left gutter and the rotated y-axis title” as the section name in
spec/api/styling.md and update the corresponding reference text in
spec/matplotlib/compat.md to use that exact wording.

---

Nitpick comments:
In `@js/src/50_chartview.ts`:
- Around line 5130-5139: Rename the absolute-pixel fontRoom parameter in
tickLabelOffset to fontRoomPx, preserving the existing pad + fontRoomPx
calculation and call behavior. Keep this JavaScript helper’s pixel-based
contract distinct from the Python font-size-multiple API.

In `@python/xy/_figure.py`:
- Around line 1047-1069: In the extent calculation around the margin handling,
preserve whether the caller explicitly configured a margin in a separate
variable before assigning the default value. Update the logarithmic branch and
any corresponding configured-margin checks to use that explicitness variable,
while continuing to use the resolved numeric margin for padding calculations.

In `@python/xy/_svg.py`:
- Around line 2410-2436: Update _estimated_text_width to measure each line using
the existing _fontmetrics.advance path, matching _legend_text_width and the
rasterizer’s DejaVu face. Remove the hand-written glyph advance table and revise
the docstring to reflect the real font-metrics measurement, while preserving the
maximum-line-width and empty-input behavior.
- Around line 2320-2340: Normalize the parsed ranges in _svg_mathtext_spans
before rendering: sort them by start position and clamp each range against the
current cursor so overlapping or out-of-order ranges cannot re-emit already
written characters. Preserve the existing escaping and italic tspan output for
non-overlapping ranges.
- Around line 1272-1302: The _y_tick_label_room calculation is duplicated in
_y_axis_left_room because _y_title_baseline already measures tick labels. Add a
lightweight outside-title predicate or equivalent result reuse, then only call
_y_tick_label_room once per axis while preserving the existing gutter
calculations for titled and untitled left y axes.

In `@python/xy/marks.py`:
- Around line 2316-2327: Update the width branching logic around the scalar
validation to use np.ndim(width) == 0 instead of np.isscalar(width), ensuring
Python scalars, NumPy scalars, and 0-D arrays use _positive_scalar while
preserving the existing 1-D array validation path.
- Around line 645-661: Update the width conversion logic in the validation block
to catch failures from np.asarray(width, dtype=np.float64) and re-raise a
ValueError labeled with “{kind} width” and the existing scalar-or-numeric-values
message, suppressing the raw NumPy exception.

In `@python/xy/pyplot/__init__.py`:
- Around line 303-328: Update the docstrings for both subplots() and
subplot_mosaic() to document the consumed layout keyword explicitly, including
its accepted behavior and effect on figure layout. Ensure the remaining-keywords
description no longer implies that layout is forwarded to figure().

In `@python/xy/pyplot/_axes.py`:
- Around line 1823-1852: Remove the redundant second assignment to n_bars in the
bar-label handling flow; reuse the value computed before thickness validation,
leaving the existing label-length validation unchanged.
- Around line 165-176: In the vertical-boundary handling within the surrounding
intersection loop, remove the redundant `hit = True` assignment inside the `if
hit:` block after iterating over `(y_lo, y_hi)`. Keep the conditional break so
the loop exits once the boundary intersection has set `hit`.

In `@python/xy/styles.py`:
- Around line 160-172: Refactor _signed_px to reuse the existing _px parser by
adding a signed=True option to _px and routing signed values through it.
Preserve _signed_px’s acceptance of negative lengths while keeping the shared px
grammar, finite-number validation, and error-message behavior in one
implementation.

In `@tests/pyplot/test_layout_text_parity_fixes.py`:
- Around line 248-259: Update the comments in the test around the left_band and
top_band assertions to describe the figures as differing by set_ylabel, not by a
title; refer to the comparison as the same axes without the y-label while
preserving the existing assertion logic.

In `@tests/test_svg_export.py`:
- Around line 898-903: Centralize the duplicate _tick_label_positions helper by
keeping tests/test_svg_export.py:898-903 as the canonical implementation, or
moving it to a shared test utility. In tests/pyplot/test_axes_layout.py:204-210,
remove the local definition and import the shared helper, adapting the
chart.to_svg() call if the helper accepts raw SVG text.

In `@tests/test_text_weight_defaults.py`:
- Around line 78-93: Update _raster_text_bold to recognize styled records
regardless of their range count. Derive the emphasis-flags offset from the
encoded ranges value and inspect _TEXT_BOLD there, while retaining the
plain-opcode fallback only for genuinely plain records.

In `@tests/test_ui_issue_regressions.py`:
- Line 361: Name the shared 0.4 × label_size gutter factor used by the
assertions near result["gap"] in both tests. Define a module-level constant
representing the renderer’s title-to-tick pad rule, add a concise comment
identifying its source, and replace both inline 0.4 multipliers with that
constant.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 44a6f24d-11b7-4416-913c-f69a298bc613

📥 Commits

Reviewing files that changed from the base of the PR and between 077c53f and 85e3686.

⛔ Files ignored due to path filters (3)
  • pr-assets/matplotlib-gallery-consolidation/annotation-basic.png is excluded by !**/*.png
  • pr-assets/matplotlib-gallery-consolidation/bar-label-demo.png is excluded by !**/*.png
  • pr-assets/matplotlib-gallery-consolidation/horizontal-barchart-legend.png is excluded by !**/*.png
📒 Files selected for processing (84)
  • benchmarks/test_codspeed_pyplot.py
  • js/src/10_colormaps.ts
  • js/src/20_theme.ts
  • js/src/40_gl.ts
  • js/src/50_chartview.ts
  • js/src/51_annotations.ts
  • pr-assets/matplotlib-gallery-consolidation/provenance.json
  • python/xy/_figure.py
  • python/xy/_fontmetrics.py
  • python/xy/_native.py
  • python/xy/_paint.py
  • python/xy/_raster.py
  • python/xy/_svg.py
  • python/xy/channels.py
  • python/xy/components.py
  • python/xy/marks.py
  • python/xy/pyplot/__init__.py
  • python/xy/pyplot/_artists.py
  • python/xy/pyplot/_axes.py
  • python/xy/pyplot/_colors.py
  • python/xy/pyplot/_grid.py
  • python/xy/pyplot/_mathtext.py
  • python/xy/pyplot/_mplfig.py
  • python/xy/pyplot/_plot_types.py
  • python/xy/pyplot/_rc.py
  • python/xy/pyplot/_ticker.py
  • python/xy/pyplot/_translate.py
  • python/xy/styles.py
  • scripts/gen_font.py
  • spec/api/styling.md
  • spec/design/pan-and-zoom-configuration.md
  • spec/design/rust-engine.md
  • spec/matplotlib/compat-changelog.md
  • spec/matplotlib/compat.md
  • spec/matplotlib/shim-todo.md
  • src/font.rs
  • src/kernels.rs
  • src/lib.rs
  • src/raster.rs
  • tests/pyplot/test_advanced_compatibility.py
  • tests/pyplot/test_annotation_box_text_fidelity.py
  • tests/pyplot/test_autoscale_margin_bar_regressions.py
  • tests/pyplot/test_axes_charts.py
  • tests/pyplot/test_axes_layout.py
  • tests/pyplot/test_axis_tick_gallery_compat.py
  • tests/pyplot/test_best_legend_placement.py
  • tests/pyplot/test_categorical_gallery_regressions.py
  • tests/pyplot/test_color_pipeline_fixes.py
  • tests/pyplot/test_contour_gallery_colors.py
  • tests/pyplot/test_frame_geometry.py
  • tests/pyplot/test_gallery_auto_ticks_compat.py
  • tests/pyplot/test_gallery_canvas_gutters.py
  • tests/pyplot/test_gallery_colorbar_options.py
  • tests/pyplot/test_gallery_hist_errorbar_compat.py
  • tests/pyplot/test_gallery_layout_api_regressions.py
  • tests/pyplot/test_gallery_statistics_semantics.py
  • tests/pyplot/test_gallery_stem_regressions.py
  • tests/pyplot/test_gallery_text_pie_compat.py
  • tests/pyplot/test_grid_legend_contracts.py
  • tests/pyplot/test_launch_compat.py
  • tests/pyplot/test_layout_text_parity_fixes.py
  • tests/pyplot/test_line_gallery_semantics.py
  • tests/pyplot/test_line_legend_gallery_compat.py
  • tests/pyplot/test_marker_fidelity.py
  • tests/pyplot/test_matplotlib_mismatch_regressions.py
  • tests/pyplot/test_mesh_autoscale_regressions.py
  • tests/pyplot/test_p3_option_contracts.py
  • tests/pyplot/test_pdsh_gap_features.py
  • tests/pyplot/test_rc_chrome_contracts.py
  • tests/pyplot/test_rc_color_export_contracts.py
  • tests/pyplot/test_reference_semantics.py
  • tests/pyplot/test_silent_drop_regressions.py
  • tests/pyplot/test_state_and_figs.py
  • tests/pyplot/test_visible_style_contracts.py
  • tests/test_axis_title_gutter.py
  • tests/test_components.py
  • tests/test_css_mark_styles.py
  • tests/test_kernels.py
  • tests/test_legend_resize_regression.py
  • tests/test_png_export.py
  • tests/test_static_client_security.py
  • tests/test_svg_export.py
  • tests/test_text_weight_defaults.py
  • tests/test_ui_issue_regressions.py

Comment thread js/src/50_chartview.ts Outdated
Comment thread python/xy/_figure.py
Comment thread python/xy/_paint.py Outdated
Comment thread python/xy/components.py Outdated
Comment thread python/xy/pyplot/_artists.py Outdated
Comment thread spec/matplotlib/shim-todo.md
Comment thread src/raster.rs
Comment on lines +824 to +832
#[inline]
fn symbol_extent(r: f32, sym: u8) -> f32 {
if matches!(sym, 2 | 14) {
r * std::f32::consts::SQRT_2
} else {
r
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

symbol_extent only widens diamond/thin-diamond; square, snapped-pixel, and triangle markers have the same overflow but aren't covered.

symbol_extent special-cases sym 2 and 14 with r * SQRT_2, but looking at symbol_sdf:

  • sym == 1 (square) and sym == 13 (snapped pixel) both use px.abs().max(py.abs()) - r, whose corners sit at (r, r) — distance r*SQRT_2 from center, exactly the same overflow this PR just fixed for diamonds.
  • sym 3/8/9/10 (triangle variants) use triangle_sdf(d, (0.0,-r), (-r,r), (r,r)); the base corners (-r,r)/(r,r) are also r*SQRT_2 from center.

Every caller of symbol_extent (point bbox at line 878, banding extents at 1849/1923, export-size estimates at 2286/2427) inherits this gap, so squares/triangles rendered with a large enough radius will have their corners silently clipped by the bounding-box loop — the exact class of bug this PR fixes for diamonds.

🐛 Proposed fix
 fn symbol_extent(r: f32, sym: u8) -> f32 {
-    if matches!(sym, 2 | 14) {
+    if matches!(sym, 1 | 2 | 3 | 8 | 9 | 10 | 13 | 14) {
         r * std::f32::consts::SQRT_2
     } else {
         r
     }
 }

Also applies to: 878-878, 1849-1849, 1923-1923, 2286-2286, 2427-2427

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/raster.rs` around lines 824 - 832, Update symbol_extent to apply the r *
SQRT_2 extent to all marker symbols whose geometry reaches diagonal corners:
square (1), snapped pixel (13), diamond (2, 14), and triangle variants (3, 8, 9,
10). Preserve the existing r extent for other symbols so every caller receives
sufficient bounds without changing unrelated marker sizing.

Comment thread src/raster.rs
Comment thread tests/pyplot/test_p3_option_contracts.py Outdated
Comment thread tests/pyplot/test_pdsh_gap_features.py Outdated
@sselvakumaran

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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