Skip to content

Fix pie wedges and annotation connectors [mpl compatibility] - #337

Merged
Alek99 merged 9 commits into
agent/gallery-log-scale-compatfrom
agent/pie-annotation-round2
Jul 27, 2026
Merged

Fix pie wedges and annotation connectors [mpl compatibility]#337
Alek99 merged 9 commits into
agent/gallery-log-scale-compatfrom
agent/pie-annotation-round2

Conversation

@sselvakumaran

@sselvakumaran sselvakumaran commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

This groups the pie-wedge and annotation-connector fixes needed by the selected Matplotlib pie gallery:

  • render each pie/donut body as joined fills instead of visible triangle fans;
  • keep explicit wedge strokes on the exterior perimeter and inner/outer donut rings;
  • preserve patch-style legend handles for wedges;
  • accept and retain annotate(zorder=...);
  • keep connectionstyle="angle" as a sharp elbow while angle3 remains quadratic;
  • allow annotation connectors to leave the axes when their target is in bounds.

Visual comparison

This image is stored on immutable review-only evidence commit 683199b; it is not part of this PR's diff or pr-assets.

Matplotlib / XY before / XY after

The before rendering exposes pie-triangulation seams and reduces explicit wedge legend handles to line swatches. The after rendering uses clean joined sectors and patch swatches like Matplotlib.

Exact gallery result

The exact adapted pie_and_polar_charts/pie_and_donut_labels.py now emits both figures:

  • labeled pie with patch legend handles;
  • donut with boxed labels and elbow connectors.

On main, the exact run emitted only the first figure and then stopped at annotate(zorder=0) with an unsupported-keyword error. After the fix, the donut connectors and labels remain within the figure bounds and closely match the Matplotlib reference.

Verification

After rebasing onto current main:

  • focused pie/annotation plus native PNG/SVG tests: 15 passed, 1 browser test deselected
  • exact gallery figures regenerated with unchanged reviewed hashes
  • Ruff and git diff --check: passed

Before the resource-limit adjustment, the same change also passed:

  • tests/pyplot: 799 passed, 65 skipped, 1 deselected
  • SVG/PNG/client tests: 162 passed
  • JavaScript build
  • one browser connector probe

No additional Chrome/Playwright runs were launched after the resource constraint.

Summary by CodeRabbit

  • New Features
    • Added minor tick values and independent minor tick/grid styling across interactive, SVG, and raster charts.
    • Added configurable handling of non-positive values on logarithmic axes.
    • Added horizontal and vertical line scatter markers, including Matplotlib marker mappings.
    • Added elbow-style annotation connectors with improved clipping.
    • Expanded nonlinear scale support and improved pie-chart hatches, outlines, shadows, and errorbar caps.
  • Documentation
    • Updated marker, axis styling, and protocol documentation for the new capabilities.
  • Bug Fixes
    • Improved layout, tick rendering, legend behavior, and annotation output consistency.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f32bcbfa-4ad3-4df5-86ec-03d0195a82ce

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

Axis protocol and rendering

Layer / File(s) Summary
Axis contracts and protocol wiring
python/xy/components.py, python/xy/_figure.py, python/xy/config.py, spec/...
Adds minor tick/style fields, log nonpositive handling, and protocol v9 documentation.
Nonlinear scales and tick generation
python/xy/pyplot/_axes.py, python/xy/pyplot/_ticker.py, python/xy/pyplot/_rc.py
Adds nonlinear locators, formatters, aspect handling, explicit tick-domain expansion, and independent major/minor grid styling.
Renderer axis output and validation
js/src/..., python/xy/_svg.py, python/xy/_raster.py, tests/pyplot/...
Renders minor grids and ticks across canvas, SVG, and raster outputs and validates log-axis behavior.

Line marker symbols

Layer / File(s) Summary
Marker vocabulary and mappings
python/xy/marks.py, python/xy/_validate.py, python/xy/pyplot/_translate.py, docs/..., spec/...
Adds horizontal_line and vertical_line to marker validation, translation, symbol codes, and documentation.
Cross-renderer marker implementation
js/src/40_gl.ts, js/src/50_chartview.ts, python/xy/_svg.py, python/xy/_raster.py, src/raster.rs
Adds line-marker geometry and symbol handling across renderer implementations.
Marker fidelity coverage
tests/pyplot/test_marker_fidelity.py
Tests marker translation, SVG paths, and cross-renderer symbol-code alignment.

Pyplot chart rendering

Layer / File(s) Summary
Annotation geometry and artist lifecycle
python/xy/_arrowgeom.py, js/src/51_annotations.ts, python/xy/pyplot/_artists.py, python/xy/pyplot/_axes.py
Adds elbow connectors, conditional unclipped annotation rendering, z-order handling, and cleanup of attached artists.
Pie and errorbar rendering
python/xy/pyplot/_plot_types.py, python/xy/pyplot/_artists.py
Adds pie hatches, outlines, shadows, joined fills, and scatter-based errorbar caps.
Pyplot contract and layout validation
python/xy/pyplot/_mplfig.py, tests/pyplot/..., tests/test_ui_issue_regressions.py
Updates tight-layout suptitle handling and validates pie, grid, annotation, errorbar, and live-renderer behavior.

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

Possibly related PRs

Suggested reviewers: farhanaliraza, alek99, carlosabadia

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.53% 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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main changes: pie rendering fixes and annotation connector compatibility work.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/pie-annotation-round2

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

@codspeed-hq

codspeed-hq Bot commented Jul 27, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 103 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing agent/pie-annotation-round2 (c853f94) with agent/gallery-log-scale-compat (bcc04cc)

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 changed the base branch from main to agent/gallery-log-scale-compat July 27, 2026 19:15

@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: 9

Caution

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

⚠️ Outside diff range comments (1)
python/xy/pyplot/_rc.py (1)

183-192: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

grid.alpha is only checked for non-negativity.

It lands verbatim in grid_opacity on the axis style (_rc_axis_style/_rc_minor_axis_style) and reaches all three renderers as an opacity, so rcParams["grid.alpha"] = 5 ships an out-of-range value. Elsewhere the shim validates alphas as a closed range (legend framealpha must be between 0 and 1).

