Skip to content

Polar zoom defaults to off; opt-in per chart with interaction_config - #390

Open
masenf wants to merge 6 commits into
mainfrom
claude/polar-graphs-zoom-defaults-pe7dhk
Open

Polar zoom defaults to off; opt-in per chart with interaction_config#390
masenf wants to merge 6 commits into
mainfrom
claude/polar-graphs-zoom-defaults-pe7dhk

Conversation

@masenf

@masenf masenf commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Polar charts now ship with zoom disabled by default, with wind_rose as the sole exception. This addresses the fundamental mismatch between zoom semantics and polar compositions: the center is a fixed point, so zooming a pie, radial bar, gauge, or radar crops the rim rather than navigating it. Leaving zoom off also releases the wheel to the page, so scrolling works normally over embedded charts.

Key Changes

Python API and defaults:

  • Figure._interaction_spec() resolves zoom to False under coords="polar" when unspecified, and ships the flag explicitly on the wire (breaking the usual "unspecified keys stay absent" rule because the client cannot re-derive it — Chart.kind never reaches the payload).
  • wind_rose() opts back in by shipping zoom=True in its interaction spec.
  • interaction_config(zoom=True) and the zoom= chart prop both override the default, allowing authors to enable zoom when the radius is a measured quantity.
  • Updated docstrings on interaction_config(), polar_chart(), and related factories to document the new default and escape hatch.

Client-side (TypeScript):

  • Modebar history and zoom menu are now gated on canZoom (not just canHistory), so a polar chart with zoom off shows no zoom controls, zoom percentage, or Back/Next history — because nothing can move the view.
  • Wheel handler returns before preventDefault() when zoom is off, leaving the event uncancelled so the page scrolls normally.

Tests and documentation:

  • New test test_polar_zoom_is_off_by_default_and_releases_page_scroll() verifies the three user-visible consequences: radial range unchanged, wheel event uncancelled, modebar zoom controls absent.
  • Updated test_polar_wheel_zoom_is_alive_and_keeps_r_lo_fixed() to explicitly opt into zoom with xy.interaction_config(zoom=True).
  • New parametrized test test_polar_ships_zoom_disabled_by_default() covers all polar chart factories.
  • New tests for wind_rose keeping zoom on and for interaction_config overriding the default.
  • Comprehensive documentation updates across polar chart guides, interaction docs, and API reference explaining why zoom is off by default, when to enable it, and how.

Implementation Details

The decision to resolve zoom in Python (rather than infer it in the renderers) is structural: a pie and a wind rose are identical figures to the client, so the choice must be on the wire. This is the one documented exception to the "unspecified keys stay absent" rule in pan-and-zoom-configuration.md §5.2.

Radial zoom always keeps r_min fixed, so an ordinary zoom never turns a disc into an annulus. Values above the zoomed r_max are culled in the shader, so zooming in on a chart whose interesting structure sits at the rim hides it; authors can reach for r_axis(type_="log") when a wide radial range needs to be readable at every scale.

https://claude.ai/code/session_01KkjqqbCGF1AEdsMiuRqRGM

Review in cubic

Summary by CodeRabbit

  • New Features

    • Polar charts now disable zoom by default; wind rose charts retain zoom enabled.
    • Zoom can be configured per chart through interaction settings.
    • Disabled polar zoom preserves page scrolling and hides zoom/history controls.
  • Bug Fixes

    • Unsupported polar drag-action settings are rejected during chart construction.
    • Navigation history controls appear only when supported interactions can record history.
  • Documentation

    • Updated polar chart and interaction guidance covering zoom, reset behavior, and limitations.

masenf and others added 2 commits July 31, 2026 00:51
The centre of a disc is a fixed point of the polar transform and radial
zoom pins the minimum, so zooming a composition whose radius is *layout*
rather than data — a pie or donut (the value is the angle, the radius a
constant rim), a radial bar, a gauge, a radar on a fixed frame — could
only crop the rim while the geometry stayed welded to the middle. That
reads as a broken chart, not as navigation.

`coords="polar"` now resolves `zoom` to `False` and ships the flag
explicitly. `wind_rose` is the exception the default is written around
and ships `zoom=True`: its radius is a frequency count, so pulling the
outer ring in magnifies the short sectors of a rose dominated by one
prevailing direction. Either side is overridable with
`xy.interaction_config(zoom=...)` or a `zoom=` chart prop, and the
gesture itself is unchanged once enabled.

The decision is made in Python rather than inferred by the renderers
because `Chart.kind` never reaches the wire: a pie and a wind rose are
the same figure to the client and want opposite answers. That makes this
the one documented exception to pan-and-zoom-configuration.md §5.2's
"unspecified keys stay absent" rule, and §28 wants the choice on the wire
anyway.

