Data-bound chart components 8/8: full mark-kind coverage

Merged
FA
2mo ago
+1252-141

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_columns drops the shared-length requirement. A data var may carry mixed-length and 2-D columns — a stairs mark's len+1 edges 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.mean by 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 /kinds page 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 showing data=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.