Skip to content

Add CSS-first styling and selectable PNG engines - #45

Merged
Alek99 merged 4 commits into
mainfrom
feat/css-first-customizability
Jul 15, 2026
Merged

Add CSS-first styling and selectable PNG engines#45
Alek99 merged 4 commits into
mainfrom
feat/css-first-customizability

Conversation

@Alek99

@Alek99 Alek99 commented Jul 14, 2026

Copy link
Copy Markdown
Member

What changed

  • add a CSS-first mark styling contract using standard paint properties: fill, stroke, stroke-width, independent fill/stroke/whole-mark opacity, gradients, dashes, and border radius
  • carry supported styling consistently through WebGL, SVG, and the fast native raster exporter
  • add a strict cross-renderer axis-style vocabulary with build-time validation, CSS px normalization, separate tick/tick-label paint, and grid dash/opacity support
  • expand chrome-slot styling while removing the public mark_style state schema so Reflex remains responsible for reactive state and events
  • add Engine.default and Engine.chromium for PNG export, with native export as the fast default and automatic installed-browser discovery for browser/CSS fidelity
  • allow custom_css= for Chromium single and batch PNG export; native export rejects browser-only author stylesheets
  • resolve complete chart-level color tokens such as var(--accent) in native SVG/PNG
  • fix FacetChart.to_png() forwarding gl to a FacetGrid signature that previously did not accept it
  • update examples, benchmarks, documentation, migration notes, smoke scripts, and public API checks

Why

XY should accept familiar CSS vocabulary that Reflex users and LLMs already understand instead of introducing an XY-specific styling language or duplicating Reflex conditions, event handling, and application state.

PNG export has a simple tradeoff: a millisecond-scale native default for normal use, plus an explicit Chromium engine when browser CSS, fonts, and WebGL fidelity matter.

User impact

chart.to_png()
chart.to_png(engine=xy.Engine.default)
chart.to_png(
    engine=xy.Engine.chromium,
    custom_css='[data-fc-slot="title"] { font-family: Inter; }',
)

For mark style={...}, use stroke on line-like marks and fill on filled marks. The legacy color= argument remains supported, but color is not a CSS paint alias inside style.

Axis style={...} now rejects unknown keys and invalid colors, directions, opacities, or lengths when the axis is created. Pixel properties accept numbers or values such as "3px".

Migration

  • MarkStyle / mark_style(...) are removed; interaction styling stays in Reflex state, conditions, events, and ordinary CSS.
  • PNG export now defaults to Engine.default (native). Use Engine.chromium for the installed-browser path.
  • String engine values remain deprecated compatibility aliases.
  • Browser executable parameters are replaced by automatic discovery or XY_BROWSER.
  • custom_css= is Chromium-only for PNG export.

Review follow-ups addressed

  • pin the intentional CSS rule that whole-mark opacity also dims area outlines
  • validate and document every renderer-backed axis style key before rendering
  • preserve separate pyplot tick-mark and tick-label colors/sizes
  • align browser, SVG, and native fallback paint to the same blue-gray
  • prove colorbar(show=False) clears generated colorbar options
  • confirm stroke_perimeter=False is strictly validated as a boolean

Validation

  • ruff check .
  • ruff format --check .
  • ty check
  • scripts/check_public_api.py
  • focused review suite: 173 passed, 1 skipped
  • full suite: 1,693 passed, 63 skipped; one known pyplot timing guardrail failure
  • static JS bundle freshness check
  • native and Chromium PNG smoke exports both produced correctly sized, nonblank images

Known baseline CI noise

The remaining red checks were reproduced unchanged on current main and are not caused by this PR:

  • tests/pyplot/test_perf_guardrail.py::test_pyplot_build_tracks_declarative[10000-60-1.6]
  • CodSpeed test_first_payload_hexbin_core_2d payload assertion

@Alek99
Alek99 force-pushed the feat/css-first-customizability branch from 6fdd045 to 404ce4e Compare July 14, 2026 21:43
@Alek99
Alek99 marked this pull request as ready for review July 14, 2026 22:31
@codspeed-hq

codspeed-hq Bot commented Jul 15, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 85.49%

❌ 2 regressed benchmarks
✅ 78 untouched benchmarks
⏩ 1 skipped benchmark1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
test_first_payload_contour_core_2d 5.3 ms 58.6 ms -91.02%
test_first_payload_hexbin_core_2d 8 ms 34 ms -76.55%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing feat/css-first-customizability (0b098c2) with main (dfd247b)2

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.

  2. No successful run was found on main (eb0c3f7) during the generation of this report, so dfd247b was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@Alek99
Alek99 merged commit c36e908 into main Jul 15, 2026
14 of 15 checks passed
FarhanAliRaza pushed a commit that referenced this pull request Jul 15, 2026
Two regressions from #37 kept CI red on main and every open PR:

Hexbin (benchmarks/test_codspeed_kernels.py::test_first_payload_hexbin_core_2d):
_emit_hexbin expanded every cell into six fan triangles across seven f32
columns (168 B/cell — 3.17 MB at gridsize=128, 2x the raw input; #45 had
loosened the benchmark bound to accept it). Hexagons all share one geometry,
so ship centers plus one color value per cell (12 B/cell, 227 KB) and expand
locally in each renderer: instanced triangles in the WebGL client
(_buildHexbinMark), one polygon per cell in SVG, vectorized triangles in the
raster exporter. Headless-Chromium readback is pixel-identical to the old
wire shape. The benchmark bound is restored to grid-bounded centers.

Perf guardrail (tests/pyplot/test_perf_guardrail.py, 10k case):
pyplot's fixed per-build cost had grown to ~2x the declarative baseline
locally. Removed per-build revalidation and O(n) temporaries:
- styles.compile_axis_style / compile_mark_style and
  _validate.style_mapping now memoize per exact input dict — the same few
  rc-derived chrome dicts were re-parsed through the native CSS grammar on
  every build.
- The rc chrome snapshot (_load_rc_chrome) is cached per
  (RcParams.version, dpi); RcParams now versions its mutations.
- plot() gap/path detection drops an astype copy + diff + compare for the
  native is_sorted kernel and skips the isfinite mask entirely for clean
  data via a sum probe (non-finite anywhere poisons the sum).

Local ratio at 10k: 2.03x -> ~1.6x; 100k: 1.26x. Full suite, ruff, ty,
render smoke pass; hexbin WebGL/SVG/PNG verified.
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