Skip to content

Center resolved Y-axis titles in the browser [codex bughunt] - #356

Merged
sselvakumaran merged 3 commits into
agent/bughunt-axes-helper-parityfrom
agent/bughunt-y-axis-label-centering
Jul 28, 2026
Merged

Center resolved Y-axis titles in the browser [codex bughunt]#356
sselvakumaran merged 3 commits into
agent/bughunt-axes-helper-parityfrom
agent/bughunt-y-axis-label-centering

Conversation

@sselvakumaran

@sselvakumaran sselvakumaran commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • restore centered browser placement for resolved Y-axis titles using translate(-50%, -50%) rotate(...) and a centered transform origin
  • apply the same path to primary and named/secondary Y axes on either side
  • position outside titles from the union of the relevant spine and tick-label bounds, while respecting explicit label rotation and label padding
  • reserve the rotation-projected title extent in the gutter so the edge clamp cannot consume authored label padding
  • batch all Y-title geometry reads before applying position writes on multi-axis charts
  • keep structured label_position CSS authoritative

This is a general renderer fix for a consolidation regression, not a Cohere special case. It ports the relevant placement model from 8cd0c9c without cherry-picking the old commit wholesale. The static SVG renderer already resolves the longitudinal axis-label anchor correctly; the browser renderer now follows the same semantics.

Stack: this PR is layer 3 and targets agent/bughunt-axes-helper-parity (#355).

Focused coverage

  • default primary Y title centered at plot.y + plot.h / 2
  • named/secondary Y axes
  • left and right sides
  • label_position=start|center|end
  • explicit rotation and label padding
  • inward tick labels and tickless spine fallback
  • browser/static-SVG longitudinal placement consistency

Validation

  • uv run --with pre-commit pre-commit run --all-files
  • uv run ruff check .
  • uv run ruff format --check .
  • TypeScript typecheck and non-browser client build
  • focused static/SVG and layout regressions: 12 passed
  • batching-adjacent non-browser selection: 3 passed
  • git diff --check

The browser geometry tests run in CI, including the exact rotated-title labelpad case that previously failed. Local browser capture was intentionally avoided; no Chrome workaround was used.

Visual acceptance: Matplotlib Cohere

The Matplotlib Cohere gallery example is the acceptance target. Static XY already centers Coherence; this PR makes the interactive browser title use that same resolved position. Evidence images will be linked in a follow-up comment so they remain outside the product branch.

Summary by CodeRabbit

  • Bug Fixes
    • Improved Y-axis title sizing, placement, and rotation for both primary and secondary (named) axes.
    • Corrected title alignment relative to tick labels and axis spines on left and right sides, including consistent behavior for inward offset calculations.
    • Enhanced handling for secondary axes without ticks to ensure correct fallback spacing and positioning.
  • Tests
    • Added SVG- and browser-based UI regression coverage to verify Y-axis title geometry, offsets, and centered layout consistency across axes.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Y-axis title rendering now shares placement logic across primary and named axes, using measured tick geometry and fallback spacing. New SVG and browser regression tests validate centering, rotation, inward spacing, and tickless-axis behavior.

Changes

Y-axis title layout

Layer / File(s) Summary
Shared title rendering and tick geometry
js/src/50_chartview.ts
Primary and named Y-axis titles use deferred attachment, measured tick edges, angle-aware sizing, and label-offset or font-size fallback gaps.
SVG and browser geometry regression coverage
tests/test_ui_issue_regressions.py
Tests validate SVG and DOM title positions, rotations, inward tick spacing, and tickless secondary-axis fallback behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • reflex-dev/xy#340: Modifies related Y-axis title-to-tick attachment and placement behavior.

Suggested reviewers: alek99, farhanaliraza, adhami3310

🚥 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 describes the main browser-side Y-axis title centering fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/bughunt-y-axis-label-centering

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

Copy link
Copy Markdown
Contributor Author

Cohere visual acceptance

Matplotlib reference:

Matplotlib Cohere reference

XY static renderer target (already centered; the browser renderer now uses the same resolved Y-title anchor):

XY Cohere static centered target

This regression exists only in the interactive browser fallback, so I am not presenting the static image as a browser before/after. The focused browser geometry tests assert the actual fix for default primary placement, named secondary axes, both sides, start/center/end, explicit rotation and label padding, tickless axes, and SVG longitudinal consistency.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

The Comment image 1 (Matplotlib reference) shows two vertically stacked charts: a time-series plot from 0.00–2.00 seconds with the vertical Y-axis title “s1 and s2”, and a coherence plot over frequency with the vertical Y-axis title “Coherence” centered alongside the lower plot.

The Comment image 2 (XY static renderer target) shows the corresponding two charts with matching data, layout, and centered vertical Y-axis labels—especially “Coherence” on the lower panel—consistent with the stated static-renderer target.

@sselvakumaran Thanks for providing the visual acceptance evidence and clarifying that the regression is browser-only. I’ll review the implementation and focused coverage with that distinction in mind.

✅ Action performed

Review finished.

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

@sselvakumaran
sselvakumaran force-pushed the agent/bughunt-y-axis-label-centering branch from 8091a4b to a705bf1 Compare July 27, 2026 23:22
@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/bughunt-y-axis-label-centering (93aad1c) with agent/bughunt-axes-helper-parity (6de8b45)

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.

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

🧹 Nitpick comments (1)
js/src/50_chartview.ts (1)

5952-5995: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Batch tick/title measurement reads before writes across all y-axes.

Each renderYTitleattachYTitleToTicks call performs its own getBoundingClientRect() reads immediately followed by a title.style.left write, once per axis (primary + every named y-axis). Interleaving reads and writes like this across the extraYAxes loop forces the browser to re-flow between each axis's title placement instead of measuring all axes first and writing once. This runs on every _drawChrome() pass, including non-animated hover redraws (updateLabels is true whenever there's no active _viewAnim), so charts with several outside y-axis titles pay repeated layout thrashing on frequent interaction frames.

