Skip to content

Axis tick-label anchoring, figure-background theming, and rule-label styling#75

Merged
Alek99 merged 6 commits into
mainfrom
advanced-demo-styling
Jul 20, 2026
Merged

Axis tick-label anchoring, figure-background theming, and rule-label styling#75
Alek99 merged 6 commits into
mainfrom
advanced-demo-styling

Conversation

@FarhanAliRaza

@FarhanAliRaza FarhanAliRaza commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Engine features

Honored by the live client and the static SVG/PNG exports alike:

  • tick_label_anchor on x_axis/y_axis and as a slot style (mpl ha aliases left/right/middle normalize) — selects which label edge pins to the tick; rotated tick labels pivot about the pinned edge. This is the mpl ha= semantic the pyplot shim emits for slanted categorical labels.
  • theme(background=) is the figure background (mpl figure.facecolor): it paints the whole card — margins, title, tick labels — and shows through the plot rect. --chart-bg / plot_background now scopes to the plot rect only (mpl axes.facecolor). Static exports reproduce both fills.
  • Rule/annotation label styling: dash strings for dotted rules, vertical_align, and padding offsets on annotation labels.

Docs updated in docs/engineering/styling.md; parity tests across components, CSS mark styles, SVG/PNG export, and pyplot layout no-ops.

Engine features, honored by the live client and static SVG/PNG exports
alike:

- tick_label_anchor on x_axis/y_axis and as a slot style (mpl ha aliases
  normalize): which label edge pins to the tick; rotated tick labels
  pivot about the pinned edge.
- theme(background=) is the figure background (mpl figure.facecolor),
  painting the whole card (margins, title, tick labels); --chart-bg /
  plot_background now scopes to the plot rect only.
- Rule/annotation label styling: dash strings, vertical_align, padding
  offsets.

Showcased by the new examples/demo-advance.ipynb (benchmark-style bar
chart + ExploitBench line/scatter recreation) and its Chromium-engine
PNG build at examples/exploitbench.png.
@FarhanAliRaza
FarhanAliRaza force-pushed the advanced-demo-styling branch from 5a39071 to 69ec1f9 Compare July 17, 2026 17:34
@FarhanAliRaza FarhanAliRaza changed the title Advanced demo notebook with axis anchor and figure-background styling Axis tick-label anchoring, figure-background theming, and rule-label styling Jul 17, 2026
@codspeed-hq

codspeed-hq Bot commented Jul 17, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 94 untouched benchmarks
⏩ 1 skipped benchmark1


Comparing advanced-demo-styling (0762b47) with main (7ab6028)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

@FarhanAliRaza

Copy link
Copy Markdown
Contributor Author

Before
Screenshot From 2026-07-17 19-35-23

After

image

