Consolidate Matplotlib gallery rendering repairs [mpl compatibility]#280
Consolidate Matplotlib gallery rendering repairs [mpl compatibility]#280sselvakumaran wants to merge 12 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (3)
📒 Files selected for processing (41)
🚧 Files skipped from review as they are similar to previous changes (25)
📝 WalkthroughWalkthroughThis 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. ChangesCompatibility and rendering
Estimated code review effort: 5 (Critical) | ~120 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
9297980 to
1df83ef
Compare
Merging this PR will improve performance by 64.01%
Performance Changes
Tip Curious why this is faster? Comment Comparing Footnotes
|
1df83ef to
ba93136
Compare
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
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
anchorinserted mid-order breaksLegend's documented positional contract.Lines 245-246 state new fields must append after
renderbecause positional construction over the released field order must keep binding. Insertinganchorat position 3 silently rebinds existingLegend(True, "upper right", 2, "Title")calls (2now lands onanchorand trips the new validation or, worse,ncolsfalls 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, ...]] = NoneNote the same ordering question applies to
Axis.marginat Line 218, which is inserted betweendomainandbounds.🤖 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 winMismatched section name between the two files.
spec/api/styling.mdintroduces the heading "Measured left gutter and the rotated y-axis title", butspec/matplotlib/compat.mdpoints 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.isscalarmisses 0-d arrays and NumPy-scalar-like inputs.
np.isscalar(np.array(1.1))isFalse, so a 0-d width array falls into_as_1d_floatand fails with "contour width must be 1-D".np.ndim(width) == 0covers 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 valueNon-numeric
widthescapes with a raw NumPy message.
np.asarray(width, dtype=np.float64)on e.g.width="wide"raises NumPy's ownValueErrorwith no{kind} widthlabel, 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 valueReuse of
marginfor two meanings makes the log branch easy to break.
marginis overwritten with the0.03default at Line 1060, so Lines 1061 and 1075 must re-readopts.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
fontRoomhere is absolute px; the Python twin takes a font-size multiple.
_svg._axis_tick_label_offset(axis, unstyled, font_room)returnspad + font_size * font_room(called with0.2/0.8), while this returnspad + fontRoom(called withsize * 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_pxis_pxminus the sign check — consider one parser.The parse/error-message path is duplicated verbatim; a
signed=Trueflag on_pxkeeps 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 winMeasure the bbox with
_fontmetrics.advanceinstead of the em table.The docstring's premise ("neither Python emitter has a browser text measurer") no longer holds —
_legend_text_widthin this same module measures real DejaVu advances from_fontmetrics, which is also the face the rasterizer blits. Reusing it here would maketext(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 valueSort and clamp the parsed math ranges before slicing.
rangesis consumed in wire order; an unsorted or overlapping pair makescursorrun past the nextstart, andline[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_roomruns 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.advancetwice on the sharedlayout()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_barsis computed twice.Line 1844 recomputes the value already bound at line 1823;
valsis 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 valueRedundant flag assignment in the vertical-boundary branch.
hitis alreadyTruewhen that block runs; the innerif hit:after theadyloop 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 valueName the
0.4 × label_sizegutter 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 valueDocument the new
layoutkeyword in the docstring.
subplots()now consumeslayout, but the parameter list still routes readers to "remaining keywords are forwarded tofigure", which is no longer true forlayout. Same forsubplot_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_boldonly 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 valueThe 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 winDuplicate
_tick_label_positionshelper 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 astests/conftest.py).tests/pyplot/test_axes_layout.py#L204-L210: drop the local duplicate and import the shared helper instead (adapting thechart.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
⛔ Files ignored due to path filters (3)
pr-assets/matplotlib-gallery-consolidation/annotation-basic.pngis excluded by!**/*.pngpr-assets/matplotlib-gallery-consolidation/bar-label-demo.pngis excluded by!**/*.pngpr-assets/matplotlib-gallery-consolidation/horizontal-barchart-legend.pngis excluded by!**/*.png
📒 Files selected for processing (84)
benchmarks/test_codspeed_pyplot.pyjs/src/10_colormaps.tsjs/src/20_theme.tsjs/src/40_gl.tsjs/src/50_chartview.tsjs/src/51_annotations.tspr-assets/matplotlib-gallery-consolidation/provenance.jsonpython/xy/_figure.pypython/xy/_fontmetrics.pypython/xy/_native.pypython/xy/_paint.pypython/xy/_raster.pypython/xy/_svg.pypython/xy/channels.pypython/xy/components.pypython/xy/marks.pypython/xy/pyplot/__init__.pypython/xy/pyplot/_artists.pypython/xy/pyplot/_axes.pypython/xy/pyplot/_colors.pypython/xy/pyplot/_grid.pypython/xy/pyplot/_mathtext.pypython/xy/pyplot/_mplfig.pypython/xy/pyplot/_plot_types.pypython/xy/pyplot/_rc.pypython/xy/pyplot/_ticker.pypython/xy/pyplot/_translate.pypython/xy/styles.pyscripts/gen_font.pyspec/api/styling.mdspec/design/pan-and-zoom-configuration.mdspec/design/rust-engine.mdspec/matplotlib/compat-changelog.mdspec/matplotlib/compat.mdspec/matplotlib/shim-todo.mdsrc/font.rssrc/kernels.rssrc/lib.rssrc/raster.rstests/pyplot/test_advanced_compatibility.pytests/pyplot/test_annotation_box_text_fidelity.pytests/pyplot/test_autoscale_margin_bar_regressions.pytests/pyplot/test_axes_charts.pytests/pyplot/test_axes_layout.pytests/pyplot/test_axis_tick_gallery_compat.pytests/pyplot/test_best_legend_placement.pytests/pyplot/test_categorical_gallery_regressions.pytests/pyplot/test_color_pipeline_fixes.pytests/pyplot/test_contour_gallery_colors.pytests/pyplot/test_frame_geometry.pytests/pyplot/test_gallery_auto_ticks_compat.pytests/pyplot/test_gallery_canvas_gutters.pytests/pyplot/test_gallery_colorbar_options.pytests/pyplot/test_gallery_hist_errorbar_compat.pytests/pyplot/test_gallery_layout_api_regressions.pytests/pyplot/test_gallery_statistics_semantics.pytests/pyplot/test_gallery_stem_regressions.pytests/pyplot/test_gallery_text_pie_compat.pytests/pyplot/test_grid_legend_contracts.pytests/pyplot/test_launch_compat.pytests/pyplot/test_layout_text_parity_fixes.pytests/pyplot/test_line_gallery_semantics.pytests/pyplot/test_line_legend_gallery_compat.pytests/pyplot/test_marker_fidelity.pytests/pyplot/test_matplotlib_mismatch_regressions.pytests/pyplot/test_mesh_autoscale_regressions.pytests/pyplot/test_p3_option_contracts.pytests/pyplot/test_pdsh_gap_features.pytests/pyplot/test_rc_chrome_contracts.pytests/pyplot/test_rc_color_export_contracts.pytests/pyplot/test_reference_semantics.pytests/pyplot/test_silent_drop_regressions.pytests/pyplot/test_state_and_figs.pytests/pyplot/test_visible_style_contracts.pytests/test_axis_title_gutter.pytests/test_components.pytests/test_css_mark_styles.pytests/test_kernels.pytests/test_legend_resize_regression.pytests/test_png_export.pytests/test_static_client_security.pytests/test_svg_export.pytests/test_text_weight_defaults.pytests/test_ui_issue_regressions.py
| #[inline] | ||
| fn symbol_extent(r: f32, sym: u8) -> f32 { | ||
| if matches!(sym, 2 | 14) { | ||
| r * std::f32::consts::SQRT_2 | ||
| } else { | ||
| r | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
🎯 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) andsym == 13(snapped pixel) both usepx.abs().max(py.abs()) - r, whose corners sit at(r, r)— distancer*SQRT_2from center, exactly the same overflow this PR just fixed for diamonds.sym3/8/9/10 (triangle variants) usetriangle_sdf(d, (0.0,-r), (-r,r), (r,r)); the base corners(-r,r)/(r,r)are alsor*SQRT_2from 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.
|
@coderabbitai review |
✅ Action performedReview finished.
|
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:
loc="best"legend layoutIf 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.
Annotation geometry
Stack A brings the arrow position, stroke, and text placement back toward the Matplotlib reference without clipping the frame.
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.
Verification
10b22b9: 77/88 paired examples, up from 52/88 on main, with 133 paired figures.legend(loc="best")choice while retaining the 512-vertex budget for connected paths; its exact regression and focused legend suite pass 33/33.git diff --checkalso pass.10b22b9only by the evidence/provenance commit; it contains no later render-code change.Performance
The exact-head CodSpeed comparison at
c196c12is green: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
Summary by CodeRabbit
New Features
Bug Fixes