🛡️ Proposed fix
             "grid.linewidth",
-            "grid.alpha",
         }:
             value = float(value)
             if value < 0:
                 raise ValueError(f"{key} must be non-negative")
+        if key == "grid.alpha":
+            value = float(value)
+            if not 0.0 <= value <= 1.0:
+                raise ValueError(f"{key} must be between 0 and 1")
🤖 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/_rc.py` around lines 183 - 192, Update validation for the
grid.alpha branch in the rc parameter handling so values must be within the
closed range 0 through 1, while retaining non-negative validation for the other
size, width, and linewidth keys. Use the existing alpha validation pattern and
error style, and ensure the constrained value passed through _rc_axis_style and
_rc_minor_axis_style remains in range.
🧹 Nitpick comments (5)
tests/pyplot/test_axis_tick_gallery_compat.py (1)

61-63: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Also assert the y minor tier stayed untouched.

The test's purpose is that the proxy targets only its own dimension; for the new minor tier it currently only checks x. xaxis.grid(which="minor") must leave y's minor_style transparent, which is exactly the routing grid()'s new tiers loop could get wrong.

💚 Proposed addition
     ax.xaxis.grid(which="minor", color="0.9")
     assert ax._axis_props("x")["minor_style"]["grid_color"] == "rgb(230,230,230)"
+    assert ax._axis_props("y")["minor_style"]["grid_color"] == "transparent"
     assert ax._axis_props("y")["style"]["grid_color"] == "`#1f77b4`"
🤖 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_axis_tick_gallery_compat.py` around lines 61 - 63, Extend
the test around the xaxis.grid minor-tier assertion to also verify that
ax._axis_props("y")["minor_style"]["grid_color"] remains transparent. Keep the
existing x minor-style and y major-style assertions, confirming the grid call
only updates the targeted dimension.
python/xy/pyplot/_ticker.py (2)

325-330: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Return sorted ticks like the sibling locators.

With multi-entry subs, negative decades emit descending values (-10, -20, -50), so tick_values is non-monotonic. Renderers are order-insensitive, but LogLocator.tick_values sorts and Axes.get_*ticks(minor=True) passes this through unsorted, so the public tick list can come back non-monotonic on a symlog axis.

♻️ Proposed fix
-        return np.asarray(ticks, dtype=float)
+        return np.sort(np.asarray(ticks, dtype=float))
🤖 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/_ticker.py` around lines 325 - 330, Update the tick
construction in the locator method containing the shown `ticks` comprehension to
return the generated values in ascending order. Preserve the existing
decade/substitution logic, including the zero-decade special case, and sort the
final array before returning it so public symlog tick values are monotonic.

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

symthresh is accepted but never used.

It is stored and settable via set_params, yet tick_values never reads it, so AsinhLocator(symthresh=...) is silently ignored rather than symmetrizing the range as Matplotlib does. Either implement it or drop it from the surface so the omission isn't silent.

Also applies to: 363-364

🤖 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/_ticker.py` around lines 341 - 347, Implement symthresh
handling in AsinhLocator.tick_values, using the stored self.symthresh value to
symmetrize the computed tick range when the threshold condition is met, matching
Matplotlib behavior; ensure values updated through set_params are honored. Do
not leave symthresh as an unused constructor or configuration parameter.
js/src/50_chartview.ts (1)

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

Minor ticks/grid are primary-axis only; secondary axes silently drop theirs.

minorTicks is computed for xAxis/yAxis only, and the extraXAxes/extraYAxes tick loops below have no minor pass. The Python side does publish minor_tick_values for y2 (_build_chart calls _apply_tickers("y2", ...)), so a twinned log axis ships minors that no renderer draws — _raster.render_raster likewise only calls minor_axis_ticks(xa)/(ya).

All three renderers agree, so nothing diverges; the gap is just undocumented. Worth recording in the spec (or extending the loops) so twin-axis minor ticks aren't a silent omission.

As per coding guidelines, "Record every decimation and tier decision in the specification; decisions must never be silent" and "Keep the entire spec/ directory current with every relevant code, configuration, build, and release change".

Also applies to: 5090-5102, 5114-5126

🤖 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 4961 - 5002, Document in the relevant
spec that minor ticks and minor grid lines are intentionally rendered only for
the primary xAxis and yAxis, while extraXAxes and extraYAxes do not receive a
minor-tick rendering pass. Record that secondary-axis minor_tick_values,
including y2 values produced by _apply_tickers, are currently omitted
consistently across renderers, including _raster.render_raster; do not change
rendering behavior unless extending all corresponding axis loops.

Source: Coding guidelines

python/xy/pyplot/_axes.py (1)

654-661: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Mirror the major-locator guards on set_minor_locator.

The major path rejects non-Locator objects and no-ops on pandas units-registry tickers; the minor path stores anything. A pandas TimeSeries_DateLocator installed as a minor locator now reaches _apply_tickers, which only checks hasattr(tick_values), and publishes unit-registry ordinals as minor_tick_values on a native ms axis.

♻️ Proposed parity fix
     def set_minor_locator(self, locator: Any) -> None:
+        if self._is_units_registry_ticker(locator):
+            return
+        if not hasattr(locator, "tick_values"):
+            raise TypeError("set_minor_locator() requires a Locator with tick_values()")
         host, key = self._ticker_slot()
         host._tickers[(key, "minor_locator")] = locator
         self.axes._invalidate()
🤖 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 654 - 661, Update set_minor_locator
to mirror set_major_locator’s validation and pandas units-registry guard: reject
non-Locator values and no-op for registry-managed pandas tickers before storing
the locator. Preserve the existing ticker storage and invalidation behavior for
valid minor locators, preventing TimeSeries_DateLocator values from reaching
_apply_tickers.
🤖 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/00_header.ts`:
- Around line 31-33: Update the bump comment above PROTOCOL to reference an
older v8 client instead of v7, keeping the rest of the comment unchanged.

In `@js/src/40_gl.ts`:
- Line 87: Update the mode == 3 branches in xyAxisCoord() and xyViewCoord() to
return the existing -1e30 off-screen sentinel for non-positive values, matching
mode == 1; remove the uintBitsToFloat NaN fallback while preserving the
logarithm calculation for positive values.

