mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Chart builder: SelectControl pickers, channel chooser, per-type aggregates
This commit is contained in:
@@ -421,6 +421,13 @@ open, wraps `Tab`/`Shift+Tab` within the modal, and restores focus on close. The
|
||||
optional `initialSelector` picks _which_ child takes focus (e.g. Cancel for a
|
||||
destructive confirm); it falls back to the first focusable child.
|
||||
|
||||
The trap wraps Tab **only within the shell element**: content portaled to
|
||||
`<body>` (a `SelectControl` panel or other disclosure popover opened from inside
|
||||
a modal) is outside both the trap's DOM subtree and its keydown listener. Such
|
||||
popovers must therefore handle Tab themselves — close and refocus their trigger
|
||||
(the native-select convention) — so focus can't strand outside the dialog while
|
||||
it is open.
|
||||
|
||||
```ts
|
||||
// src/app/hooks/useFocusTrap.ts
|
||||
import { useRef, useEffect } from 'react';
|
||||
|
||||
@@ -471,6 +471,10 @@ the existing view is re-measured via a `ResizeObserver`-driven event — see §8
|
||||
- **Don't** put reference resolution or fit-mode logic in the renderer — it is
|
||||
pure core logic and must be unit-testable without a DOM.
|
||||
- **Don't** mutate the input spec anywhere in the pipeline.
|
||||
- **Fit modes overwrite the spec's own sizing** (Width replaces `width` _and
|
||||
deletes_ `height`, etc.), so a surface that lets the user set an explicit
|
||||
width/height must pass `fitMode: 'default'` while either is set and reserve the
|
||||
container fit for auto sizing — the Chart Builder preview does exactly this.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -418,6 +418,34 @@ _(Consulted via /council → NN/g #4 consistency, #6 recognition-over-recall, #8
|
||||
WAI-ARIA APG disclosure + menu-and-menubar; Carbon popover/overflow-menu/text-toolbar. This
|
||||
bullet is the contract.)_
|
||||
|
||||
**Resolved — value pickers are the SelectControl disclosure, not native `<select>`.** A
|
||||
native select's popup can't be token-styled and renders differently on every browser/OS — a
|
||||
foreign object inside a designed surface — so anywhere a control is part of one,
|
||||
`SelectControl` replaces it: the same disclosure primitive as the settings popovers (trigger
|
||||
with `aria-expanded`/`aria-controls`; portaled, `fixed`-positioned panel; labelled `group` of
|
||||
option buttons — **not** an ARIA menu or combobox; single-open registry; Esc closes and
|
||||
refocuses the trigger; outside press closes; open lands focus on the selected option;
|
||||
Arrow/Home/End rove). The selected option carries `aria-current` and a visible ✓, never
|
||||
colour alone. The same control doubles as an **action picker** (no `value`; e.g. "Add field
|
||||
to which channel?"). A custom `triggerClassName` _replaces_ the default trigger styling, so
|
||||
chip-styled triggers (the pill's type chip, the shelf's field chips) stay chips.
|
||||
_(Consulted via /council → WAI-ARIA APG disclosure/menu-button/radio, Carbon, NN/g #4. This
|
||||
bullet is the contract; cite it, not the source.)_
|
||||
|
||||
**Resolved — field→channel assignment: explicit choice, visible armed state.** Clicking a
|
||||
shelf field with no channel armed opens an explicit **channel chooser** (the channels that
|
||||
accept the field; an occupied one is labelled with what it replaces) — never a silent
|
||||
first-empty-seat grab (NN/g #3, user control). Arming a channel slot short-circuits the
|
||||
chooser (the fast path) and **must be visible where the next click happens**: the shelf
|
||||
gains an accent ring plus a polite `role="status"` line naming the target ("Assigning to X —
|
||||
choose a field below. Esc cancels"); **Esc disarms** without closing the modal (captured
|
||||
before the dialog's own Escape handling). Chart-level properties (title/subtitle,
|
||||
width/height) live **on the chart side**, in a strip under the preview — a chart property
|
||||
belongs on the chart (NN/g #4; the Tableau/Lyra convention). A link-styled affordance that
|
||||
_acts_ rather than navigates is mis-dressed: such actions are **ghost buttons** with
|
||||
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 — 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,43 @@
|
||||
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-12 (polish batch)** — **the 2B visual-pass findings fixed; the parked council
|
||||
batch resolved and applied; two capability gaps closed.**
|
||||
- **Aggregation for any field type.** `validAggregateOps` replaces the quantitative-only
|
||||
gate: **Count distinct** on anything (a nominal Colour/Y now measures unique values —
|
||||
emitted as a quantitative `{ aggregate: 'distinct' }`), Min/Max on temporal/ordinal,
|
||||
arithmetic still quantitative-only. Retyping keeps a still-valid aggregate.
|
||||
- **Title/subtitle** (pulled forward from the placement discussion): top-level `title`
|
||||
emission; a user title becomes the snippet name; subtitle gated on a title.
|
||||
- **Council batch applied:** the type chip and every native `<select>` in the builder are
|
||||
now **`SelectControl`** — a reusable value-picker disclosure (the SortControl primitive
|
||||
generalized; replaces native selects app-wide where the control is part of a designed
|
||||
surface). Field chips open an explicit **channel chooser** (occupied channels say what
|
||||
they'd replace); an **armed** channel short-circuits it and is now visible (accent ring +
|
||||
status line + Esc disarms). "or constant" → **"Use a constant"** ghost button. Chart
|
||||
properties (Title · Subtitle · W · H) moved to a **strip under the preview**; Sort/Stack
|
||||
stay by the encodings. Resolutions recorded in `architecture/10` §5; `ux-second-pass.md`
|
||||
cleared (drag stays deferred).
|
||||
- **Visual-pass fixes:** pill ✕ flush right (pills hug content), Swap X/Y beside the Axes
|
||||
heading, field shelf scrolls (280px viewport, sticky group heads), per-channel transforms
|
||||
inline beside the pill, preview honors explicit width/height (fit-mode only while auto).
|
||||
- **Verified:** `typecheck` + `test` (774, +10) + `eslint` + `build`. **Owed:** a fresh
|
||||
visual look at the reworked surfaces (user-driven).
|
||||
|
||||
- **2026-06-12 (scope)** — **3D added to Phase 3: builder entry points & discoverability**
|
||||
(council-reviewed: NN/g #6 recognition + #7 expert accelerators, Carbon empty-states).
|
||||
Today the builder's only door is Datasets → row "Build Chart" while the primary "Create
|
||||
New Snippet" lands in blank Monaco — the data model's shape, not the user's intent.
|
||||
Decided: fork the library's creation surface (primary **Build Chart**, ghost **New JSON
|
||||
snippet**), give the builder an internal dataset picker + a no-datasets empty state, add
|
||||
an onboarding tile. 3D lands **with or just before 3A** — the Build-Chart button opens
|
||||
onto the intent front door, making 3A the app's guided creation flow. Detail in §3 · 3D.
|
||||
|
||||
- **2026-06-11 (scope)** — **3C added to Phase 3: open-in-builder (strict spec hydration).**
|
||||
A snippet-list button, enabled exactly when the spec round-trips losslessly through the
|
||||
builder dialect (checked by re-assemble + deep-compare, not feature enumeration). Strict
|
||||
only — no lossy/residue modes (§5's round-trip trap stays closed). Detail in §3 · 3C.
|
||||
|
||||
- **2026-06-11 (Phase 2)** — **2A + 2B shipped: the field-first interaction substrate.** Up next: **3A intent-first front door**.
|
||||
- **2A · Value-or-field channels (the Property model).** `ChannelMapping` gained a constant
|
||||
`value` arm; the assembler emits `{ value }` and the measure/stack/area-split/prune logic
|
||||
@@ -304,6 +341,12 @@ revision (field shelf alongside the channel rows)._
|
||||
|
||||
### Phase 3 — Tier C (the intent-first front door) — _the defining feature of this push_
|
||||
|
||||
The grand idea, stated once: **the builder is the app's rapid, intuitive on-ramp; Monaco
|
||||
is the expert surface; the JSON spec stays the document.** Phase 3 is where that becomes
|
||||
true — 3A gives the builder an intent-first opening screen, 3D makes that screen the
|
||||
app's guided creation flow (not a feature hidden in a modal), and 3B/3C seed and re-enter
|
||||
it.
|
||||
|
||||
**3A · Intent-first front door** — _the B→C step_
|
||||
Source: research §5/§8 Tier C (FT Visual Vocabulary + Datawrapper intent taxonomy). A
|
||||
_"what do you want to show?"_ entry mapping **intent × column types → recommended mark +
|
||||
@@ -331,7 +374,72 @@ this as the deferred tier, so this is the planned amendment, not drift._
|
||||
Source: Lyra §3.7. A small set of **curated starter snippets**, one per covered FT intent
|
||||
(Magnitude/Bar, Change-over-time/Line, Correlation/Point, Distribution/histogram,
|
||||
Part-to-whole/stacked). Improves first-run, doubles as living documentation of what the app
|
||||
does well. Natural home: the snippet library. _Spec impact: §02 (library seed content)._
|
||||
does well. Natural home: the snippet library. **Author them in the builder dialect so they
|
||||
hydrate via 3C.** _Spec impact: §02 (library seed content)._
|
||||
|
||||
**3C · Open in builder (strict spec hydration)** — _decided 2026-06-11; pairs with 3B_
|
||||
Reverse the assembler: a pure `parseChartSpec(spec) → BuilderConfig | null` so an existing
|
||||
snippet can re-enter the builder populated. **Strict policy only** — a snippet-list button
|
||||
("Open in builder") enabled exactly when the spec is **losslessly** representable in the
|
||||
builder's dialect; anything richer stays Monaco-only. Losslessness is checked not by
|
||||
enumerating unsupported features but by **re-assembling the parsed config and
|
||||
deep-comparing against the original** (ignoring key order, `$schema`, and the injected
|
||||
`tooltip: true`) — exact, ~20 lines, and it stays correct automatically as the dialect
|
||||
grows (every Phase-4 addition widens what hydrates for free). No lossy or
|
||||
residue-preserving mode: the moment the builder can overwrite a richer spec, the GUI starts
|
||||
competing with the JSON as the document (the Lyra round-trip trap — §5).
|
||||
|
||||
Value: converts the builder from **create-only to create-and-revise** for its own output,
|
||||
and makes the 3B starters openable as builder seeds. Known fiddly bits (all mechanical):
|
||||
inverse of `escapeVegaField`, filter-value un-coercion back to the `BuilderFilter` string
|
||||
shape, `sort: "-y"` → `{sort: 'descending'}`. Round-trip property tests
|
||||
(`parse(build(config))` ≈ `config`) are the natural core coverage. Per-row check in the
|
||||
library is cheap but memoize it. **Decisions at build time:** save semantics on an opened
|
||||
snippet (update-in-place via a builder "edit" mode vs. always-create-new — today the
|
||||
builder only creates) and the disabled-button affordance (disabled-with-reason vs. hidden —
|
||||
park in `ux-second-pass.md` if non-obvious). _Spec impact: §02 (library row action) + §06
|
||||
(hydration + the strict gate)._
|
||||
|
||||
**3D · Entry points & discoverability** — _decided 2026-06-12, council-reviewed; lands with or just before 3A_
|
||||
Today the builder has **one entry, two levels deep**: Datasets modal → per-dataset "Build
|
||||
Chart" (`DatasetsModal.tsx`) — the data model's shape ("charts come from datasets"), not
|
||||
the user's intent ("I want to make a chart"). Meanwhile the library's pinned primary
|
||||
action, "Create New Snippet", drops straight into blank Monaco — the **expert** path is
|
||||
the default, and the audience the builder serves may never find it. Council ruling
|
||||
(recorded here; contract updates at build time):
|
||||
|
||||
- **NN/g #6 (recognition over recall):** the build-a-chart action must be visible where
|
||||
the intent forms — the library's creation surface, not recalled as a dataset-row action.
|
||||
- **NN/g #7 (flexibility/efficiency):** the guided path is what novices see; raw JSON is
|
||||
the expert accelerator — visible, one click, never hidden. Builder-forward, Monaco
|
||||
intact.
|
||||
- **Carbon empty-states:** one primary action per surface ("pick the most important"; a
|
||||
tertiary button for the secondary CTA); a no-data state explains what the space will
|
||||
hold, offers one primary next step, and never dead-ends.
|
||||
|
||||
The decided shape:
|
||||
|
||||
1. **Fork the library's creation surface** — primary **Build Chart** (opens the builder),
|
||||
ghost/tertiary **New JSON snippet** (today's instant create, unchanged). Two plain
|
||||
buttons with clear hierarchy — no split/menu-button widget (two static choices don't
|
||||
earn an ARIA menu).
|
||||
2. **Dataset picker inside the builder** (header select, default = most recently used), so
|
||||
the builder opens without a preselected dataset. Build-time detail: switching datasets
|
||||
mid-build re-derives smart defaults vs. prunes incompatible encodings — decide then.
|
||||
Useful beyond 3D (switch data without leaving the builder).
|
||||
3. **No-datasets empty state** in the builder, per the Carbon no-data pattern: what the
|
||||
builder does + one primary action ("Add a dataset" → Datasets modal). No dead end.
|
||||
4. **Onboarding tile** — a data-first path ("import your data → build a chart") beside the
|
||||
existing examples gallery.
|
||||
5. **Keep the dataset-row "Build Chart"** — contextual shortcut, pre-picks the dataset; it
|
||||
just stops being the only door.
|
||||
|
||||
**Composition with 3A (the point of the sequencing):** the Build-Chart button lands on the
|
||||
intent front door — "what do you want to show, with which data?" _is_ the builder's
|
||||
opening screen. 3D without 3A opens onto the mark-first builder (fine, interim); 3A
|
||||
without 3D is a front door nobody finds. Discoverability is a prerequisite for 3A's value,
|
||||
so: **3D with or just before 3A.** _Spec impact: §02 (library creation actions +
|
||||
onboarding) + §06 (dataset picker, empty state)._
|
||||
|
||||
### Builder UX & perf — in-flight fixes (2026-06-10, from dogfooding the Superstore dataset)
|
||||
|
||||
@@ -418,8 +526,10 @@ Phase 1 1A actionable hints ✓ done
|
||||
1E expr-validate ✓ done (syntax + unknown-field; completion popup deferred)
|
||||
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 3A intent-first front door (Tier C) ← next; built on 2B; the defining feature
|
||||
Phase 3 3D entry points & discoverability ← next, with/just before 3A (its prerequisite)
|
||||
3A intent-first front door (Tier C) ← built on 2B; the defining feature
|
||||
3B starter examples
|
||||
3C open in builder (strict hydration) ← added 2026-06-11; pairs with 3B
|
||||
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
|
||||
@@ -431,7 +541,10 @@ Rationale for the order: Phase 1 is the cheapest large quality jump and needs no
|
||||
interaction model, so it ships value while the bigger design settles. Phase 2 is pure
|
||||
substrate — low _user-visible_ payoff alone, but Phase 3 is much cleaner on top of it than
|
||||
bolted onto the channel-first UI. Phase 3 delivers the brief's headline ("recommendations
|
||||
on the fly"). Phase 4 is deliberately deferred to protect the "moderately capable" ceiling.
|
||||
on the fly") — 3D first, because a front door nobody finds delivers nothing: the entry
|
||||
points make the builder the app's guided creation flow, then 3A gives that flow its
|
||||
intent-first opening. Phase 4 is deliberately deferred to protect the "moderately capable"
|
||||
ceiling.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ The Chart Builder is a visual, no-JSON way to compose a Vega-Lite chart from a s
|
||||
|
||||
A two-pane modal:
|
||||
|
||||
- **Left — configuration:** dataset name, a **Data** section (row filters, calculated fields, and a collapsible row preview — see _Data_ below), mark type selector, a **field shelf** (the dataset's columns, field-first), a **Marks** card (the Colour and Size encodings), optional width/height inputs, and a "Create Snippet" action.
|
||||
- **Left — configuration:** dataset name, a **Data** section (row filters, calculated fields, and a collapsible row preview — see _Data_ below), mark type selector, a **field shelf** (the dataset's columns, field-first), a **Marks** card (the Colour and Size encodings), and a "Create Snippet" action. Chart-level properties (title/subtitle, width/height) sit on the **preview side**, under the chart (see _Chart properties_).
|
||||
- **Right — live preview:** the **Columns** (X) and **Rows** (Y) shelves stacked above a rendered chart that updates as the configuration changes, with a placeholder/error area. Position is a property of the chart, so its controls sit on the chart (Tableau's Columns/Rows metaphor).
|
||||
|
||||
## Data (preview, filters, calculated fields)
|
||||
@@ -62,9 +62,9 @@ The section is ordered **input → shaping** so the distinction reads at a glanc
|
||||
Exactly four channels are offered: **X, Y** (the positional axes, on the on-chart Columns/Rows shelves) and **Colour, Size** (the Marks card). Assignment is **field-first**.
|
||||
|
||||
- **The field shelf** lists the dataset's detected columns (see _Datasets_), each with a small type glyph, plus any calculated fields and a field-less **"Count of records"** measure (Vega-Lite `count`). Past a threshold of columns the shelf groups into **Dimensions** (categories/dates) and **Measures** (numerics); a small dataset stays a single flat list. A column already mapped somewhere is dimmed (it may still be placed on more than one channel).
|
||||
- **Assigning a field:** clicking a shelf field places it on the **armed** channel, else the first empty channel that accepts it (X→Y→Colour→Size). A channel slot is armed by clicking it (then a clicked field fills it); the armed slot shows a "pick a field" prompt. A field that no channel can take is a no-op.
|
||||
- **A mapped channel is a pill:** a leading type chip, the field (or "Count") label, and a remove (×). The type chip **is the field-type control** — activating it cycles the field's type within the set **valid for that column** and channel (Tier B valid-type locking): number → {Quantitative (default), Ordinal, Nominal}; date → {Temporal}; text → {Nominal (default), Ordinal}; boolean → {Nominal}. When only one type applies (e.g. a date), the chip is inert. A fresh mapping defaults its type from the inferred column type (numeric → Quantitative, date → Temporal, otherwise Nominal).
|
||||
- **Constant values (the Property model):** the **Colour** and **Size** channels may instead hold a **fixed constant** — a literal colour or size applied to every mark, emitted as Vega-Lite `{ value }` rather than a field binding. An empty Colour/Size slot offers an "or constant" affordance; the bound constant shows a colour picker (Colour) or a number (Size). X and Y stay field-only (a constant position is not useful). Switching a channel between a field and a constant is reversible, and the prior field type is preserved across the toggle.
|
||||
- **Assigning a field:** clicking a shelf field opens an explicit **channel chooser** listing the channels that accept it (an occupied channel is labelled with what it would replace); picking one places the field there. With a channel **armed**, the chooser is skipped — the clicked field fills the armed channel directly. A channel slot is armed by clicking it; arming is visible at the shelf (an accent ring plus a status line naming the target — "Assigning to X — choose a field below. Esc cancels"), and **Esc** disarms without closing the builder. A field that no channel can take offers no choices.
|
||||
- **A mapped channel is a pill:** a leading type chip, the field (or "Count") label, and a remove (×). The type chip **is the field-type control** — activating it opens a **direct pick** of the types **valid for that column** and channel (Tier B valid-type locking): number → {Quantitative (default), Ordinal, Nominal}; date → {Temporal}; text → {Nominal (default), Ordinal}; boolean → {Nominal}. When only one type applies (e.g. a date), the chip is inert. A fresh mapping defaults its type from the inferred column type (numeric → Quantitative, date → Temporal, otherwise Nominal).
|
||||
- **Constant values (the Property model):** the **Colour** and **Size** channels may instead hold a **fixed constant** — a literal colour or size applied to every mark, emitted as Vega-Lite `{ value }` rather than a field binding. An empty Colour/Size slot offers a **"Use a constant"** ghost button; the bound constant shows a colour picker (Colour) or a number (Size). X and Y stay field-only (a constant position is not useful). Switching a channel between a field and a constant is reversible, and the prior field type is preserved across the toggle.
|
||||
- **Size discipline:** the **Size** channel accepts only columns whose natural type is a magnitude (numeric) — size implies an ordered magnitude, so categories and dates are not placed on Size by assignment (they remain available on X/Y/Colour). A constant size is always allowed.
|
||||
- **Clearing** a channel (the pill's ×) leaves it out of the produced spec.
|
||||
- A **Swap X/Y** control, by the on-chart shelves, exchanges the X and Y bindings (field/constant and type) in one click — for quickly flipping the axes of the pre-populated default.
|
||||
@@ -77,7 +77,7 @@ Each on-chart shelf shows, beside its axis slot, a **non-interactive placeholder
|
||||
|
||||
Once a column is mapped, the channel offers the transforms that apply to its field type — and only those:
|
||||
|
||||
- **Aggregate** (a measure / Quantitative field): one of `Sum`, `Mean`, `Median`, `Min`, `Max`, or `None`. (The field-less `Count` measure is chosen via the "Count of records" column option above.)
|
||||
- **Aggregate** (any field — the menu narrows by field type): a Quantitative field offers `Sum`, `Mean`, `Median`, `Min`, `Max`, `Count distinct`, or `None`; a Temporal or Ordinal field offers `Min`, `Max`, `Count distinct` (an ordering but no arithmetic); a Nominal field offers `Count distinct` alone. `Count distinct` counts a field's unique values, so the channel reads as a **quantitative measure** whatever the field's own type (e.g. "unique customers per region" on a Color or Y channel); the field's asserted type is preserved and restored when the aggregate is removed. (The field-less `Count` measure is chosen via the "Count of records" column option above.)
|
||||
- **Bin** (a Quantitative field): bins the values into ranges — e.g. a Quantitative X binned with a Count Y is a histogram. Binning and aggregating the same field are mutually exclusive (setting one clears the other).
|
||||
- **Granularity** (a Temporal field): a Vega-Lite `timeUnit` — Year, Year-Quarter, Year-Month, Year-Month-Day, Quarter, Month, Week, Day of month, Day of week, Hour — or `None` (raw timestamps). Defaults to **None** (no silent change to what the raw data shows).
|
||||
|
||||
@@ -106,10 +106,13 @@ The builder surfaces short, plain-language hints for configurations that render
|
||||
|
||||
A clean configuration shows no hints.
|
||||
|
||||
### Dimensions (optional)
|
||||
### Chart properties (optional)
|
||||
|
||||
A slim strip pinned **under the live preview** — these describe the chart itself, so they live on the chart side rather than in the configuration pane:
|
||||
|
||||
- **Title** and **Subtitle** text inputs, written into the spec's top-level `title` (a bare string for a lone title; the `{ text, subtitle }` object form when both are set). A subtitle is emitted only alongside a title — Vega-Lite has no standalone subtitle — so the Subtitle input is disabled until a title exists. A non-empty title is also preferred verbatim as the created snippet's name.
|
||||
- Optional numeric **Width** and **Height** inputs in pixels.
|
||||
- When left empty, the chart uses default/responsive sizing (consistent with _Live Preview_); when provided, the values are written into the spec.
|
||||
- When left empty, sizing is default/responsive (consistent with _Live Preview_); when provided, the values are written into the spec **and the builder preview renders at that explicit size** (the preview's fit-to-pane sizing applies only while sizing is auto).
|
||||
|
||||
## Live Preview
|
||||
|
||||
@@ -127,7 +130,7 @@ A clean configuration shows no hints.
|
||||
|
||||
Selecting "Create Snippet" produces the final artifact:
|
||||
|
||||
- Builds a complete Vega-Lite spec containing: the schema reference, a named data reference to the dataset, any top-level `transform` (calculated fields first, then row filters — see _Data_), the chosen mark (with tooltips enabled), the bound encodings (a field encoding carries its field and field type plus any aggregate / bin / `timeUnit` transform; a constant encoding is a `{ value }`), chart-level sort and stacking where set, and any explicit width/height.
|
||||
- Builds a complete Vega-Lite spec containing: the schema reference, a named data reference to the dataset, any top-level `transform` (calculated fields first, then row filters — see _Data_), the chosen mark (with tooltips enabled), the bound encodings (a field encoding carries its field and field type plus any aggregate / bin / `timeUnit` transform; a constant encoding is a `{ value }`), chart-level sort and stacking where set, any title/subtitle, and any explicit width/height.
|
||||
- Channels left unmapped are omitted; if no encodings exist the spec omits the encoding block entirely (prevented by validation here).
|
||||
- Creates a new snippet from that spec with an auto-generated descriptive name, adds it to the snippet library, and records that it was built from the dataset.
|
||||
- Links the snippet to the dataset by recording the dataset reference, so the bidirectional snippet↔dataset relationship is established (see _Datasets_).
|
||||
|
||||
+10
-18
@@ -8,23 +8,15 @@ record the resolution into the contract (`docs/architecture/09`+`10` and the rel
|
||||
|
||||
## Open
|
||||
|
||||
### Chart Builder — encoding type control: cycle vs. explicit pick
|
||||
_(none — the 2026-06-12 batch resolved all parked items: type-cycle chip → direct-pick
|
||||
SelectControl; field-assignment flow → explicit channel chooser + visible armed state, drag
|
||||
still deferred; "or constant" → "Use a constant" ghost button; chart-level controls →
|
||||
properties strip under the preview. Resolutions recorded in `architecture/10` §5 and
|
||||
`spec/06`.)_
|
||||
|
||||
- **Where:** `src/app/components/ChartBuilderModal.tsx` → `ChannelPill` (the `pillType` chip).
|
||||
- **What:** a mapped channel's field type is shown as a **cycling chip** — activating it steps
|
||||
N → O → Q → T within the set valid for the column/channel. It is compact and matches the
|
||||
approved field-first pill design, but a _cycling_ button gives a screen-reader / keyboard
|
||||
user no way to jump straight to a type; they step through the cycle. The pre-2B control was
|
||||
an explicit `N | O | Q | T` segmented radio (direct pick, APG radio pattern).
|
||||
- **Decision to make:** keep the cycle, restore the explicit segmented control, or a hybrid
|
||||
(chip for a quick mouse cycle + an accessible radio/menu for direct selection).
|
||||
- **Lens:** WAI-ARIA APG (radio vs. toggle button), IBM Carbon, the project's keyboard-first
|
||||
accessibility bar.
|
||||
## Deferred (not design debts, revisit on demand)
|
||||
|
||||
### Chart Builder — empty Colour/Size "or constant" affordance
|
||||
|
||||
- **Where:** `ChannelPill` / `ChannelSlot` — the constant-value entry on Colour/Size (2A).
|
||||
- **What:** an empty Colour/Size slot offers a small underlined "or constant" text button to
|
||||
switch the channel from a field binding to a fixed `{ value }`. Confirm the copy, emphasis,
|
||||
and discoverability read well alongside the field-assign affordance.
|
||||
- **Lens:** Carbon / GOV.UK content + control emphasis.
|
||||
- **Drag-and-drop field assignment** — chips are click/keyboard-first by design; drag would
|
||||
be a progressive enhancement on top of the chooser model, not a replacement. Revisit if
|
||||
real usage asks for it (likely alongside Phase 4 faceting, where drag-to-shelf reads most
|
||||
naturally).
|
||||
|
||||
Reference in New Issue
Block a user