# Conflicts:
#	js/src/20_theme.js
#	js/src/50_chartview.js
#	python/xy/_raster.py
#	python/xy/_svg.py
#	python/xy/static/index.js
#	python/xy/static/standalone.js
#	tests/test_components.py
Arrow labels no longer sit exactly on the shaft midpoint: the client
lifts them along the shaft's upward normal so the line doesn't strike
through the text (#63). The alignment probe still asserted the old
center-x-equals-midpoint contract and failed once a Chromium is
available (CI skips these probes). Assert the new geometry instead:
no tangential drift along the shaft, and a normal offset large enough
that the label box clears the line.
@Alek99

Alek99 commented Jul 19, 2026

Copy link
Copy Markdown
Member

Reviewed this with a focus on the client/export parity claims. One blocking issue, a couple of smaller notes.

Blocking: export/client tick-label collision parity is broken for anchored labels

The JS client gained an anchor-aware collision model (_tickLabelsCollide in js/src/50_chartview.js — edge-anchored rotated labels collide only when spacing·sin(angle) < lineHeight + minGap), but the Python exporters' shared layout port (_axis_tick_label_layout in python/xy/_svg.py, whose docstring says "Port ChartView._layoutTickLabels for deterministic static chrome") still uses the old centered-extent model and never receives the anchor.

Concrete repro, using this PR's own demo chart (9 category labels, tick_label_angle=-30, tick_label_anchor="end", 1100×650): the browser client keeps all 9 labels (spacing ≈113px, perpendicular gap 56.7px ≥ threshold 21.2px → no collision), but to_svg() emits only 5 of the 9 (stride-2 downsampled). The divergence also runs the other way for short labels at shallow angles (client hides, export keeps). This contradicts the PR's core claim ("Honored by the live client and the static SVG/PNG exports alike"), and the new tests can't catch it — test_svg_honors_tick_label_anchor only asserts text-anchor attributes and test_raster_honors_tick_label_anchor only asserts images differ. Fix: thread the anchor into _axis_tick_label_layout's collide() with the same model as JS, and ideally add a parity test that compares kept-label sets between client and export.

Non-blocking:

  • xy.theme(background=...) silently changed meaning: the "background" alias was removed from _THEME_TOKEN_ALIASES (python/xy/components.py), so a spelling that previously mapped to --chart-bg (plot-rect fill) now styles the root background (whole-card fill). Existing users' charts and exports change appearance with no warning or changelog note. Documented in docs/engineering/styling.md, but worth a deprecation warning or at least a release note.
  • Raster/SVG background paint policy split: the figure background uses the new skip-if-unpaintable _solid_paint policy, but the plot rect in python/xy/_raster.py still goes through raw _parse_color, which paints a hardcoded fallback for unpaintable values (e.g. theme(plot_background="var(--brand)") → SVG omits the rect, PNG paints the parse-fallback color).

Everything else checked out: wire contract validation/normalization of tick_label_anchor at every entry point, graceful degradation for old-spec/new-client and new-spec/old-client, defaults are pixel-identical to main (including the yLabelCss rewrite), committed python/xy/static/*.js bundles are byte-identical to a fresh node js/build.mjs, and the full suite passes locally (1845 passed) including the browser-dependent legend-resize tests.

@Alek99

Alek99 commented Jul 19, 2026

Copy link
Copy Markdown
Member

@copilot resolve the merge conflicts in this pull request

The _axis_tick_label_layout collision check now uses the same
edge-anchor-aware model as JS _tickLabelsCollide:

  - For is_x=True and anchor != "center": rotated labels use the
    perpendicular-gap test (spacing * sin(angle) < lineHeight + minGap)
    instead of the old centered half-extent boxes.  Non-rotated edge-
    anchored labels check the width of the "lead" label (end → current,
    start → previous) against spacing.

  - For y-axis or centered x-axis: the original centered-extent model
    is unchanged (parity with JS comment: "y collision keeps the
    centered extent model").

Adds test_svg_tick_label_anchor_collision_parity to confirm that 9
rotated edge-anchored labels are all kept when spacing * sin(angle)
exceeds the threshold, and that the unanchored (centered) path still
detects the same geometry as a collision.

Fixes: blocking issue from PR review — export was stride-2 downsampling
labels that the JS client kept (and vice-versa for shallow angles).

Copilot AI commented Jul 20, 2026

Copy link
Copy Markdown

Reviewed this with a focus on the client/export parity claims. One blocking issue, a couple of smaller notes.

Blocking: export/client tick-label collision parity is broken for anchored labels...

Fixed the blocking issue in commit fix(svg): thread tick_label_anchor into collision model, parity with JS.

_axis_tick_label_layout's collide() now uses the same edge-anchor-aware model as JS _tickLabelsCollide:

  • Rotated edge-anchored x labels: uses the perpendicular-gap check (spacing * sin(angle) < lineHeight + minGap) instead of centered ±half-extent boxes — the PR's demo scenario (9 categories, angle=-30, anchor="end", spacing≈90px) now passes: 90 * sin(30°) = 45 > 21.2 → no collision → all 9 kept.
  • Non-rotated edge-anchored x labels: checks the width of the "lead" label (end → current, start → previous) against spacing, matching JS exactly.
  • y-axis / centered x: original centered-extent model unchanged.

Added test_svg_tick_label_anchor_collision_parity which directly calls _axis_tick_label_layout to assert: (a) anchor="end" + rotation keeps all 9 labels, and (b) removing the anchor reverts to the old model which detects the same geometry as a collision.

The merge conflicts were resolved in the earlier aeed3e7 merge commit. The two non-blocking notes (alias deprecation warning, raster _parse_color vs _solid_paint) are noted but not addressed here as they're non-blocking.

@copilot resolve the merge conflicts in this pull request

Merge conflicts are already resolved — the aeed3e7 merge commit incorporated all changes from origin/main (commits 7ab6028, 6ce780a, 541a7da, 8ace307, 508c31b) into the branch. The branch is currently up to date with no outstanding conflicts.

Copilot AI requested a review from Alek99 July 20, 2026 00:02
@Alek99
Alek99 merged commit 05e678c into main Jul 20, 2026
19 of 20 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.

3 participants