In `@js/src/51_annotations.ts`:
- Around line 304-323: Balance the per-annotation ctx.save() in the annotation
rendering loop by calling ctx.restore() before every continue in the band/rule
branches, including the branches around lines 329, 332, and 345–347. Ensure
annotations that apply the connector-target clip restore that state before being
skipped, while preserving the existing rendering behavior for processed
annotations.

In `@python/xy/config.py`:
- Around line 22-24: Update the predecessor-version reference in the comment
above PROTOCOL_VERSION from v7 to v8, keeping the rest of the protocol-version
explanation unchanged.

In `@python/xy/pyplot/_axes.py`:
- Around line 539-542: Update the symlog minor locator configuration in the axes
ticker setup so a missing subs value uses automatic subdivisions rather than
SymmetricalLogLocator’s degenerate (1.0,) default. Preserve explicitly
configured spec.get("subs") values, and align the behavior with the sibling
LogLocator handling so minor ticks remain distinct from major decades.
- Around line 3313-3317: Move z-order ordering out of the existing-entry-only
branch in annotate and into the entry-addition or draw path used by the host
axis. Ensure host._entries is reordered by each entry’s _zorder whenever artists
are added or rendered, so entries added after annotate(zorder=...) are
positioned correctly while preserving existing annotation z-order behavior.
- Around line 3555-3557: Validate log-scale bounds before storing raw extents in
_aspect_bounds, covering both autoscale(tight=True) and image-extent assignment
paths. Reuse the positivity and finiteness validation expected by
_aspect_coordinates() so invalid bounds raise at assignment time rather than
later during get_position() box-aspect layout.

In `@python/xy/pyplot/_plot_types.py`:
- Around line 618-633: In the candidate/triangle clipping loop, add a cheap
axis-aligned bounding-box rejection before calling _clip_segment_to_triangle:
skip pairs whose segment bounds do not overlap the triangle bounds. Precompute
each triangle’s bounds once outside the candidate loop, preserve all existing
clipping and output behavior for overlapping pairs, and avoid introducing NumPy
allocations for the rejection.

In `@tests/pyplot/test_gallery_hist_errorbar_compat.py`:
- Around line 328-331: Update the cap color assertion in the loop over
horizontal and vertical caps so it validates the actual stored color without
relying on the hard-coded default from kwargs.get("stroke", "red"). Either
assert the entry’s existing color field directly or explicitly populate stroke
in the cap kwargs if that is the intended contract.

---

Outside diff comments:
In `@python/xy/pyplot/_rc.py`:
- Around line 183-192: Update validation for the grid.alpha branch in the rc
parameter handling so values must be within the closed range 0 through 1, while
retaining non-negative validation for the other size, width, and linewidth keys.
Use the existing alpha validation pattern and error style, and ensure the
constrained value passed through _rc_axis_style and _rc_minor_axis_style remains
in range.

---

Nitpick comments:
In `@js/src/50_chartview.ts`:
- Around line 4961-5002: Document in the relevant spec that minor ticks and
minor grid lines are intentionally rendered only for the primary xAxis and
yAxis, while extraXAxes and extraYAxes do not receive a minor-tick rendering
pass. Record that secondary-axis minor_tick_values, including y2 values produced
by _apply_tickers, are currently omitted consistently across renderers,
including _raster.render_raster; do not change rendering behavior unless
extending all corresponding axis loops.

In `@python/xy/pyplot/_axes.py`:
- Around line 654-661: Update set_minor_locator to mirror set_major_locator’s
validation and pandas units-registry guard: reject non-Locator values and no-op
for registry-managed pandas tickers before storing the locator. Preserve the
existing ticker storage and invalidation behavior for valid minor locators,
preventing TimeSeries_DateLocator values from reaching _apply_tickers.

In `@python/xy/pyplot/_ticker.py`:
- Around line 325-330: Update the tick construction in the locator method
containing the shown `ticks` comprehension to return the generated values in
ascending order. Preserve the existing decade/substitution logic, including the
zero-decade special case, and sort the final array before returning it so public
symlog tick values are monotonic.
- Around line 341-347: Implement symthresh handling in AsinhLocator.tick_values,
using the stored self.symthresh value to symmetrize the computed tick range when
the threshold condition is met, matching Matplotlib behavior; ensure values
updated through set_params are honored. Do not leave symthresh as an unused
constructor or configuration parameter.

In `@tests/pyplot/test_axis_tick_gallery_compat.py`:
- Around line 61-63: Extend the test around the xaxis.grid minor-tier assertion
to also verify that ax._axis_props("y")["minor_style"]["grid_color"] remains
transparent. Keep the existing x minor-style and y major-style assertions,
confirming the grid call only updates the targeted dimension.
🪄 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: 55e0be04-7c7e-4678-9eb5-4eda352ce974

📥 Commits

Reviewing files that changed from the base of the PR and between 9fdc236 and 9d2ff34.

📒 Files selected for processing (39)
  • docs/charts/scatter.md
  • docs/styling/mark-styles.md
  • js/src/00_header.ts
  • js/src/40_gl.ts
  • js/src/50_chartview.ts
  • js/src/51_annotations.ts
  • python/xy/_arrowgeom.py
  • python/xy/_figure.py
  • python/xy/_raster.py
  • python/xy/_svg.py
  • python/xy/_validate.py
  • python/xy/components.py
  • python/xy/config.py
  • python/xy/interaction.py
  • python/xy/marks.py
  • python/xy/pyplot/__init__.py
  • python/xy/pyplot/_artists.py
  • python/xy/pyplot/_axes.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
  • spec/api/styling.md
  • spec/design/renderer-architecture.md
  • spec/design/wire-protocol.md
  • src/raster.rs
  • tests/pyplot/test_axes_charts.py
  • tests/pyplot/test_axes_layout.py
  • tests/pyplot/test_axis_tick_gallery_compat.py
  • tests/pyplot/test_gallery_hist_errorbar_compat.py
  • tests/pyplot/test_gallery_text_pie_compat.py
  • tests/pyplot/test_grid_legend_contracts.py
  • tests/pyplot/test_log_scale_gallery_compat.py
  • tests/pyplot/test_marker_fidelity.py
  • tests/pyplot/test_nonlinear_scale_gallery_compat.py
  • tests/pyplot/test_p3_option_contracts.py
  • tests/pyplot/test_pie_annotation_grouped_repair.py
  • tests/test_ui_issue_regressions.py