Two consequences are the point of the change rather than side effects.
The wheel handler returns before `preventDefault()`, so a page scrolls
normally over a pie or gauge instead of the chart swallowing the gesture.
And the modebar grows no zoom menu: `canRecordHistory` now gates view
history on the durable state actually being mutable, since an enabled
`history` flag alone was otherwise enough to build a "100%" trigger over
two permanently dead Back/Next items.

Specs, the polar overview's Hover and Zoom section (including a live
opt-in example), and every sibling polar guide that claimed radial zoom
was available are updated to match.
@cubic-dev-ai

cubic-dev-ai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

We've triggered an ultrareview automatically — This PR changes the default zoom behavior for all polar charts, affecting both the Python API and client-side interaction model across 20 files, which could break existing charts or cause unexpected behavior if a subtle bug is missed.. I'll post findings when complete.

An ultrareview is cubic's deepest review, catching hard-to-find bugs in the most critical PRs. It runs a longer, multi-pass analysis using cubic's most capable review models, and typically takes around 30 minutes. It consumes your team's reviewed-lines allowance at 3× the standard rate.

Automated ultrareviews are disabled by default. We triggered this run as part of your trial. Want cubic to do this for every high-risk PR? Enable auto-ultrareview in your settings.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Polar charts now disable zoom by default, while wind rose charts retain enabled zoom. Explicit chart or interaction settings override these defaults. Frontend controls, validation, documentation, and tests reflect the resolved behavior.

Changes

Polar zoom behavior

