mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Chart builder: intent front door, Heatmap mark, role-aware guidance
This commit is contained in:
@@ -504,6 +504,27 @@ _acts_ rather than navigates is mis-dressed: such actions are **ghost buttons**
|
||||
verb-first labels (Carbon links-vs-buttons; "Use a constant"). _(Consulted via /council →
|
||||
NN/g #1/#3/#4, Carbon button/link usage. This bullet is the contract.)_
|
||||
|
||||
**Resolved — the intent front door is an APG toolbar of toggle chips (Tier C "do it for me").**
|
||||
The Chart Builder's _"What do you want to show?"_ strip (spec §06 → Intent) is a **WAI-ARIA
|
||||
`toolbar`** (one tab stop, roving tabindex, `aria-labelledby` the visible heading) of chips —
|
||||
**not** seven independently-tabbable buttons (the pane-toggle precedent above). Each chip is a
|
||||
**toggle button** (`aria-pressed`) whose pressed state is **derived from the configuration**
|
||||
(the chip whose recommended layout the live chart matches), never stored — so a hand-edit
|
||||
resolves to "Custom" (none pressed) for free. Arrow keys **move focus only**; **Enter/Space
|
||||
applies** — because applying an intent reshapes the whole chart, a radiogroup's select-on-arrow
|
||||
would do that on every keypress (so this is a toolbar, not a `SegmentedControl` radiogroup).
|
||||
Selection shows as an **accent ring, never an accent fill** (fill stays the primary-action
|
||||
signal — arch 09 §3.3). Intents the dataset can't satisfy are **disabled via `aria-disabled`
|
||||
and kept arrow-reachable** (APG: focusable disabled controls "where discoverability of a
|
||||
function is crucial"), with the reason in the chip's accessible name (`aria-label`
|
||||
"Correlation — needs two number columns") plus a `title` for sighted hover — **never hidden**
|
||||
(Tableau _Show Me_). The chips are framed as **intents, not chart shapes** (FT Visual
|
||||
Vocabulary / Datawrapper organize by intent); _Heatmap_ is the one chart-type label retained
|
||||
— a deliberate divergence for recognizability that also mirrors the mark selector's _Heatmap_
|
||||
label, so the intent and the mark read as one thing (revisit if it confuses). _(Consulted via /council →
|
||||
APG toolbar + button(toggle); FT Visual Vocabulary / Datawrapper intent framing; NN/g #6
|
||||
recognition. This bullet is the contract.)_
|
||||
|
||||
**Resolved — an error names the right fix, not a boilerplate one.** Don't staple a generic
|
||||
remedy onto every failure. A missing dataset reference is **not** a JSON/spec syntax problem,
|
||||
so the preview gives it a tailored, fixable line — _"Dataset «X» not found. Create it from
|
||||
|
||||
@@ -25,6 +25,49 @@
|
||||
Newest first. The at-a-glance build-order tracker is §4; per-item detail is §3. This log is
|
||||
the quick "where are we" — read it first.
|
||||
|
||||
- **2026-06-13 (guidance: reason over role, not raw type)** — closed a **false-positive class**
|
||||
in `builderWarnings` (eng-council + council consult). A histogram (bar, binned-Q X, count Y)
|
||||
tripped "two measures → scatter" because `isMeasureMapping`/`effectiveType` ignored `bin`.
|
||||
Fix: `isMeasureMapping` is now **bin-aware** (a binned field is a discretized dimension —
|
||||
mirrors Vega-Lite's own `isDiscrete(fieldDef)`); a new `isReorderableCategory` keeps Sort
|
||||
from being offered on a histogram's binned axis (the council-flagged regression); the scatter
|
||||
rule routes through the role predicate + a positive mark list (deletes the `mark !== 'rect'`
|
||||
bolt-on); `stackMeasureChannel` excludes binned. Pinned with regression tests (no
|
||||
scatter/Sort on a histogram; real bar still sorts; two raw measures still nudge). Promoted to
|
||||
**/alignment check #15** ("reason over role, not raw type"). **Not done (deliberately):**
|
||||
intent-gating the taste warnings — verified no current intent layout trips one, so the gate
|
||||
would be dead code; recorded as a standing principle in `builderWarnings` instead. The
|
||||
broader posture (curate good via the front door > enumerate bad via warnings) is the council's
|
||||
recommended bottom for the combinatorial-warning worry. Verified: typecheck + test (913) +
|
||||
eslint + build.
|
||||
|
||||
- **2026-06-13 (3A + heatmap mark)** — **the intent-first front door shipped (Tier C), and
|
||||
the mark set gained `rect` (Heatmap).**
|
||||
- **Heatmap mark** — `rect` added to `MARK_TYPES` (six marks; labelled **Heatmap** in the
|
||||
picker, which now wraps to two rows in the 320–360px pane). Guidance rewired: both-axes
|
||||
rule covers heatmaps; a new hint nudges a Colour measure on a two-axis heatmap with a
|
||||
one-click **Colour by count**; `rect` is exempt from the two-measures→scatter nudge (a
|
||||
binned 2-D histogram is valid). Never the auto-default mark; names read "Heatmap of …".
|
||||
Spec §06 mark-type + guidance updated.
|
||||
- **3A intent front door** — a **persistent strip** at the top of the config pane (the
|
||||
interaction model chosen with the user over a replace-on-open screen — "shows all
|
||||
controls + a do-it-for-me", the Tableau _Show Me_ parallel). Core (`chart-builder.ts`):
|
||||
`CHART_INTENTS` (Compare/Ranking/Change-over-time/Correlation/Distribution/Part-to-whole/
|
||||
Heatmap), `intentLayout` (intent × column-roles → mark + channels), `intentApplicable`
|
||||
(Show-Me gating), `applyIntent` (reshape, keep dataset/transforms/title), and
|
||||
`activeIntent` (structural match → the live chart's intent highlights with **no stored
|
||||
state**; lights the smart default on open). Store: `setIntent`. UI: an APG **toolbar** of
|
||||
toggle chips (roving tabindex, arrows move / Enter applies, accent-ring selection,
|
||||
`aria-disabled`+reason on inapplicable intents).
|
||||
- **Council** run on the front-door copy/flow → toolbar (not radiogroup — select-on-arrow
|
||||
would reshape the chart), disabled-focusable-with-reason, intent-framing (Heatmap kept as
|
||||
the one chart-type label, mirroring the mark). Recorded in `architecture/10` §5; spec §06
|
||||
gained an **Intent (the front door)** subsection.
|
||||
- **Verified:** `typecheck` + `test` (909, +17: intent core/store, heatmap guidance/naming)
|
||||
- `eslint` + `build`. **Owed:** a visual pass on the live strip (chips, disabled states,
|
||||
keyboard) + a heatmap rendered from real data. **Open (user's call):** whether the
|
||||
_Heatmap_ chip should read as an intent phrase instead (council's intent-framing point).
|
||||
|
||||
- **2026-06-12 (3D)** — **entry points & discoverability shipped. Up next: 3A (the
|
||||
front door now has somewhere to be found).**
|
||||
- **Library creation surface forked:** primary **Build Chart** (accent, takes the
|
||||
@@ -557,9 +600,10 @@ Phase 1 1A actionable hints ✓ done
|
||||
Phase 2 2A value-or-field channels (Property model) ✓ done
|
||||
2B field shelf + in-place type cycling ✓ done (field-first + on-chart shelves)
|
||||
Phase 3 3D entry points & discoverability ✓ done (2026-06-12)
|
||||
3A intent-first front door (Tier C) ← next; built on 2B; the defining feature
|
||||
3B starter examples
|
||||
3A intent-first front door (Tier C) ✓ done (2026-06-13); persistent strip
|
||||
3B starter examples ← next; pairs with 3C
|
||||
3C open in builder (strict hydration) ← added 2026-06-11; pairs with 3B
|
||||
Marks +rect (Heatmap) ✓ done (2026-06-13)
|
||||
Phase 4 (gated) theta/facets/styling-overrides/undo/lookup — decide after Phase 3
|
||||
|
||||
Also shipped (builder UX/perf, from dogfooding): near-fullscreen modal, internal-scroll
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
The Chart Builder is a visual, no-JSON way to compose a Vega-Lite chart from a selected dataset. The user picks a mark type and maps the dataset's columns to encoding channels; the builder produces a complete Vega-Lite spec and saves it as a new snippet that references the dataset. It is intended for users who want to start a chart quickly without hand-writing JSON in the _Spec Editor & Draft/Published Workflow_.
|
||||
|
||||
> **Design level — "smart + guarded" (Tier B).** The builder is field-first — the user works from a shelf of the dataset's columns and drops them onto encoding channels — and stays within the inputs below, but it is not a dumb composer: it picks a sensible default mark for the data shape, offers only field types valid for each column, keeps unsuitable channel mappings out of reach, and surfaces non-blocking guidance for encodings that render poorly. These behaviors are derived from cross-source chart-choice research recorded in [`docs/chart-builder-research.md`](../chart-builder-research.md) (the convergence of Draco, Voyager, the FT Visual Vocabulary, and Datawrapper). The richer "intent-first" front door (ask _what do you want to show?_ and recommend a chart) is explicitly out of scope for now and noted there as a future tier.
|
||||
> **Design level — "smart + guarded" (Tier B).** The builder is field-first — the user works from a shelf of the dataset's columns and drops them onto encoding channels — and stays within the inputs below, but it is not a dumb composer: it picks a sensible default mark for the data shape, offers only field types valid for each column, keeps unsuitable channel mappings out of reach, and surfaces non-blocking guidance for encodings that render poorly. These behaviors are derived from cross-source chart-choice research recorded in [`docs/chart-builder-research.md`](../chart-builder-research.md) (the convergence of Draco, Voyager, the FT Visual Vocabulary, and Datawrapper). On top of this "smart + guarded" base sits the **intent-first front door** (Tier C — _what do you want to show?_, see _Intent_ below): an on-ramp that recommends a whole chart from the user's stated intent, without replacing the mark-first builder beneath it.
|
||||
|
||||
## Opening
|
||||
|
||||
@@ -25,6 +25,17 @@ Which data the chart builds from is itself a builder choice. A **Dataset** picke
|
||||
|
||||
With an empty dataset library the builder shows a **no-datasets state** instead of controls: it says what the builder does and offers one primary next step — **Add a dataset**, which opens the _Datasets_ manager on its create form. The guided path never dead-ends.
|
||||
|
||||
### Intent (the front door)
|
||||
|
||||
A persistent **"What do you want to show?"** strip sits at the top of the configuration pane, under the dataset picker — the builder's guided on-ramp (Tier C). It offers a small set of analytic **intents**, each of which, when chosen, **sets the whole chart up for you** ("do it for me", modelled on Tableau's _Show Me_):
|
||||
|
||||
- The intents map the FT Visual Vocabulary / Datawrapper taxonomy onto the builder's marks and channels: **Compare** (magnitude across categories → a bar of counts), **Ranking** (the same, sorted by value), **Change over time** (a line of the first measure over a date), **Correlation** (a scatter of two measures), **Distribution** (a histogram — a binned measure vs count), **Part-to-whole** (a stacked bar split by a second category), and **Heatmap** (a two-category grid shaded by count).
|
||||
- Picking an intent reshapes the chart — its **mark, encodings, and sort/stack** — to that intent's recommended layout, derived from the dataset's column roles. It **keeps** the dataset, the data transforms (filters / calculated fields), and the chart properties (title/subtitle/size): those are orthogonal to _what kind of chart_.
|
||||
- The strip is an **on-ramp, not a gate**: it seeds the mark-first builder, which the user can then adjust freely or ignore entirely (and can always drop to Monaco). The chosen intent is builder-local steering — it **never enters the produced spec** (the JSON stays the document).
|
||||
- On open, the strip **pre-highlights the intent matching the data-aware default** (so a category-vs-count default opens on _Compare_). The highlight is **derived from the configuration, not stored**: the chip whose recommended layout the live chart currently matches stays highlighted; once the user edits away from any recommended layout, none is highlighted — a **Custom** chart.
|
||||
- Intents the dataset **cannot satisfy** are **disabled** (Tableau _Show Me_): a _Correlation_ needs two number columns, a _Heatmap_ or _Part-to-whole_ needs two category columns, _Change over time_ needs a date, and so on. A disabled chip stays perceivable and carries the reason in its accessible name; it is never hidden.
|
||||
- Keyboard/focus follow [`architecture/10`](../architecture/10-interaction-and-feedback.md) §5 (an APG **toolbar**: one tab stop, a roving tabindex, arrow keys move focus, Enter/Space applies the intent — so navigation never reshapes the chart by accident).
|
||||
|
||||
## Layout
|
||||
|
||||
A two-pane modal:
|
||||
@@ -68,8 +79,8 @@ The section is ordered **input → shaping** so the distinction reads at a glanc
|
||||
|
||||
### Mark type
|
||||
|
||||
- Single selection from an exact set of five mark types: **Bar, Line, Point, Area, Circle**.
|
||||
- On open, the mark **defaults to the type that best fits the pre-populated X/Y field-type shape** (Tier B smart default): a temporal axis against a measure → **Line**; two measures → **Point**; a category against a measure → **Bar**; two categories → **Point**; and **Bar** as the fallback when only one axis (or none) is mapped. The user can switch to any of the five afterward.
|
||||
- Single selection from an exact set of six mark types: **Bar, Line, Point, Area, Circle, Heatmap**. **Heatmap** is the Vega-Lite `rect` mark — an X×Y grid of cells shaded by a Colour measure; it is labelled by the chart it makes rather than its geometry, since "Rect" is opaque to the no-JSON audience.
|
||||
- On open, the mark **defaults to the type that best fits the pre-populated X/Y field-type shape** (Tier B smart default): a temporal axis against a measure → **Line**; two measures → **Point**; a category against a measure → **Bar**; two categories → **Point**; and **Bar** as the fallback when only one axis (or none) is mapped. **Heatmap is never the auto-default** — it reads only with a Colour measure, which the X/Y shape alone can't determine, so it stays a deliberate pick (guidance nudges the missing Colour). The user can switch to any of the six afterward.
|
||||
- Exactly one mark type is active at any time; selecting one updates the preview.
|
||||
|
||||
### Encoding channels
|
||||
@@ -113,9 +124,10 @@ These controls appear only when they apply:
|
||||
|
||||
The builder surfaces short, plain-language hints for configurations that render but read poorly — advisory only, never blocking the **Create Snippet** action (validation below is the sole gate). A hint states the _problem_; where there is an obvious remedy, it also offers one or more **one-click fix** buttons that apply the change to the configuration (e.g. _Aggregate as Sum_, _Swap X/Y_, _Switch to Point_, _Stack_, _Remove colour_). A fix is an offer, never a forced change — applying it updates the config and the hint re-derives away. Interaction/accessibility of these actions follows [`architecture/10`](../architecture/10-interaction-and-feedback.md) §5 (polite announcement, focus moved off the removed button). These follow the chart-choice research ([`docs/chart-builder-research.md`](../chart-builder-research.md)) and include, for example:
|
||||
|
||||
- A **Line** or **Area** mark with only one axis mapped (both axes are needed to draw it).
|
||||
- A **Line**, **Area**, or **Heatmap** mark with only one axis mapped (both axes are needed to draw it).
|
||||
- A **Bar/Line/Area** whose X and Y are both categories (nothing to measure).
|
||||
- **Two measures** on a non-scatter mark (a scatter — Point/Circle — usually reads better).
|
||||
- A **Heatmap** with both axes mapped but **no measure on Colour** (its cells have nothing to shade) — offers a one-click _Colour by count_, the canonical cross-tab heatmap. _Two measures on a heatmap are exempt from the scatter nudge below: a binned 2-D histogram is two quantitative axes shaded by count._
|
||||
- **Two measures** on a non-scatter mark (a scatter — Point/Circle — usually reads better; Heatmap excepted).
|
||||
- An **Area** chart split into multiple colour series (per-series change is hard to see).
|
||||
- A **Bar/Line/Area** that pairs a category axis with a **raw (un-aggregated) measure** over a many-row dataset — it draws one mark, and one axis label, per row, so the category axis becomes an unreadable picket fence. The hint suggests aggregating the measure (one mark per category) or, for a bar, flipping to a horizontal bar (Swap X/Y) where long labels stay readable (FT Visual Vocabulary / Datawrapper). Only the un-aggregated case (mark-count = row-count) is detected; flagging an _aggregated_ axis that still has many distinct categories needs per-column distinct counts the profiler does not yet compute (a known gap).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user