Consider splitting attachYTitleToTicks into a measure phase (collect {title, targetLeft} for every axis) and an apply phase (write all style.left values after all measurements complete) to keep reads and writes batched.

🤖 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 5952 - 5995, The y-axis title placement
currently interleaves getBoundingClientRect reads and style.left writes across
axes, causing repeated layout recalculation. Refactor attachYTitleToTicks and
the renderYTitle flow so each eligible title first collects its measured
targetLeft, including primary and extra y-axes, then applies all
title.style.left writes after measurement is complete; preserve structured
placement behavior where placement.style is non-null.
🤖 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.

Nitpick comments:
In `@js/src/50_chartview.ts`:
- Around line 5952-5995: The y-axis title placement currently interleaves
getBoundingClientRect reads and style.left writes across axes, causing repeated
layout recalculation. Refactor attachYTitleToTicks and the renderYTitle flow so
each eligible title first collects its measured targetLeft, including primary
and extra y-axes, then applies all title.style.left writes after measurement is
complete; preserve structured placement behavior where placement.style is
non-null.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2c24b6ff-f902-4c61-b1ea-c73cd1174533

📥 Commits

Reviewing files that changed from the base of the PR and between 06c7aed and 8091a4b.

📒 Files selected for processing (2)
  • js/src/50_chartview.ts
  • tests/test_ui_issue_regressions.py

@sselvakumaran
sselvakumaran force-pushed the agent/bughunt-y-axis-label-centering branch from a705bf1 to 7609729 Compare July 27, 2026 23:33
masenf
masenf previously approved these changes Jul 27, 2026
@sselvakumaran
sselvakumaran dismissed masenf’s stale review July 28, 2026 00:03

The merge-base changed after approval.

@sselvakumaran
sselvakumaran force-pushed the agent/bughunt-y-axis-label-centering branch from 7609729 to 93aad1c Compare July 28, 2026 00:05

Copy link
Copy Markdown
Contributor Author

Addressed the two remaining issues on the rebased head:

  • fixed the deterministic CI failure by reserving the rotation-projected horizontal extent of an explicitly angled Y-axis title, so the canvas clamp no longer consumes the requested 18 px labelpad;
  • addressed the review performance nitpick by rendering all eligible Y titles first, measuring every attachment in a read-only pass, then applying all style.left writes together. Structured label_position placement remains authoritative.

The existing failing assertion was intentionally kept unchanged. TypeScript, the non-browser client build, focused static/layout tests, full pre-commit, Ruff, formatting, and git diff --check pass locally. Browser geometry remains the fresh CI acceptance gate.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

@sselvakumaran Thanks for the detailed update. I’ll review the rebased head, with particular attention to the rotated-title labelpad reservation, batched layout writes, and browser-geometry coverage.

✅ Action performed

Review finished.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/test_ui_issue_regressions.py (1)

44-57: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate chart fixture between the two SVG/DOM parity tests.

The xy.chart(...) construction (4 lines, 4 y-axes, marks, sizing) is byte-for-byte identical between test_svg_y_axis_title_locations_center_primary_and_named_axes and test_y_axis_titles_center_and_match_svg_longitudinally_for_named_axes. Since these two tests exist specifically to cross-check that browser-rendered geometry matches the static SVG for the same configuration, keeping them in sync via a shared helper avoids silent drift if one copy is edited without the other.

♻️ Suggested extraction
+_NAMED_AXES_LABELS = {"primary default", "right start", "left center", "right end"}
+
+
+def _named_axes_chart() -> xy.Chart:
+    return xy.chart(
+        xy.line([0, 1], [0, 1]),
+        xy.line([0, 1], [1, 2], y_axis="y2"),
+        xy.line([0, 1], [2, 3], y_axis="y3"),
+        xy.line([0, 1], [3, 4], y_axis="y4"),
+        xy.x_axis(),
+        xy.y_axis(label="primary default", side="left"),
+        xy.y_axis(id="y2", label="right start", side="right", label_position="start"),
+        xy.y_axis(id="y3", label="left center", side="left", label_position="center"),
+        xy.y_axis(id="y4", label="right end", side="right", label_position="end"),
+        width=720,
+        height=440,
+        padding=(48, 120, 48, 120),
+    )