Layer / File(s) Summary
Resolve polar zoom defaults
python/xy/_figure.py, python/xy/components.py, tests/test_polar_charts.py
Polar payloads serialize zoom=False by default. Wind roses retain zoom=True. Explicit settings override both defaults. Polar drag actions accept only "auto" and "none".
Apply resolved zoom in the modebar
js/src/53_interaction.ts, tests/test_polar_client_regressions.py
History controls require recordable interaction capabilities. Disabled polar zoom leaves wheel events uncancelled and hides zoom-related controls.
Define polar interaction contracts
spec/api/*, spec/design/*, docs/core-concepts/interactions.md, docs/components/modebars-and-interaction-controls.md
Specifications document radial zoom, reset behavior, fixed radial limits, page scrolling, overrides, and unsupported polar interactions.
Document and validate chart behavior
docs/charts/*, docs/api-reference/*, docs/guides/display-and-export.md, docs/app/tests/test_docs_site.py, tests/reflex_adapter/test_assets.py, CHANGELOG.md
Chart documentation, stale-content checks, bundle export checks, export guidance, and changelog entries describe or validate the updated behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ChartFactory
  participant PythonInteraction
  participant PolarPayload
  participant BrowserInteraction
  participant Modebar
  ChartFactory->>PythonInteraction: resolve polar zoom setting
  PythonInteraction->>PolarPayload: serialize zoom capability
  PolarPayload->>BrowserInteraction: provide interaction payload
  BrowserInteraction->>Modebar: create controls for enabled capabilities
  BrowserInteraction-->>ChartFactory: apply radial zoom or preserve page scrolling
Loading

Possibly related issues

  • reflex-dev/xy#393: Updates the bundle asset test to validate exports from minified bundles.

Possibly related PRs

Suggested reviewers: carlosabadia

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: polar zoom is disabled by default and can be enabled per chart with interaction_config.
Docstring Coverage ✅ Passed Docstring coverage is 82.61% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/polar-graphs-zoom-defaults-pe7dhk

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

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

🤖 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 `@docs/charts/polar-chart.md`:
- Around line 251-256: Update the polar-chart documentation to reflect the
resolved reset policy: with zoom disabled, polar charts must not describe reset
controls or double-click axis restoration as available; retain the documented
zoom-enabled behavior only if it matches the implementation, including wind-rose
exceptions.

In `@docs/charts/wind-rose.md`:
- Around line 119-125: Update the wind rose documentation to remove the claim
that other polar chart radii are layout-only. Keep the statement that other
polar chart types default to zoom off, and direct authors to use
xy.interaction_config(zoom=True) when their radial values are meaningful and
worth magnifying.

In `@js/src/53_interaction.ts`:
- Around line 1277-1279: Update the history eligibility logic around
canRecordHistory and canHistory so selection history remains available when
navigation is disabled; do not require canNavigate for selection-only
configurations, while preserving navigation and zoom gating behavior. Add a
regression test covering navigation=False with selection enabled, then run the
relevant core, JavaScript build, ABI, rendering, smoke, test, lint, formatting,
and type-check commands, excluding native-core fallback tests.

In `@python/xy/_figure.py`:
- Around line 1724-1740: Update the polar interaction validation around
_validate_interaction() so default_drag_action="zoom" is checked against the
resolved zoom value after polar defaults are applied, rejecting the incompatible
false combination while preserving wind_rose’s explicit zoom=True behavior. Add
a regression test that confirms the invalid configuration raises during
construction.
🪄 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: ee50ab5b-4582-4afb-b15d-2ecb17e7c9ff

📥 Commits

Reviewing files that changed from the base of the PR and between d505ef5 and 277bdda.

📒 Files selected for processing (20)
  • CHANGELOG.md
  • docs/api-reference/limitations-and-alpha-status.md
  • docs/app/tests/test_docs_site.py
  • docs/charts/pie-chart.md
  • docs/charts/polar-chart.md
  • docs/charts/radar-chart.md
  • docs/charts/radial-bar-chart.md
  • docs/charts/wind-rose.md
  • docs/components/modebars-and-interaction-controls.md
  • docs/core-concepts/interactions.md
  • docs/guides/display-and-export.md
  • js/src/53_interaction.ts
  • python/xy/_figure.py
  • python/xy/components.py
  • spec/api/chart-kind-contract.md
  • spec/api/interaction.md
  • spec/design/pan-and-zoom-configuration.md
  • spec/design/polar-axes.md
  • tests/test_polar_charts.py
  • tests/test_polar_client_regressions.py

Comment thread docs/charts/polar-chart.md Outdated
Comment thread docs/charts/wind-rose.md
Comment thread js/src/53_interaction.ts
Comment on lines +1277 to +1279
const canRecordHistory = canPan || canSelect
|| (canZoom && this._axisPolicy("zoom_axes").length > 0);
const canHistory = canNavigate && this._historyEnabled() && canRecordHistory;

@coderabbitai coderabbitai Bot Jul 31, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- candidate file outline ---'
ast-grep outline js/src/53_interaction.ts --match 'class $_' --view compact 2>/dev/null || true
printf '%s\n' '--- target implementation ---'
sed -n '1220,1310p' js/src/53_interaction.ts
printf '%s\n' '--- navigation/history references ---'
rg -n -C 3 'canNavigate|canRecordHistory|canHistory|_historyEnabled|history|navigation' js/src/53_interaction.ts js/src js/test tests 2>/dev/null | head -n 500
printf '%s\n' '--- relevant files ---'
git ls-files | rg '(^|/)(53_interaction|interaction|history|navigation|select|brush|render|test)' | head -n 300

Repository: reflex-dev/xy

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- target implementation ---'
sed -n '1220,1310p' js/src/53_interaction.ts
printf '%s\n' '--- relevant symbols and tests ---'
rg -n -C 4 'canNavigate|canRecordHistory|canHistory|_historyEnabled|history|navigation' js/src js/test tests 2>/dev/null | head -n 500
printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(53_interaction|interaction|history|navigation|select|brush|render|test)' | head -n 300

Repository: reflex-dev/xy

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- interaction contract and history documentation ---'
rg -n -C 5 'navigation|history|selection.*history|history.*selection|brush.*history' \
  spec docs python tests --glob '!tests/pyplot/**' --glob '!tests/**/corpus/**' | head -n 350
printf '%s\n' '--- focused test filenames ---'
git ls-files tests js | rg -i '(toolbar|interaction|view.?state|history|select|brush|modebar)'
printf '%s\n' '--- focused test references ---'
rg -n -C 5 'history|navigation|modebar|selection' tests/test_toolbar_controls.py tests/test_interaction_probe_retries.py tests/test_modebar_select_drill.py tests/test_boxzoom_tool_keeps_wheel_and_reset.py tests/reflex_adapter/test_view_state_push.py 2>/dev/null | head -n 400
printf '%s\n' '--- interaction flag definitions/defaults ---'
rg -n -C 6 '_interactionFlag|INTERACTION|navigation.*true|history.*true' js/src python/xy spec/api/interaction.md | head -n 400

Repository: reflex-dev/xy

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- interaction contract and history documentation ---'
rg -n -C 5 'navigation|history|selection.*history|history.*selection|brush.*history' \
  spec docs python tests --glob '!tests/pyplot/**' --glob '!tests/**/corpus/**' | head -n 350
printf '%s\n' '--- focused test filenames ---'
git ls-files tests js | rg -i '(toolbar|interaction|view.?state|history|select|brush|modebar)'
printf '%s\n' '--- focused test references ---'
rg -n -C 5 'history|navigation|modebar|selection' \
  tests/test_toolbar_controls.py tests/test_interaction_probe_retries.py \
  tests/test_modebar_select_drill.py tests/test_boxzoom_tool_keeps_wheel_and_reset.py \
  tests/reflex_adapter/test_view_state_push.py 2>/dev/null | head -n 400
printf '%s\n' '--- interaction flag definitions/defaults ---'
rg -n -C 6 '_interactionFlag|INTERACTION|navigation.*true|history.*true' \
  js/src python/xy spec/api/interaction.md | head -n 400

Repository: reflex-dev/xy

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- focused contract excerpts ---'
sed -n '45,65p' spec/api/interaction.md
sed -n '286,302p' spec/api/interaction.md
sed -n '344,354p' spec/api/interaction.md
printf '%s\n' '--- focused view-state contract ---'
rg -n -C 8 'history|selection|navigation' spec/design/view-state.md | head -n 220
printf '%s\n' '--- standalone invariant verifier ---'
python3 - <<'PY'
from pathlib import Path
import re

src = Path("js/src/53_interaction.ts").read_text()
viewstate = Path("js/src/57_viewstate.ts").read_text()
contract = Path("spec/api/interaction.md").read_text()

assert re.search(
    r'const canSelect = this\._interactionFlag\("brush", true\)\s*'
    r'&& this\._interactionFlag\("select", true\);',
    src,
)
assert re.search(
    r'const canRecordHistory = canPan \|\| canSelect\s*'
    r'\|\| \(canZoom && this\._axisPolicy\("zoom_axes"\)\.length > 0\);',
    src,
)
history_line = re.search(r'const canHistory = ([^;]+);', src).group(1)
assert "this._historyEnabled()" in history_line
assert "canRecordHistory" in history_line
assert "canNavigate" in history_line

# Evaluate the relevant capability matrix using the implementation's boolean
# structure, without importing or executing repository code.
def current_can_history(navigation, select, history, pan=False, zoom=False, zoom_axes=False):
    can_pan = navigation and pan
    can_zoom = navigation and zoom
    can_record = can_pan or select or (can_zoom and zoom_axes)
    return navigation and history and can_record

def intended_can_history(navigation, select, history, pan=False, zoom=False, zoom_axes=False):
    can_pan = navigation and pan
    can_zoom = navigation and zoom
    can_record = can_pan or select or (can_zoom and zoom_axes)
    return history and can_record

case = dict(navigation=False, select=True, history=True)
print("case:", case)
print("current:", current_can_history(**case))
print("selection-independent:", intended_can_history(**case))
assert current_can_history(**case) is False
assert intended_can_history(**case) is True

# Selection itself is explicitly independent of navigation in the documented
# gesture contract, while history is documented as a durable-state feature.
assert "`navigation` | on | Master gate" in contract
assert "`brush`, `select`, `_pickable`" in contract
assert "history" in contract
assert "this._historyEnabled()" in viewstate
assert "if (!this._historyPast || !this._historyEnabled()" in viewstate
print("invariant: navigation suppresses the history UI despite selectable durable state")
PY

Repository: reflex-dev/xy

Length of output: 19038


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
import re

src = Path("js/src/53_interaction.ts").read_text()
viewstate = Path("js/src/57_viewstate.ts").read_text()
contract = Path("spec/api/interaction.md").read_text()

assert re.search(
    r'const canSelect = this\._interactionFlag\("brush", true\)\s*'
    r'&& this\._interactionFlag\("select", true\);',
    src,
)
assert re.search(
    r'const canRecordHistory = canPan \|\| canSelect\s*'
    r'\|\| \(canZoom && this\._axisPolicy\("zoom_axes"\)\.length > 0\);',
    src,
)
history_line = re.search(r'const canHistory = ([^;]+);', src).group(1)
assert "this._historyEnabled()" in history_line
assert "canRecordHistory" in history_line
assert "canNavigate" in history_line

def current(navigation, select, history, pan=False, zoom=False, zoom_axes=False):
    can_pan = navigation and pan
    can_zoom = navigation and zoom
    can_record = can_pan or select or (can_zoom and zoom_axes)
    return navigation and history and can_record

def intended(navigation, select, history, pan=False, zoom=False, zoom_axes=False):
    can_pan = navigation and pan
    can_zoom = navigation and zoom
    can_record = can_pan or select or (can_zoom and zoom_axes)
    return history and can_record

case = dict(navigation=False, select=True, history=True)
print("case:", case)
print("current:", current(**case))
print("selection-independent:", intended(**case))
assert current(**case) is False
assert intended(**case) is True

assert "this._historyEnabled()" in viewstate
assert "if (!this._historyPast || !this._historyEnabled()" in viewstate
assert "`brush`, `select`, `_pickable`" in contract
print("invariant: navigation suppresses the history UI despite selectable durable state")
PY

Repository: reflex-dev/xy

Length of output: 336


Do not gate selection history on navigation.

navigation=False still permits selection, so this gate hides Back/Next for selection-only history configurations.

Fix
-    const canHistory = canNavigate && this._historyEnabled() && canRecordHistory;
+    const canHistory = this._historyEnabled() && canRecordHistory;

Add a regression test and run the relevant core, JavaScript build, ABI, rendering, smoke, test, lint, formatting, and type-check commands without native-core fallback tests.

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

Suggested change
const canRecordHistory = canPan || canSelect
|| (canZoom && this._axisPolicy("zoom_axes").length > 0);
const canHistory = canNavigate && this._historyEnabled() && canRecordHistory;
const canRecordHistory = canPan || canSelect
|| (canZoom && this._axisPolicy("zoom_axes").length > 0);
const canHistory = this._historyEnabled() && canRecordHistory;
🤖 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/53_interaction.ts` around lines 1277 - 1279, Update the history
eligibility logic around canRecordHistory and canHistory so selection history
remains available when navigation is disabled; do not require canNavigate for
selection-only configurations, while preserving navigation and zoom gating
behavior. Add a regression test covering navigation=False with selection
enabled, then run the relevant core, JavaScript build, ABI, rendering, smoke,
test, lint, formatting, and type-check commands, excluding native-core fallback
tests.

Source: Coding guidelines

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Skipping — the canNavigate term is pre-existing and unchanged by this PR. Before: const canHistory = canNavigate && this._historyEnabled();. My edit only appended && canRecordHistory, which narrows further; it did not introduce the navigation gate.

The underlying observation is fair (§5.3 does say navigation=False leaves selection working, so selection-only history could in principle record), but dropping canNavigate here has a consequence beyond this PR's scope: history lives inside the zoom menu, whose trigger renders a "100%" zoom-percentage label. Removing the gate would put a zoom indicator on a chart that cannot zoom — the same dead-affordance problem canRecordHistory exists to remove, just inverted. Fixing it properly means deciding whether view history belongs in the zoom menu at all for a navigation-disabled chart, which is a separate change to non-polar behavior.

Filing it as a follow-up rather than folding it in here.


Generated by Claude Code

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@masenf Understood. The canNavigate gate predates this PR. This PR only adds canRecordHistory to prevent empty history controls for polar charts with zoom disabled.

Removing canNavigate would require a separate design for selection-only history controls. The current zoom-menu trigger would otherwise show a misleading zoom percentage. I will not pursue this finding in this PR.

🐇


✏️ Learnings added
Learnt from: masenf
Repo: reflex-dev/xy PR: 390
File: js/src/53_interaction.ts:1277-1279
Timestamp: 2026-07-31T17:25:42.743Z
Learning: In `js/src/53_interaction.ts`, `canHistory` is intentionally gated by `canNavigate` because Back and Next controls are currently hosted in the zoom menu, whose trigger displays a zoom-percentage label. Supporting selection-only history when `navigation=False` requires a separate UI design for history controls and is outside polar zoom changes.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

Comment thread python/xy/_figure.py Outdated
@codspeed-hq

codspeed-hq Bot commented Jul 31, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 109 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing claude/polar-graphs-zoom-defaults-pe7dhk (e4e1d8a) with main (a2f2976)

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ultrareview completed in 10m 37s

1 issue found across 20 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="python/xy/components.py">

<violation number="1" location="python/xy/components.py:6865">
P2: Default wind roses expose Box Zoom and resolve plain drag to box zoom because this enables the generic `zoom` capability while `box_zoom` still defaults to true. Box rectangles are explicitly unsupported for polar charts, so the wind-rose exception should retain radial wheel/button zoom without restoring that unsupported drag mode.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread python/xy/_figure.py Outdated
Comment thread python/xy/components.py Outdated
Comment thread python/xy/components.py Outdated
# sectors of a rose dominated by one prevailing direction). `setdefault`, so
# an author's own `zoom=` — or an `xy.interaction_config(zoom=False)` child,
# which is applied after chart props — still wins.
props.setdefault("zoom", True)

@cubic-dev-ai cubic-dev-ai Bot Jul 31, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Default wind roses expose Box Zoom and resolve plain drag to box zoom because this enables the generic zoom capability while box_zoom still defaults to true. Box rectangles are explicitly unsupported for polar charts, so the wind-rose exception should retain radial wheel/button zoom without restoring that unsupported drag mode.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At python/xy/components.py, line 6865:

<comment>Default wind roses expose Box Zoom and resolve plain drag to box zoom because this enables the generic `zoom` capability while `box_zoom` still defaults to true. Box rectangles are explicitly unsupported for polar charts, so the wind-rose exception should retain radial wheel/button zoom without restoring that unsupported drag mode.</comment>

<file context>
@@ -6842,6 +6853,16 @@ def wind_rose(
+    # sectors of a rose dominated by one prevailing direction). `setdefault`, so
+    # an author's own `zoom=` — or an `xy.interaction_config(zoom=False)` child,
+    # which is applied after chart props — still wins.
+    props.setdefault("zoom", True)
     return Chart("wind_rose", children, **props)
 
</file context>
Suggested change
props.setdefault("zoom", True)
props.setdefault("zoom", True)
props.setdefault("box_zoom", False)
Fix with cubic

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not reproducible — the premise doesn't hold. _interactionFlag in 50_chartview.ts forces box_zoom, select, brush, and crosshair to false under coords="polar" regardless of the flag values, so enabling the generic zoom capability cannot restore box zoom. I probed a default wind rose in headless Chromium:

boxZoomFlag: false, boxZoomItem: false, panButton: false, dragMode: "none",
zoomFlag: true, zoomAxes: ["y"], zoomIn: true, resetItem: true

No Box Zoom item, no Pan button, and plain drag resolves to nonedragMode === "zoom" also can't engage because that branch requires canBoxZoom. Wind roses already had zoom effectively on before this PR, with the same result. props.setdefault("box_zoom", False) would be dead config.

Locked in as test_wind_rose_keeps_radial_zoom_without_restoring_box_zoom in tests/test_polar_client_regressions.py (b844f51), which asserts exactly this split: radial zoom + reset granted, every rectangle gesture withheld.


Generated by Claude Code

Comment thread docs/charts/wind-rose.md Outdated
Comment thread docs/core-concepts/interactions.md Outdated
Comment thread docs/api-reference/limitations-and-alpha-status.md Outdated
Comment thread docs/guides/display-and-export.md Outdated
@coderabbitai coderabbitai Bot mentioned this pull request Jul 31, 2026
Validation and the payload disagreed about the polar zoom default:
`_validate_interaction` read an absent `zoom` as enabled while
`_interaction_spec` resolved it to False, so
`polar_chart(..., default_drag_action="zoom")` passed construction and
then shipped the self-contradicting
`{"zoom": false, "default_drag_action": "zoom"}`. Both now go through one
`Figure._zoom_enabled` predicate, so that combination raises at
construction. Two neighbouring holes are older than this rule and are left
alone, recorded in polar-axes.md §8: with zoom explicitly True, polar
still accepts `default_drag_action="zoom"`, and it accepts `"pan"` in any
case, though the client forces `box_zoom` off and `pan_axes` to `[]` for
polar.

`wind_rose(..., zoom=None)` disabled zoom instead of keeping the rose's
default. `None` means "unset" everywhere else in the interaction API, so
a wrapper forwarding an `Optional[bool]` turned zoom off by passing the
value that means "no opinion". Only a real bool now overrides.

Docs: reset follows the zoom capability, since the resolved reset-axis
policy is pan axes union zoom axes and both are empty under polar with
zoom off — Fit Data / Reset View do not render and double-click restores
nothing. Every polar guide that promised reset as part of the default
contract is corrected. The wind rose page no longer says other polar
radii are "layout rather than data", which contradicted the overview's own
opt-in example (a measured `polar_chart` radius is the expected opt-in
case); the rationale is the fixed centre, not the data. The
limitations page names the `zoom=` chart prop alongside
`interaction_config`, and the export guide names the wind-rose exception.

Tests: the resolved-zoom refusal, `zoom=None` on a rose, and a browser
probe asserting the wind-rose exception grants radial zoom and reset
WITHOUT restoring box zoom, the Pan button, selection, or a `zoom` drag
mode — the client forces every rectangle gesture off under polar
regardless of the flag, so the generic `zoom` capability cannot leak them
back. Docs contract tests pin the corrected claims and guard the removed
ones.

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

🤖 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 `@docs/charts/polar-chart.md`:
- Around line 251-257: Qualify the default-only reset and zoom claims in
docs/charts/polar-chart.md (251-257), docs/charts/pie-chart.md (676-683),
docs/charts/radar-chart.md (138-147), and
docs/components/modebars-and-interaction-controls.md (26-36) to state that an
explicit reset_axes policy still enables Reset View and double-click reset when
zoom is off. Update spec/design/polar-axes.md (577-581) to state that the
resolved reset policy is empty only when reset_axes is not supplied.

In `@spec/design/polar-axes.md`:
- Around line 530-538: Update polar configuration validation to reject both
default_drag_action values "pan" and "zoom" whenever coords is "polar",
including when zoom is explicitly true. Keep validation for other coordinate
systems unchanged and align the accepted actions with the inert polar pan_axes
and box_zoom behavior described near Figure._zoom_enabled.
🪄 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: d6fdf314-86bd-47c1-bb46-6da31fc5d8f6

📥 Commits

Reviewing files that changed from the base of the PR and between 277bdda and b844f51.

📒 Files selected for processing (16)
  • CHANGELOG.md
  • docs/api-reference/limitations-and-alpha-status.md
  • docs/app/tests/test_docs_site.py
  • docs/charts/pie-chart.md
  • docs/charts/polar-chart.md
  • docs/charts/radar-chart.md
  • docs/charts/radial-bar-chart.md
  • docs/charts/wind-rose.md
  • docs/components/modebars-and-interaction-controls.md
  • docs/core-concepts/interactions.md
  • docs/guides/display-and-export.md
  • python/xy/_figure.py
  • python/xy/components.py
  • spec/design/polar-axes.md
  • tests/test_polar_charts.py
  • tests/test_polar_client_regressions.py
🚧 Files skipped from review as they are similar to previous changes (8)
  • docs/guides/display-and-export.md
  • tests/test_polar_client_regressions.py
  • CHANGELOG.md
  • docs/api-reference/limitations-and-alpha-status.md
  • docs/charts/radial-bar-chart.md
  • tests/test_polar_charts.py
  • docs/app/tests/test_docs_site.py
  • python/xy/components.py

Comment thread docs/charts/polar-chart.md Outdated
Comment thread spec/design/polar-axes.md Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 16 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread docs/charts/pie-chart.md Outdated
Comment thread docs/api-reference/limitations-and-alpha-status.md Outdated
Comment thread docs/components/modebars-and-interaction-controls.md Outdated
Comment thread docs/core-concepts/interactions.md Outdated
`default_drag_action` now accepts only "auto"/"none" under coords="polar".
A disc has no drag tools at all — the client resolves `pan_axes` to `[]`
and forces `box_zoom`/`select`/`brush` off whatever the flags say — so
"pan", "zoom", and the four `select*` values were each accepted and then
resolved to no usable tool, including with `zoom=True`, since radial zoom
is a wheel/button gesture rather than a drag. This closes the family the
previous commit narrowed to `zoom` alone and left recorded as a known
hole.

Reset is not unconditionally absent, which four pages and the spec
claimed. `_resetAxisPolicy` returns an authored `reset_axes` verbatim
instead of deriving pan-axes union zoom-axes, so Fit Data / Reset View
render and double-click restores those axes even with zoom off — verified
in Chromium. The hatch is not vestigial: a polar view can still move
through linked axes or application-driven updates, which `navigation`
does not gate, so an authored `reset_axes` is the right way to offer reset
on a gesture-free chart. Every default-only claim is qualified.

Two smaller doc corrections. The interactions page said a radial bar's or
radar's radius is not the value — it is; the reason zoom is a bad default
there is that the extent is read against an authored rim or shared domain
that zoom clips away, so the rationale is reframed around the fixed frame
rather than around the radius not being data. And the pie page told
readers `zoom=True` restores the reset controls, when those examples also
pass `xy.modebar(show=False)`; it now separates restoring double-click
from unhiding the modebar.

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

🤖 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 `@docs/charts/pie-chart.md`:
- Around line 680-683: Clarify the interaction guidance in
docs/charts/pie-chart.md lines 680-683 and docs/charts/radial-bar-chart.md lines
685-689: state that double-click reset is inert only when reset_axes is absent,
and document that explicitly authoring reset_axes restores reset behavior in
both guides.
🪄 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: ba96b030-8042-4ab9-b05a-22e0552b1130

📥 Commits

Reviewing files that changed from the base of the PR and between b844f51 and b35511f.

📒 Files selected for processing (12)
  • CHANGELOG.md
  • docs/api-reference/limitations-and-alpha-status.md
  • docs/app/tests/test_docs_site.py
  • docs/charts/pie-chart.md
  • docs/charts/polar-chart.md
  • docs/charts/radar-chart.md
  • docs/charts/radial-bar-chart.md
  • docs/components/modebars-and-interaction-controls.md
  • docs/core-concepts/interactions.md
  • python/xy/_figure.py
  • spec/design/polar-axes.md
  • tests/test_polar_charts.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • docs/api-reference/limitations-and-alpha-status.md
  • CHANGELOG.md
  • docs/charts/radar-chart.md
  • docs/app/tests/test_docs_site.py
  • docs/charts/polar-chart.md

Comment thread docs/charts/pie-chart.md Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 12 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

Comment thread python/xy/_figure.py Outdated
Comment thread docs/core-concepts/interactions.md Outdated
Comment thread tests/test_polar_charts.py Outdated
masenf added 2 commits July 31, 2026 20:17
…tion list

Four small corrections from review.

The polar drag-action diagnostic prescribed `interaction_config(zoom=True)`
as the remedy, which is inapplicable on the two paths where zoom is already
enabled (`wind_rose`, or an explicit `zoom=True`) — and enabling zoom never
makes a drag action valid anyway, since radial zoom is a wheel/button
gesture. It now describes zoom as a separate capability instead of
prescribing an action.

`test_polar_refuses_every_inert_drag_action` hand-listed the refused
actions and had already drifted: it omitted `select-y`. The list is now
derived from the public `DefaultDragAction` type minus `auto`/`none`, with
a guard test pinning the derivation, so a newly added drag action is
covered the moment it exists.

Three guides stated that double-click reset is inert on a zoom-off polar
chart without scoping it to the case the claim actually holds for. An
authored `reset_axes` grants reset — controls and double-click alike —
whatever the zoom switch says, and the interactions page contradicted
itself by noting that exception two sentences later. Every statement is now
scoped to the derived/default policy.
`test_client_source_is_the_installed_bundle` looked for `class ChartView`,
`function decodeFrame(` and `function renderStandalone(` in
`python/xy/static/index.js`. `node js/build.mjs` minifies that bundle (§33),
so those identifiers are mangled and the markers could only ever match an
unminified build — the test failed on any correctly built checkout.

It now asserts the bundle exports every name `XYChart.jsx` imports from
`./xy_client.js`, with the import list read out of the wrapper rather than
duplicated, so the adapter and the client cannot drift apart silently. That
is also what this test is actually for: export aliases are the one part of
the bundle minification preserves, and they are precisely the surface the
wrapper binds to.

The export names are parsed from the `export{...}` clause instead of
substring-matched. `"as render" in text` is satisfied by
`as renderStandalone`, so dropping the anywidget `render` entry point would
have gone unnoticed.

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

🧹 Nitpick comments (1)
tests/reflex_adapter/test_assets.py (1)

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

Parse aliased imports before comparing export names.

Although the current import is unaliased, valid ESM such as renderStandalone as render would be stored as "renderStandalone as render". That value cannot match the exported source name. Parse each import entry and compare the identifier before as.

Proposed fix
-    imported = {name.strip() for name in clause.group(1).split(",") if name.strip()}
+    imported = {
+        entry.strip().rsplit(" as ", 1)[0].strip()
+        for entry in clause.group(1).split(",")
+        if entry.strip()
+    }
🤖 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/reflex_adapter/test_assets.py` around lines 55 - 61, Update the import
parsing in the XYChart.jsx validation around the imported set so aliased ESM
entries are normalized to their source identifier before comparison. For each
comma-separated import, remove the optional “as” alias and retain the name
before it, while preserving existing handling for unaliased imports and the
empty-import assertion.
🤖 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 `@tests/reflex_adapter/test_assets.py`:
- Around line 58-62: Update the required export set in the test around imported
and exported names to explicitly include the anywidget render entry point, while
preserving the existing JSX import validation. Ensure the missing-export
assertion fails when render is absent from the client bundle.

---

Nitpick comments:
In `@tests/reflex_adapter/test_assets.py`:
- Around line 55-61: Update the import parsing in the XYChart.jsx validation
around the imported set so aliased ESM entries are normalized to their source
identifier before comparison. For each comma-separated import, remove the
optional “as” alias and retain the name before it, while preserving existing
handling for unaliased imports and the empty-import assertion.
🪄 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: 0ca0a702-2d1c-45a8-87c1-61dd7132b11b

📥 Commits

Reviewing files that changed from the base of the PR and between 6ad1032 and e4e1d8a.

📒 Files selected for processing (1)
  • tests/reflex_adapter/test_assets.py

Comment on lines +58 to +62
imported = {name.strip() for name in clause.group(1).split(",") if name.strip()}
assert imported, "XYChart.jsx imports no names from the client bundle"

missing = sorted(imported - exported)
assert not missing, f"client bundle does not export {missing}, which XYChart.jsx imports"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Assert the required anywidget render export.

The check derives required names only from the imports in python/reflex-xy/reflex_xy/assets/XYChart.jsx. Those imports contain ChartView, decodeFrame, and renderStandalone, but not render. A bundle can therefore lose the required anywidget entry point while this test still passes. Add render to the required export set.

Proposed fix
-    missing = sorted(imported - exported)
+    required = imported | {"render"}
+    missing = sorted(required - exported)
📝 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.

Suggested change
imported = {name.strip() for name in clause.group(1).split(",") if name.strip()}
assert imported, "XYChart.jsx imports no names from the client bundle"
missing = sorted(imported - exported)
assert not missing, f"client bundle does not export {missing}, which XYChart.jsx imports"
imported = {name.strip() for name in clause.group(1).split(",") if name.strip()}
assert imported, "XYChart.jsx imports no names from the client bundle"
required = imported | {"render"}
missing = sorted(required - exported)
assert not missing, f"client bundle does not export {missing}, which XYChart.jsx imports"
🤖 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/reflex_adapter/test_assets.py` around lines 58 - 62, Update the
required export set in the test around imported and exported names to explicitly
include the anywidget render entry point, while preserving the existing JSX
import validation. Ensure the missing-export assertion fails when render is
absent from the client bundle.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant