Data-bound chart components 8/8: full mark-kind coverage
Stacked on #467 (PR 8 of the reflex component-API stack). Review only the two commits here; everything below them is under review in #461–#467.
What this changes
The data-bound plan tier shipped without the aggregating marks — box, violin, hexbin, contour, heatmap, stairs, ecdf. The Phase 3 decision excluded them because their validators need at least one finite value, and a synthetic-row probe "would validate against made-up data". This lifts that exclusion.
The objection is answered by discipline instead of exclusion. Each aggregating kind's channels bind fixed placeholder columns from a recorded table (plan._SYNTHETIC_CHANNELS) — finite, positive, strictly increasing, one group for the grouped kinds, a square z grid with matching side coordinates, len+1 bin edges. Those values satisfy every value-domain precondition a validator imposes, so a probe failure still indicts the structure, never the placeholders. Extending a kind means recording its minimal contract in that table rather than guessing silently (§28 spirit).
Riding along:
- Flat factories for the newly probed kinds plus
triangle_mesh_chart(always zero-row-safe, just never wired). All 19 standalone mark kinds now have one. validate_columnsdrops the shared-length requirement. A data var may carry mixed-length and 2-D columns — a stairs mark'slen+1edges or a heatmap grid beside ordinary row columns. Coupled-shape contracts stay with the mark validators at bind, where the errors name the mark and channels involved.- Named callables content-address. A module-level callable in mark props (hexbin's
reduce_C_function,np.meanby default) serializes as its import path. Lambdas and closures have no stable address and are refused toward a module-level function or@reflex_xy.figure. Plan registration becomes last-write-wins, so a hot reload replaces stale node objects behind an unchanged digest. - Demo: a
/kindspage rendering every kind (data-bound flat factories for the 19 marks; composite kinds — pie, radar, sankey, polar, wind rose, facet — on the static tier, since those still take data directly), and §10 on the main page showingdata=rx.cond(...)swapping two data vars under one fixed plan.
Spec
Recorded in three places rather than edited in place, so the reversal is legible: the implementation doc's post-landing revision section, the options doc's §8 decision record, and reflex-integration.md §3.6 "Kind coverage (recorded decision, revised 2026-08)".
Verification
uv run pytest tests/reflex_adapter tests/test_validation_timing.py tests/test_example_apps.py — 261 passed, 1 skipped (250 on #467). New pins: the shape table and per-kind plan builds (test_plan.py), the full 19-kind flat table (test_factories.py), SHAPED_ROW_CHARTS — the xy-level half of the contract (test_validation_timing.py), and the /kinds page composing with mixed-length + 2-D columns (test_example_apps.py).
pre-commit run --all-files, ruff check, ruff format --check, ty check all clean.
Not run: the browser E2E (scripts/reflex_ws_smoke.py) against the /kinds page. Worth doing before this merges — the probe proves the plans compile, not that all 19 kinds render.