Add aggregation, binning, granularity, sort, and stacking to the Chart Builder

- Per-channel transforms: aggregate (sum/mean/median/min/max), quantitative
  bin, and temporal timeUnit granularity; bin and aggregate are mutually
  exclusive. A field-less "Count of records" measure (Voyager's count(*)).
- Chart-level sort (rank a categorical axis by its measure) and stacking
  (zero / 100% normalize), each shown only when it applies.
- Field type is a fixed N|O|Q|T segmented control with the column's invalid
  types disabled; SegmentedControl gains APG-correct disabled options.
- A crowded-category-axis warning (a raw measure drawing one mark per row over
  a large dataset) and a disabled-Create hint (says why it's disabled).
- Drop the Create success toast — the new snippet is immediately visible.
- Docs: spec §06, research-doc §8 backlog (incl. the cardinality/extent
  profiling TODO), architecture 01 (stable-selector rule) and 05 (builder-local
  preview), and a profiling breadcrumb.
This commit is contained in:
2026-06-06 18:04:24 +03:00
parent c11afc273d
commit af9ee1e4c0
14 changed files with 982 additions and 119 deletions
+68
View File
@@ -198,6 +198,74 @@ The highest-value guardrails — encodings a naive UI emits that the canon rejec
- An **all-categorical chart with no measure** (Draco soft w=30, the loudest) — warned.
- A **number typed Nominal** (Draco soft w=10) — discouraged via default = Quantitative.
## 8. Future enhancements (backlog)
The Tier-B build is the floor, not the ceiling. The enhancements below were surfaced by
the research; this is their single home (the milestone plan's M4 row points here). Status
as of 2026-06-06.
**A · Cheap wins inside the current 5-mark / 4-channel scope**
- **A1 · Sort-on-ranking** _(done)_ — chart-level Sort control (Asc/Desc/None) sorts the
categorical axis by the measure (FT: "bars display ranks much more easily when sorted").
Appears only for a category-vs-measure pair.
- **A2 · Bar orientation** _(partly done)_ — the **Swap X/Y** control is the manual path to
a horizontal bar, and the crowded-axis hint (A3, below) now auto-suggests it for the
un-aggregated case. A general "long labels → go horizontal" suggestion on _any_ vertical
bar is still deferred (needs a label-length / cardinality signal); a blanket warning was
rejected — it would fire on every ordinary vertical bar.
- **A3 · Crowded-axis & high-cardinality warnings** _(partly done)_
- _Done:_ the **un-aggregated crowded axis** — a bar/line/area with a category axis and a
**raw** measure draws one mark (and one label) per row, so over `CROWDED_CATEGORY_ROWS`
(30) rows it warns and points to aggregating, or a horizontal bar. Row-count-based:
`builderWarnings(config, rowCount)`, with `rowCount` from the loaded dataset; detects
exactly the mark-count == row-count case (URL/non-tabular → `rowCount` null → skipped).
- _Remaining (needs the profiling extension below):_ an **aggregated** axis that still has
many distinct **categories**, an unreadable **Color legend** (>10/>20 categories), and
**number-typed-Nominal**. All need per-column **cardinality**, which the profile lacks.
- **A4 · Data-aware Size guard** _(deferred — needs the profiling extension)_ — exclude
**negative**-valued columns from Size (Draco `hard.lp:56`; size implies positive
magnitude). Today only the type-level Size discipline is enforced.
> **TODO — profiling extension (the A3-remaining + A4 enabler).** `profile.ts` computes only
> `rowCount` / `columnCount` / `columnTypes`. Extend it, in the **same sample pass** that
> already feeds `inferColumnType` (so it's nearly free), to also derive per column: a
> **capped distinct count** (cardinality — cap at ~50; a sampled count is enough for a
> ">N categories" threshold, no full scan) and a **numeric extent** (min/max → sign).
> Surface them on `DatasetProfile` (alongside `columnTypes`). Then: `builderWarnings` consumes
> cardinality (legend/axis crowding) and the Size gate consumes sign (A4). **Caveats:**
> URL / non-tabular datasets have no rows at profile time → these fields are null and the
> dependent warnings simply skip; and stored datasets predate the field, so this needs a
> recompute-on-read or a `dataset-migrations` bump (see architecture 02 / 06). Keep
> thresholds in `chart-builder.ts` constants like `CROWDED_CATEGORY_ROWS`.
**B · Transform-enabled coverage (new core capability + §06 extension)** _(done)_
- **B5 · Aggregation** _(done)_ — per-channel `sum` / `mean` / `median` / `min` / `max`, plus
a field-less "Count of records" measure (Voyager's `count(*)`). The priority item.
- **B6 · Binning** _(done)_`bin` on a quantitative field → true histograms (closes the
Distribution gap); mutually exclusive with aggregate on the same field.
- **B7 · Stacking** _(done)_`stack` (`zero` / `normalize`) for bar/area + a Color series →
part-to-whole (closes that gap; enables 100%-stacked).
- **Temporal granularity** _(done)_ — Vega-Lite `timeUnit` (Year / Quarter / Month / Week /
Day / Hour, plus combined units) on a Temporal field; defaults to None (raw).
- **B8 · Faceting (Row / Column → small multiples)** _(next increment, after A+B + UI land)_
— two more channels that multiply the chart into a trellis, the clean way to compare many
categories (Voyager has it; FT/Datawrapper recommend small multiples; we currently can't
express them). Still mark-first, so a Tier-B extension. **Axis alignment** is the design
crux: Vega-Lite facets default to **shared scales** (aligned axes) — keep that as the
default; expose an "independent axes" toggle (`resolve.scale`) only as an advanced option.
**Verify** faceting against the preview's `"container"` fit modes before trusting it
(per-cell sizing on facets is finicky). Sequenced as additive after the current build.
**C · Intent-first front door (Tier C)** _(deferred)_ — see §5. "What do you want to
show?" → recommend mark + channels from the FT/Datawrapper taxonomy × column types; where
Munzner + Wilke would be seated.
**D · Plumbing****D9** URL hash routing for the open builder (owned by **M6**, spec
§01E); **D10** a GOV.UK/NN-g copy pass over the guidance-hint wording (the M4 council
seating's residual one-off debt).
---
_Citations are to files under `/Users/oleh/code/reference/`. The seated chart-choice