Comment thread python/xy/pyplot/_plot_types.py

@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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (1)
python/xy/pyplot/_rc.py (1)

183-192: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

grid.alpha is only checked for non-negativity.

It lands verbatim in grid_opacity on the axis style (_rc_axis_style/_rc_minor_axis_style) and reaches all three renderers as an opacity, so rcParams["grid.alpha"] = 5 ships an out-of-range value. Elsewhere the shim validates alphas as a closed range (legend framealpha must be between 0 and 1).

🛡️ Proposed fix
             "grid.linewidth",
-            "grid.alpha",
         }:
             value = float(value)
             if value < 0:
                 raise ValueError(f"{key} must be non-negative")
+        if key == "grid.alpha":
+            value = float(value)
+            if not 0.0 <= value <= 1.0:
+                raise ValueError(f"{key} must be between 0 and 1")
🤖 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/_rc.py` around lines 183 - 192, Update validation for the
grid.alpha branch in the rc parameter handling so values must be within the
closed range 0 through 1, while retaining non-negative validation for the other
size, width, and linewidth keys. Use the existing alpha validation pattern and
error style, and ensure the constrained value passed through _rc_axis_style and
_rc_minor_axis_style remains in range.
🧹 Nitpick comments (5)
tests/pyplot/test_axis_tick_gallery_compat.py (1)

61-63: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Also assert the y minor tier stayed untouched.

The test's purpose is that the proxy targets only its own dimension; for the new minor tier it currently only checks x. xaxis.grid(which="minor") must leave y's minor_style transparent, which is exactly the routing grid()'s new tiers loop could get wrong.

💚 Proposed addition
     ax.xaxis.grid(which="minor", color="0.9")
     assert ax._axis_props("x")["minor_style"]["grid_color"] == "rgb(230,230,230)"
+    assert ax._axis_props("y")["minor_style"]["grid_color"] == "transparent"
     assert ax._axis_props("y")["style"]["grid_color"] == "`#1f77b4`"
🤖 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_axis_tick_gallery_compat.py` around lines 61 - 63, Extend
the test around the xaxis.grid minor-tier assertion to also verify that
ax._axis_props("y")["minor_style"]["grid_color"] remains transparent. Keep the
existing x minor-style and y major-style assertions, confirming the grid call
only updates the targeted dimension.
python/xy/pyplot/_ticker.py (2)

325-330: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Return sorted ticks like the sibling locators.

With multi-entry subs, negative decades emit descending values (-10, -20, -50), so tick_values is non-monotonic. Renderers are order-insensitive, but LogLocator.tick_values sorts and Axes.get_*ticks(minor=True) passes this through unsorted, so the public tick list can come back non-monotonic on a symlog axis.

♻️ Proposed fix
-        return np.asarray(ticks, dtype=float)
+        return np.sort(np.asarray(ticks, dtype=float))
🤖 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/_ticker.py` around lines 325 - 330, Update the tick
construction in the locator method containing the shown `ticks` comprehension to
return the generated values in ascending order. Preserve the existing
decade/substitution logic, including the zero-decade special case, and sort the
final array before returning it so public symlog tick values are monotonic.

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

symthresh is accepted but never used.

It is stored and settable via set_params, yet tick_values never reads it, so AsinhLocator(symthresh=...) is silently ignored rather than symmetrizing the range as Matplotlib does. Either implement it or drop it from the surface so the omission isn't silent.

Also applies to: 363-364

🤖 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/_ticker.py` around lines 341 - 347, Implement symthresh
handling in AsinhLocator.tick_values, using the stored self.symthresh value to
symmetrize the computed tick range when the threshold condition is met, matching
Matplotlib behavior; ensure values updated through set_params are honored. Do
not leave symthresh as an unused constructor or configuration parameter.
js/src/50_chartview.ts (1)

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

Minor ticks/grid are primary-axis only; secondary axes silently drop theirs.

minorTicks is computed for xAxis/yAxis only, and the extraXAxes/extraYAxes tick loops below have no minor pass. The Python side does publish minor_tick_values for y2 (_build_chart calls _apply_tickers("y2", ...)), so a twinned log axis ships minors that no renderer draws — _raster.render_raster likewise only calls minor_axis_ticks(xa)/(ya).

All three renderers agree, so nothing diverges; the gap is just undocumented. Worth recording in the spec (or extending the loops) so twin-axis minor ticks aren't a silent omission.

As per coding guidelines, "Record every decimation and tier decision in the specification; decisions must never be silent" and "Keep the entire spec/ directory current with every relevant code, configuration, build, and release change".

Also applies to: 5090-5102, 5114-5126

🤖 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 4961 - 5002, Document in the relevant
spec that minor ticks and minor grid lines are intentionally rendered only for
the primary xAxis and yAxis, while extraXAxes and extraYAxes do not receive a
minor-tick rendering pass. Record that secondary-axis minor_tick_values,
including y2 values produced by _apply_tickers, are currently omitted
consistently across renderers, including _raster.render_raster; do not change
rendering behavior unless extending all corresponding axis loops.

Source: Coding guidelines

python/xy/pyplot/_axes.py (1)

654-661: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Mirror the major-locator guards on set_minor_locator.

The major path rejects non-Locator objects and no-ops on pandas units-registry tickers; the minor path stores anything. A pandas TimeSeries_DateLocator installed as a minor locator now reaches _apply_tickers, which only checks hasattr(tick_values), and publishes unit-registry ordinals as minor_tick_values on a native ms axis.

♻️ Proposed parity fix
     def set_minor_locator(self, locator: Any) -> None:
+        if self._is_units_registry_ticker(locator):
+            return
+        if not hasattr(locator, "tick_values"):
+            raise TypeError("set_minor_locator() requires a Locator with tick_values()")
         host, key = self._ticker_slot()
         host._tickers[(key, "minor_locator")] = locator
         self.axes._invalidate()
🤖 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 654 - 661, Update set_minor_locator
to mirror set_major_locator’s validation and pandas units-registry guard: reject
non-Locator values and no-op for registry-managed pandas tickers before storing
the locator. Preserve the existing ticker storage and invalidation behavior for
valid minor locators, preventing TimeSeries_DateLocator values from reaching
_apply_tickers.
🤖 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/00_header.ts`:
- Around line 31-33: Update the bump comment above PROTOCOL to reference an
older v8 client instead of v7, keeping the rest of the comment unchanged.

