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:
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user