Then both tests replace their inline construction with chart = _named_axes_chart() and labels = _NAMED_AXES_LABELS.

Also applies to: 665-678

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

In `@tests/test_ui_issue_regressions.py` around lines 44 - 57, Extract the
identical xy.chart construction from
test_svg_y_axis_title_locations_center_primary_and_named_axes and
test_y_axis_titles_center_and_match_svg_longitudinally_for_named_axes into a
shared _named_axes_chart() helper, preserving all marks, axes, dimensions, and
padding. Define the shared _NAMED_AXES_LABELS value for the expected labels,
then update both tests to use chart = _named_axes_chart() and labels =
_NAMED_AXES_LABELS.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@js/src/50_chartview.ts`:
- Around line 5975-5990: Update the y-axis room calculation in _yAxisLeftRoom to
use the same computed title font-size source as the axis-title gap calculation,
rather than label_size for the 0.4 multiplier. Reuse the title element’s
getComputedStyle(...).fontSize value and preserve the existing fallback behavior
so reserved space and title positioning stay consistent when axis_title is
styled by CSS.

---

Nitpick comments:
In `@tests/test_ui_issue_regressions.py`:
- Around line 44-57: Extract the identical xy.chart construction from
test_svg_y_axis_title_locations_center_primary_and_named_axes and
test_y_axis_titles_center_and_match_svg_longitudinally_for_named_axes into a
shared _named_axes_chart() helper, preserving all marks, axes, dimensions, and
padding. Define the shared _NAMED_AXES_LABELS value for the expected labels,
then update both tests to use chart = _named_axes_chart() and labels =
_NAMED_AXES_LABELS.
🪄 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: 3f3b1104-a948-4158-ac5c-9e9c36b5a275

📥 Commits

Reviewing files that changed from the base of the PR and between 8091a4b and 93aad1c.

📒 Files selected for processing (2)
  • js/src/50_chartview.ts
  • tests/test_ui_issue_regressions.py

Comment thread js/src/50_chartview.ts
Comment on lines 5975 to +5990
const fontSize = parseFloat(getComputedStyle(title).fontSize) || 12;
const labelOffset = Number(axis.label_offset || 0);
const targetEdge = onRight
? Math.max(...tickRects.map((rect) => rect.right)) + 0.4 * fontSize + labelOffset
: Math.min(...tickRects.map((rect) => rect.left)) - 0.4 * fontSize - labelOffset;
const rawOffset = axis.label_offset;
const gap = rawOffset !== undefined
&& rawOffset !== null
&& Number.isFinite(Number(rawOffset))
? Number(rawOffset)
: 0.4 * fontSize;
// Matplotlib centers the title along the axis, then offsets it
// perpendicular to the union of the tick-label bounds and the
// corresponding spine. Including the spine keeps inward/negative-pad
// labels from pulling an outside title back into the plot.
const spineEdge = root.left + (onRight ? p.x + p.w : p.x);
const tickEdge = onRight
? Math.max(spineEdge, ...tickRects.map((rect) => rect.right))
: Math.min(spineEdge, ...tickRects.map((rect) => rect.left));
const targetEdge = onRight ? tickEdge + gap : tickEdge - gap;

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
# Locate the axis_title slot defaults and any font-size declarations that could
# diverge from the spec's label_size.
fd -e ts -e css . js/src | xargs rg -n -C3 'axis_title'
rg -n -C3 'label_size' js/src

Repository: reflex-dev/xy

Length of output: 6460


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the y-axis room reservation and title rendering paths together.
sed -n '690,790p' js/src/50_chartview.ts
printf '\n---\n'
sed -n '5960,6065p' js/src/50_chartview.ts
printf '\n---\n'
sed -n '175,190p' js/src/20_theme.ts

Repository: reflex-dev/xy

Length of output: 12346


Keep the reserved y-axis room on the same font-size source as the title gap. The attachment pass uses getComputedStyle(title).fontSize, but _yAxisLeftRoom still reserves 0.4 * label_size. Since axis_title can be overridden by CSS, those two can diverge and leave the title clamped tighter than authored.

🤖 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 5975 - 5990, Update the y-axis room
calculation in _yAxisLeftRoom to use the same computed title font-size source as
the axis-title gap calculation, rather than label_size for the 0.4 multiplier.
Reuse the title element’s getComputedStyle(...).fontSize value and preserve the
existing fallback behavior so reserved space and title positioning stay
consistent when axis_title is styled by CSS.

@sselvakumaran
sselvakumaran merged commit 93aad1c into main Jul 28, 2026
28 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