In `@js/src/40_gl.ts`:
- Line 87: Update the mode == 3 branches in xyAxisCoord() and xyViewCoord() to
return the existing -1e30 off-screen sentinel for non-positive values, matching
mode == 1; remove the uintBitsToFloat NaN fallback while preserving the
logarithm calculation for positive values.

In `@js/src/51_annotations.ts`:
- Around line 304-323: Balance the per-annotation ctx.save() in the annotation
rendering loop by calling ctx.restore() before every continue in the band/rule
branches, including the branches around lines 329, 332, and 345–347. Ensure
annotations that apply the connector-target clip restore that state before being
skipped, while preserving the existing rendering behavior for processed
annotations.

In `@python/xy/config.py`:
- Around line 22-24: Update the predecessor-version reference in the comment
above PROTOCOL_VERSION from v7 to v8, keeping the rest of the protocol-version
explanation unchanged.

In `@python/xy/pyplot/_axes.py`:
- Around line 539-542: Update the symlog minor locator configuration in the axes
ticker setup so a missing subs value uses automatic subdivisions rather than
SymmetricalLogLocator’s degenerate (1.0,) default. Preserve explicitly
configured spec.get("subs") values, and align the behavior with the sibling
LogLocator handling so minor ticks remain distinct from major decades.
- Around line 3313-3317: Move z-order ordering out of the existing-entry-only
branch in annotate and into the entry-addition or draw path used by the host
axis. Ensure host._entries is reordered by each entry’s _zorder whenever artists
are added or rendered, so entries added after annotate(zorder=...) are
positioned correctly while preserving existing annotation z-order behavior.
- Around line 3555-3557: Validate log-scale bounds before storing raw extents in
_aspect_bounds, covering both autoscale(tight=True) and image-extent assignment
paths. Reuse the positivity and finiteness validation expected by
_aspect_coordinates() so invalid bounds raise at assignment time rather than
later during get_position() box-aspect layout.

In `@python/xy/pyplot/_plot_types.py`:
- Around line 618-633: In the candidate/triangle clipping loop, add a cheap
axis-aligned bounding-box rejection before calling _clip_segment_to_triangle:
skip pairs whose segment bounds do not overlap the triangle bounds. Precompute
each triangle’s bounds once outside the candidate loop, preserve all existing
clipping and output behavior for overlapping pairs, and avoid introducing NumPy
allocations for the rejection.

In `@tests/pyplot/test_gallery_hist_errorbar_compat.py`:
- Around line 328-331: Update the cap color assertion in the loop over
horizontal and vertical caps so it validates the actual stored color without
relying on the hard-coded default from kwargs.get("stroke", "red"). Either
assert the entry’s existing color field directly or explicitly populate stroke
in the cap kwargs if that is the intended contract.

---

Outside diff comments:
In `@python/xy/pyplot/_rc.py`:
- Around line 183-192: Update validation for the grid.alpha branch in the rc
parameter handling so values must be within the closed range 0 through 1, while
retaining non-negative validation for the other size, width, and linewidth keys.
Use the existing alpha validation pattern and error style, and ensure the
constrained value passed through _rc_axis_style and _rc_minor_axis_style remains
in range.

---

Nitpick comments:
In `@js/src/50_chartview.ts`:
- Around line 4961-5002: Document in the relevant spec that minor ticks and
minor grid lines are intentionally rendered only for the primary xAxis and
yAxis, while extraXAxes and extraYAxes do not receive a minor-tick rendering
pass. Record that secondary-axis minor_tick_values, including y2 values produced
by _apply_tickers, are currently omitted consistently across renderers,
including _raster.render_raster; do not change rendering behavior unless
extending all corresponding axis loops.

In `@python/xy/pyplot/_axes.py`:
- Around line 654-661: Update set_minor_locator to mirror set_major_locator’s
validation and pandas units-registry guard: reject non-Locator values and no-op
for registry-managed pandas tickers before storing the locator. Preserve the
existing ticker storage and invalidation behavior for valid minor locators,
preventing TimeSeries_DateLocator values from reaching _apply_tickers.

In `@python/xy/pyplot/_ticker.py`:
- Around line 325-330: Update the tick construction in the locator method
containing the shown `ticks` comprehension to return the generated values in
ascending order. Preserve the existing decade/substitution logic, including the
zero-decade special case, and sort the final array before returning it so public
symlog tick values are monotonic.
- Around line 341-347: Implement symthresh handling in AsinhLocator.tick_values,
using the stored self.symthresh value to symmetrize the computed tick range when
the threshold condition is met, matching Matplotlib behavior; ensure values
updated through set_params are honored. Do not leave symthresh as an unused
constructor or configuration parameter.

In `@tests/pyplot/test_axis_tick_gallery_compat.py`:
- Around line 61-63: Extend the test around the xaxis.grid minor-tier assertion
to also verify that ax._axis_props("y")["minor_style"]["grid_color"] remains
transparent. Keep the existing x minor-style and y major-style assertions,
confirming the grid call only updates the targeted dimension.
🪄 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: 55e0be04-7c7e-4678-9eb5-4eda352ce974

📥 Commits

Reviewing files that changed from the base of the PR and between 9fdc236 and 9d2ff34.

📒 Files selected for processing (39)
  • docs/charts/scatter.md
  • docs/styling/mark-styles.md
  • js/src/00_header.ts
  • js/src/40_gl.ts
  • js/src/50_chartview.ts
  • js/src/51_annotations.ts
  • python/xy/_arrowgeom.py
  • python/xy/_figure.py
  • python/xy/_raster.py
  • python/xy/_svg.py
  • python/xy/_validate.py
  • python/xy/components.py
  • python/xy/config.py
  • python/xy/interaction.py
  • python/xy/marks.py
  • python/xy/pyplot/__init__.py
  • python/xy/pyplot/_artists.py
  • python/xy/pyplot/_axes.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
  • spec/api/styling.md
  • spec/design/renderer-architecture.md
  • spec/design/wire-protocol.md
  • src/raster.rs
  • tests/pyplot/test_axes_charts.py
  • tests/pyplot/test_axes_layout.py
  • tests/pyplot/test_axis_tick_gallery_compat.py
  • tests/pyplot/test_gallery_hist_errorbar_compat.py
  • tests/pyplot/test_gallery_text_pie_compat.py
  • tests/pyplot/test_grid_legend_contracts.py
  • tests/pyplot/test_log_scale_gallery_compat.py
  • tests/pyplot/test_marker_fidelity.py
  • tests/pyplot/test_nonlinear_scale_gallery_compat.py
  • tests/pyplot/test_p3_option_contracts.py
  • tests/pyplot/test_pie_annotation_grouped_repair.py
  • tests/test_ui_issue_regressions.py
🛑 Comments failed to post (8)
js/src/00_header.ts (1)

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

Stale predecessor-version reference in the bump comment.

Comment says "an older v7 client" but this bumps v8→v9, so the stale-client reference should say "v8 client" (matching the pattern in spec/design/wire-protocol.md's own changelog, e.g. "v8 adds ... an older v7 client would accept ...").

🤖 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/00_header.ts` around lines 31 - 33, Update the bump comment above
PROTOCOL to reference an older v8 client instead of v7, keeping the rest of the
comment unchanged.
js/src/40_gl.ts (1)

87-87: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

WebGL2 GLSL ES 3.00 NaN gl_Position vertex clipping behavior undefined

💡 Result:

In WebGL2 and OpenGL ES 3.00, the behavior of gl_Position when it contains NaN (Not-a-Number) or is left uninitialized is considered undefined [1][2][3]. When gl_Position is undefined—such as when a vertex shader fails to write to it—or when it contains invalid floating-point values like NaN, the implementation is not required to handle the data in any specific way [1][4][5]. Because primitive assembly, clipping, and rasterization depend on valid clip-space coordinates, providing NaN typically leads to unpredictable rendering results [1][6][7]. Common outcomes include the triangle or primitive being discarded, rendered at incorrect positions, or causing visual artifacts, as the clipping stage cannot perform geometric tests against invalid coordinates [1][6][8]. Key aspects of this behavior include: 1. Undefined values: According to the OpenGL ES Shading Language Specification, if a result is undefined, the system may behave as if it had been assigned a random value [1]. Implementations are not required to catch these errors and may, but are not obligated to, detect the use of undefined values and behave accordingly (e.g., triggering a trap) [1]. 2. NaN support: The OpenGL ES specification permits, but does not require, implementations to support NaN in floating-point computations [6][4][5]. Consequently, an expression that evaluates to NaN on one GPU might behave differently on another [6][8]. 3. Clipping dependencies: The fixed-function clipping stage assumes gl_Position contains valid homogeneous coordinates [3][7]. If NaN is present, the mathematical comparisons required for clipping (e.g., checking if the vertex is within the view volume) become non-deterministic or fail entirely [6][7]. 4. Best practices: It is strongly discouraged to rely on NaN or uninitialized gl_Position values for flow control or discarding primitives [6][8]. Instead, developers should use well-defined techniques to discard primitives, such as placing them outside the view frustum (e.g., setting gl_Position to a value like vec4(0, 0, 0, -1)) [8], or using discard in a fragment shader if applicable.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the referenced file and nearby logic.
wc -l js/src/40_gl.ts
sed -n '1,160p' js/src/40_gl.ts

# Find the surrounding mode handling and any gl_Position assignments.
rg -n "mode == 1|mode == 3|gl_Position|uintBitsToFloat|log\\(" js/src/40_gl.ts js/src -g '*.ts'

Repository: reflex-dev/xy

Length of output: 11793


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect how the log modes are used and documented.
rg -n "u_xmode|u_ymode|mode == 3|mode == 1|xyViewValue|xyAxisCoord|log scale|log" js/src/40_gl.ts js/src -g '*.ts' -g '*.js'

# Read the smaller relevant sections around the helpers and any mode documentation.
sed -n '70,120p' js/src/40_gl.ts
sed -n '650,720p' js/src/40_gl.ts
sed -n '740,900p' js/src/40_gl.ts

Repository: reflex-dev/xy

Length of output: 28591


Use the off-screen sentinel for masked log values
The mode == 3 branch should use the same -1e30 sentinel as mode == 1 in both xyAxisCoord() and xyViewCoord(). uintBitsToFloat(0x7fc00000u) can feed NaN into gl_Position, and clip behavior is undefined there. The probe only proves the mode reaches the renderer, not that non-positive values are consistently discarded.

🤖 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/40_gl.ts` at line 87, Update the mode == 3 branches in xyAxisCoord()
and xyViewCoord() to return the existing -1e30 off-screen sentinel for
non-positive values, matching mode == 1; remove the uintBitsToFloat NaN fallback
while preserving the logarithm calculation for positive values.
js/src/51_annotations.ts (1)

304-323: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Per-annotation ctx.save() is not balanced on the continue paths.

ctx.save() now runs per annotation (Line 305) with the matching ctx.restore() only at Line 398, but the band/rule branches still continue (Lines 329, 332, 345–347). Each skipped annotation leaks a saved state and — when the connector-target clip was applied at Lines 320–322 — leaves the plot-rect clip active for every later annotation and for whatever draws on this context afterwards. That was harmless before because the save/clip lived outside the loop.

🐛 Restore before skipping
-        if (!Number.isFinite(a) || !Number.isFinite(b)) continue;
+        if (!Number.isFinite(a) || !Number.isFinite(b)) { ctx.restore(); continue; }
         const lo = Math.max(vertical ? p.x : p.y, Math.min(a, b));
         const hi = Math.min(vertical ? p.x + p.w : p.y + p.h, Math.max(a, b));
-        if (hi <= lo) continue;
+        if (hi <= lo) { ctx.restore(); continue; }
-        if (!Number.isFinite(pos)) continue;
-        if (vertical && (pos < p.x - 1 || pos > p.x + p.w + 1)) continue;
-        if (!vertical && (pos < p.y - 1 || pos > p.y + p.h + 1)) continue;
+        if (!Number.isFinite(pos)) { ctx.restore(); continue; }
+        if (vertical && (pos < p.x - 1 || pos > p.x + p.w + 1)) { ctx.restore(); continue; }
+        if (!vertical && (pos < p.y - 1 || pos > p.y + p.h + 1)) { ctx.restore(); continue; }

Alternatively, extract the per-annotation body into a method and use return so the save/restore pair stays local.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        if (!Number.isFinite(a) || !Number.isFinite(b)) { ctx.restore(); continue; }
        const lo = Math.max(vertical ? p.x : p.y, Math.min(a, b));
        const hi = Math.min(vertical ? p.x + p.w : p.y + p.h, Math.max(a, b));
        if (hi <= lo) { ctx.restore(); continue; }

        if (!Number.isFinite(pos)) { ctx.restore(); continue; }
        if (vertical && (pos < p.x - 1 || pos > p.x + p.w + 1)) { ctx.restore(); continue; }
        if (!vertical && (pos < p.y - 1 || pos > p.y + p.h + 1)) { ctx.restore(); continue; }
🤖 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/51_annotations.ts` around lines 304 - 323, Balance the per-annotation
ctx.save() in the annotation rendering loop by calling ctx.restore() before
every continue in the band/rule branches, including the branches around lines
329, 332, and 345–347. Ensure annotations that apply the connector-target clip
restore that state before being skipped, while preserving the existing rendering
behavior for processed annotations.
python/xy/config.py (1)

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

Stale predecessor-version reference, mirrors js/src/00_header.ts.

Same "v7 client" vs. expected "v8 client" issue as js/src/00_header.ts.

🤖 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/config.py` around lines 22 - 24, Update the predecessor-version
reference in the comment above PROTOCOL_VERSION from v7 to v8, keeping the rest
of the protocol-version explanation unchanged.
python/xy/pyplot/_axes.py (3)

539-542: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Symlog minor locator collapses onto the major decades.

SymmetricalLogLocator.__init__ maps subs=None(1.0,) (_ticker.py line 285), so passing spec.get("subs") (normally None) builds a minor locator that returns exactly the major decade set. _apply_tickers then drops minors coinciding with majors, leaving symlog axes with no minor ticks or minor grid at all. Matplotlib's minor SymmetricalLogLocator defaults to subs=np.arange(1.0, base).

Note the sibling LogLocator in this same change treats subs=None as "automatic subdivisions"; the two locators now disagree on that sentinel.

🐛 Proposed fix (either pass explicit subs here, or align the locator default)
             "minor_locator": SymmetricalLogLocator(
                 **locator_options,
-                subs=spec.get("subs"),
+                subs=spec.get("subs") or tuple(float(s) for s in np.arange(2.0, spec["base"])),
             ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

            "minor_locator": SymmetricalLogLocator(
                **locator_options,
                subs=spec.get("subs") or tuple(float(s) for s in np.arange(2.0, spec["base"])),
            ),
🤖 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 539 - 542, Update the symlog minor
locator configuration in the axes ticker setup so a missing subs value uses
automatic subdivisions rather than SymmetricalLogLocator’s degenerate (1.0,)
default. Preserve explicitly configured spec.get("subs") values, and align the
behavior with the sibling LogLocator handling so minor ticks remain distinct
from major decades.

3313-3317: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Which tests/galleries exercise annotate(zorder=...) and in what call order?
rg -nP -C6 'annotate\([^)]*zorder' --type=py

Repository: reflex-dev/xy

Length of output: 151


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant implementation section with line numbers.
sed -n '3260,3335p' python/xy/pyplot/_axes.py

# Find annotate call sites, especially any with zorder.
rg -n -C 4 'annotate\(' python/xy/pyplot

Repository: reflex-dev/xy

Length of output: 8973


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file structure first, then inspect annotate-related symbols.
ast-grep outline python/xy/pyplot/_axes.py --view expanded || true
rg -n -C 3 '_zorder|annotation_entries|host\._entries\.sort|def annotate|class .*Axes' python/xy/pyplot/_axes.py

Repository: reflex-dev/xy

Length of output: 18141


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the relevant implementation slice with line numbers.
nl -ba python/xy/pyplot/_axes.py | sed -n '3285,3325p'

# Search for documentation or tests mentioning zorder on annotate.
rg -n -C 3 'annotate\(.*zorder|zorder=.*annotate|_zorder|annotation_entries' .

Repository: reflex-dev/xy

Length of output: 191


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find all places that mutate host._entries or apply z-order sorting.
rg -n -C 3 'host\._entries|_entries\.append|_entries\.extend|_entries\.insert|_entries\.sort|_zorder' python/xy/pyplot/_axes.py

# Show nearby context around the annotation code path.
nl -ba python/xy/pyplot/_axes.py | sed -n '3190,3335p'

Repository: reflex-dev/xy

Length of output: 5242


annotate(zorder=...) reorders only existing entries
Because host._entries is sorted only inside annotate, any artist added afterward keeps its append position even if its zorder should place it before the annotation. If this is meant to mirror Matplotlib, z-order needs to be applied when entries are added or at draw time.

🤖 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 3313 - 3317, Move z-order ordering
out of the existing-entry-only branch in annotate and into the entry-addition or
draw path used by the host axis. Ensure host._entries is reordered by each
entry’s _zorder whenever artists are added or rendered, so entries added after
annotate(zorder=...) are positioned correctly while preserving existing
annotation z-order behavior.

3555-3557: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Any path writing a log-axis domain without the set_*lim positivity guard?
rg -nP -C3 '\["domain"\]\s*=' python/xy/pyplot/_axes.py

Repository: reflex-dev/xy

Length of output: 7233


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## outline: _axes.py\n'
ast-grep outline python/xy/pyplot/_axes.py --view expanded | sed -n '1,220p'

printf '\n## lines around _aspect_coordinates and get_position\n'
sed -n '3530,3605p' python/xy/pyplot/_axes.py

printf '\n## lines around _build_chart call sites\n'
sed -n '6910,6975p' python/xy/pyplot/_axes.py

Repository: reflex-dev/xy

Length of output: 18626


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## more callers and domain setters\n'
sed -n '6975,7145p' python/xy/pyplot/_axes.py

Repository: reflex-dev/xy

Length of output: 9663


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## grep for get_position usage\n'
rg -n "get_position\(" python/xy/pyplot/_axes.py

Repository: reflex-dev/xy

Length of output: 548


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## any tests or specs around log aspect / domain guards\n'
rg -n "log aspect|log.*domain|aspect" python/xy -g '*test*' -g '*.py'

Repository: reflex-dev/xy

Length of output: 11613


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## _figure.py log-domain validation\n'
sed -n '240,380p' python/xy/_figure.py

printf '\n## aspect setup and current-bounds capture\n'
sed -n '4630,4675p' python/xy/pyplot/_axes.py

printf '\n## image / extent path around log handling\n'
sed -n '2670,2765p' python/xy/pyplot/_axes.py

Repository: reflex-dev/xy

Length of output: 13507


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## explicit domain writes in _axes.py that may skip validation\n'
sed -n '1790,1835p' python/xy/pyplot/_axes.py
sed -n '2960,2995p' python/xy/pyplot/_axes.py
sed -n '4318,4655p' python/xy/pyplot/_axes.py

Repository: reflex-dev/xy

Length of output: 17978


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## image domain handling in _plot_types.py\n'
sed -n '4040,4120p' python/xy/pyplot/_plot_types.py

printf '\n## extent handling in imshow / image helpers\n'
rg -n "_image_extent|extent|log_domain" python/xy/pyplot/_axes.py python/xy/pyplot/_plot_types.py

Repository: reflex-dev/xy

Length of output: 13626


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '4040,4120p' python/xy/pyplot/_plot_types.py

Repository: reflex-dev/xy

Length of output: 3731


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## image extent helpers\n'
rg -n "_image_extent|_mpl_domain|extent" python/xy/pyplot/_axes.py python/xy/pyplot/_plot_types.py | sed -n '1,120p'

Repository: reflex-dev/xy

Length of output: 9747


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## imshow implementation around extent/domain writes\n'
sed -n '2670,2775p' python/xy/pyplot/_axes.py

printf '\n## any direct use of _mpl_domain in image code\n'
rg -n "_mpl_domain|log_domain|image.*domain|extent" python/xy/pyplot/_axes.py python/xy/pyplot/_plot_types.py

Repository: reflex-dev/xy

Length of output: 14858


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## _image_extent helper and nearby image logic\n'
rg -n "_image_extent|_mpl_domain|log_domain" python/xy/pyplot/_axes.py python/xy/pyplot/_plot_types.py
sed -n '3850,3915p' python/xy/pyplot/_axes.py
sed -n '4048,4118p' python/xy/pyplot/_plot_types.py

Repository: reflex-dev/xy

Length of output: 7314


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "_aspect_bounds\s*=" python/xy/pyplot/_axes.py python/xy/pyplot/_artists.py python/xy/pyplot/_plot_types.py

Repository: reflex-dev/xy

Length of output: 886


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## entry value / auto-domain logic\n'
sed -n '3710,4035p' python/xy/pyplot/_axes.py

printf '\n## autoscale/tight-domain callers around the domain write\n'
sed -n '4308,4345p' python/xy/pyplot/_axes.py

Repository: reflex-dev/xy

Length of output: 18699


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## axis() and square/equal handling\n'
sed -n '4047,4125p' python/xy/pyplot/_axes.py

printf '\n## set_aspect() current-bounds path\n'
sed -n '4159,4205p' python/xy/pyplot/_axes.py

Repository: reflex-dev/xy

Length of output: 5715


Guard _aspect_bounds before box-aspect layout _aspect_coordinates() will raise on nonpositive log bounds, so any log axis that stores raw extents into _aspect_bounds can fail later in get_position() instead of at assignment time. autoscale(tight=True) and image extents are the risky paths here.

🤖 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 3555 - 3557, Validate log-scale
bounds before storing raw extents in _aspect_bounds, covering both
autoscale(tight=True) and image-extent assignment paths. Reuse the positivity
and finiteness validation expected by _aspect_coordinates() so invalid bounds
raise at assignment time rather than later during get_position() box-aspect
layout.
tests/pyplot/test_gallery_hist_errorbar_compat.py (1)

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

Line 331's stroke check is tautological.

The cap entries never carry a stroke key, so cap["kwargs"].get("stroke", "red") always yields the hard-coded "red" default and the middle comparison can never fail — it would pass even if ecolor were ignored. Assert only what the entry actually stores (or add stroke to the cap kwargs if caps are meant to expose it).

💚 Tighten the assertion
-        assert cap["kwargs"]["color"] == cap["kwargs"].get("stroke", "red") == "red"
+        assert cap["kwargs"]["color"] == "red"
+        assert "stroke" not in cap["kwargs"]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    for cap in (horizontal, vertical):
        assert cap["_mpl_line_marker_path_points"] == 10.0
        assert cap["_mpl_line_marker_stroke_points"] == plt.rcParams["lines.markeredgewidth"]
        assert cap["kwargs"]["color"] == "red"
        assert "stroke" not in cap["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 `@tests/pyplot/test_gallery_hist_errorbar_compat.py` around lines 328 - 331,
Update the cap color assertion in the loop over horizontal and vertical caps so
it validates the actual stored color without relying on the hard-coded default
from kwargs.get("stroke", "red"). Either assert the entry’s existing color field
directly or explicitly populate stroke in the cap kwargs if that is the intended
contract.

@Alek99
Alek99 merged commit 1595eeb into agent/gallery-log-scale-compat Jul 27, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants