mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 10:12:34 +00:00
392 lines
28 KiB
Markdown
392 lines
28 KiB
Markdown
# Lyra — Repository Review & Improvement Ideas
|
||
|
||
> **Status:** review complete (2026-06-10). Source: `vega/lyra` cloned to
|
||
> `reference/lyra` (branch `lyra2019`, last commit `fb284bf`, 2021-05-14 — unmaintained).
|
||
> **Purpose:** mine Lyra, UW IDL's _direct-manipulation_ Vega design environment, for
|
||
> interaction patterns that could improve Astrolabe's chart-building experience. This is a
|
||
> companion to [`chart-builder-research.md`](./chart-builder-research.md) (which already
|
||
> seats Voyager, also from UW IDL); Lyra is the _no-code authoring_ sibling we hadn't read.
|
||
> **Bottom line:** take Lyra's **interaction primitives and data-context UI**, not its
|
||
> architecture. Astrolabe's chart-choice intelligence (Tier B) is already _ahead_ of Lyra;
|
||
> the gaps Lyra exposes are about **how the user touches fields and sees their data**, plus
|
||
> **making our guidance actionable**.
|
||
|
||
---
|
||
|
||
## 1. What Lyra is, and the one fact that decides what transfers
|
||
|
||
Lyra lets you build a custom visualization **without writing code** — drag data fields onto
|
||
graphical mark properties, position marks with connectors, resize with handles, and define
|
||
interactions by demonstration. It targets **raw Vega** (not Vega-Lite), is a React + \*\*Redux
|
||
|
||
- Immutable.js** app on a **d3 v3\*\* runtime, and is a 2019-era research prototype that was
|
||
never finished ("does not contain all functionality" — README) and has been dormant since
|
||
|
||
2021.
|
||
|
||
The single most important architectural fact:
|
||
|
||
> **Lyra's source of truth is a _decomposed GUI model_; the Vega spec is a one-way _export_.**
|
||
|
||
Lyra keeps the design as a Redux store of typed primitives — `Mark`, `Scale`, `Guide`
|
||
(axis/legend), `Pipeline`/`Dataset`, `Signal`, `Interaction`, `Widget`
|
||
(`src/js/store/factory/*`) — and `ctrl/export.ts` (`exporter()`, 546 lines) serializes that
|
||
store **into** a Vega spec on demand. There is no inverse: Lyra **cannot import and edit an
|
||
arbitrary Vega spec**. The GUI _is_ the document.
|
||
|
||
**Astrolabe is the exact inverse.** Our source of truth is the **JSON spec** (a snippet);
|
||
the builder is a _view that emits_ JSON (`buildChartSpec`/`buildSnippetSpecText` in
|
||
`src/core/chart-builder.ts`), and the user can always drop to Monaco and hand-edit. For a
|
||
**snippet manager** this is the right call — Lyra's model would forbid the round-trip our
|
||
whole product depends on. So we take **none of Lyra's architecture** and **all of the
|
||
transferable lessons are at the interaction layer**, where the GUI-vs-JSON question doesn't
|
||
matter.
|
||
|
||
This also reframes the comparison: Lyra is not a "better chart builder" to catch up to. On
|
||
**chart-choice intelligence** Astrolabe is already further along — Lyra has no
|
||
recommendation logic at all (that was Voyager's job, which we already mined). Lyra's value to
|
||
us is the parts of the _authoring experience_ we haven't built: field-first interaction, live
|
||
data context, and one-click assistance.
|
||
|
||
---
|
||
|
||
## 2. Where Astrolabe already leads Lyra (so we don't chase the wrong things)
|
||
|
||
| Dimension | Lyra | Astrolabe (today) |
|
||
| ------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||
| Chart-choice guidance | none — manual mark + manual encoding | Tier B: smart default mark, valid-type locking, Size discipline, non-blocking warnings (`builderWarnings`) |
|
||
| Transforms in the builder | data-pipeline only (filter/formula/aggregate/lookup), not per-encoding | per-channel Aggregate / Bin / `timeUnit`, chart-level Sort / Stack |
|
||
| Target language | raw Vega (verbose, low-level) | Vega-Lite (the right altitude for "pick mark + channels") |
|
||
| Import / round-trip | impossible (GUI is the doc) | native — JSON is the doc, builder emits it |
|
||
| Stack | d3 v3 / Redux / Immutable / class components (stale) | React + Zustand + CSS Modules (current) |
|
||
|
||
The takeaways below deliberately **avoid** re-building anything in the right column.
|
||
|
||
### 2.1 — Counterpoint: Lyra exposes a far larger _control surface_
|
||
|
||
On chart-choice _intelligence_ we lead, but on **raw number of direct controls** Lyra is well
|
||
ahead. Selecting a primitive (mark / scale / axis / legend) opens an inspector of literal
|
||
knobs — and the inventory dwarfs our builder's:
|
||
|
||
| Primitive (`components/inspectors/*`) | Lyra's direct controls |
|
||
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||
| Mark — **Point** (`Symbol.tsx`) | x, y, **shape**, size, fill **color**, fill **opacity**, stroke color, stroke width |
|
||
| Mark — **Bar/Rect** (`Rect.tsx`) | X extent (start/center/end _spatial preset_), Y extent, fill color/opacity, stroke color/width |
|
||
| Mark — **Line** (`Line.tsx`) | x, y, stroke color/width, **interpolate (curve)**, tension |
|
||
| Mark — **Area** (`Area.tsx`) | baseline extent, fill color/opacity, stroke, interpolate, tension, orient |
|
||
| Mark — **Text** (`Text.tsx`) | text (expr/template), **font face/size/weight/style**, color, opacity, x/y, dx/dy offset, **align**, baseline, **rotation** |
|
||
| **Scale** (`Scale.tsx`) | **type** (linear/log/time/ordinal/band/point), **zero/nice/clamp/reverse/round**, **domain** (auto fields or manual min/max / value list), **range / colour scheme** (tableau, category10/20, sequential, diverging), padding(Inner/Outer) |
|
||
| **Axis** (`Axis.tsx`) | orient, axis-line colour/width, **title text + font/size/colour/offset**, **label size + angle + colour**, **grid on/off** + colour/opacity/width, **tick count/size/colour** |
|
||
| **Legend** (`Legend.tsx`) | orient, border, title text/size/colour, label font, symbol shape/size/fill/opacity, gradient |
|
||
|
||
And the structural multiplier: **every mark property is _droppable_** — each is _either_ a
|
||
literal value _or_ a bound field. Lyra therefore has **no fixed channel set**; you can bind
|
||
data to strokeWidth, opacity, shape, fontSize, dx… Our builder caps at **X / Y / Color /
|
||
Size** and exposes **zero** styling, scale, axis, or legend controls.
|
||
|
||
**Why this is not simply "Lyra is more capable":**
|
||
|
||
1. **It's a _builder-surface_ gap, not a capability gap.** Everything above, Vega-Lite
|
||
expresses too (mark config, `scale`/`axis`/`legend`, channels like `opacity`/`shape`/
|
||
`theta`/`row`/`column`). In Astrolabe you reach it by **hand-editing the JSON in Monaco**.
|
||
Lyra had **no editor and no round-trip** — the GUI was the _only_ way to touch the spec, so
|
||
it was forced to surface every knob. We split the work on purpose: a small **guarded**
|
||
encoding builder + a first-class JSON editor for the long tail.
|
||
2. **Lyra's breadth carries zero intelligence and high cost** — no defaults, no valid-type
|
||
locking, no guardrails, on raw low-level Vega, in an unfinished inspector.
|
||
3. So the question is not "match Lyra's control count" but **"which slices of that surface are
|
||
common _and_ awkward enough in JSON to deserve promotion into the guarded builder?"**
|
||
|
||
**Slices worth promoting (ranked):**
|
||
|
||
- **Constant-value styling** via the value-or-field channel (§3.3): fixed fill colour /
|
||
opacity / point shape / line curve without touching JSON — the most common "tweak the look."
|
||
- **Colour-scheme picker** on the Color channel (categorical / sequential / diverging) + a
|
||
measure-axis **`zero` / `log`** toggle. Palette changes are common and fiddly in JSON.
|
||
- **A few more channels** — `opacity`, `shape`, `tooltip` (already auto-enabled), **`theta`**
|
||
(unlocks pie/donut → a true part-to-whole), **`row`/`column`** facets (backlog **B8**). These
|
||
_are_ the field-shelf model (§3.4): adding channels and adding the shelf are one project.
|
||
- **Light axis/legend text** — custom **axis title**, **legend title / hide**. Common, low-risk.
|
||
|
||
**Guardrail:** promote a control only when it is _both_ common _and_ awkward in JSON.
|
||
Otherwise the clean guarded builder drifts toward Lyra's fiddly everything-inspector — the
|
||
very complexity that helped leave Lyra unfinished. Styling/scale/axis breadth for its own sake
|
||
belongs in Monaco, not the builder.
|
||
|
||
---
|
||
|
||
## 3. High-value ideas to adopt (ranked, in-scope, mapped to our code)
|
||
|
||
### 3.1 — Make `builderWarnings` **actionable** (Lyra's `Hints`) — _highest value, smallest lift_
|
||
|
||
Lyra's hint system (`components/hints/Hints.tsx`) is a contextual nudge that carries an
|
||
**action**: `{title, text, action, action_text}` — a one-click button that _applies the fix_
|
||
and clears the hint. Our guidance is strictly advisory text today: `BuilderWarning`
|
||
(`chart-builder.ts:333`) renders as a passive hint with no affordance.
|
||
|
||
Several of our existing warnings have an obvious one-click remedy and should become
|
||
**suggested actions** (Carbon "inline notification with action" / NN-g "make the system do the
|
||
work"):
|
||
|
||
- _"…draws one mark per row; aggregate the measure"_ → **[Aggregate as Sum]** sets the
|
||
measure channel's `aggregate`.
|
||
- _"…long labels; flip to a horizontal bar"_ → **[Swap X/Y]** (we already have the action —
|
||
just wire it to the hint).
|
||
- _"Two measures usually read better as a scatter"_ → **[Switch to Point]**.
|
||
- _"Area split into many colour series…"_ → **[Stack]** or **[Remove colour]**.
|
||
|
||
This keeps the Tier-B "guarded, non-blocking" philosophy — the action is an _offer_, never a
|
||
forced change — while turning a wall of advice into a guided improvement loop. Concretely:
|
||
extend `BuilderWarning` with an optional `fix?: { label: string; apply: (c) => BuilderConfig }`
|
||
(pure, lives in `chart-builder.ts`, fully unit-testable), and render the button in
|
||
`ChartBuilderModal`. **Run the copy + affordance through `/council`** (it auto-fires on
|
||
guidance copy and new interactive affordances).
|
||
|
||
### 3.2 — A live **data-table preview** in the builder (Lyra's `DataTable`) — _closes a real gap_
|
||
|
||
Confirmed gap: neither `ChartBuilderModal` nor `DatasetsModal` ever shows the **actual data
|
||
rows**. The builder picks columns from a dropdown and renders a _chart_ preview, but the user
|
||
never sees the values they're encoding. Lyra always shows the data — `DataTable.tsx` is a
|
||
paged, scrollable grid with a **per-field type icon** in each column header and hover
|
||
inspection.
|
||
|
||
A compact, read-only **data preview** (first N rows, per-column type chip in the header)
|
||
would let the user sanity-check "is this column really a date / really numeric" _before_
|
||
building — exactly when type inference is most likely to surprise them. Good homes:
|
||
|
||
- a collapsible "Data" strip in the builder's left pane (under the dataset name), and/or
|
||
- the Datasets manager, where a dataset's shape is otherwise invisible.
|
||
|
||
We already compute per-column type + cardinality + extent in `profile.ts` (the A3/A4
|
||
profiling extension), so the header chips are free; we only need the row sample. Keep it
|
||
**read-only** — editing data is out of scope (and is where Lyra's pipeline complexity lives).
|
||
|
||
### 3.3 — The unified **Property** primitive: one control = value · field · scale (Lyra's signature UI)
|
||
|
||
Lyra's best idea is `components/inspectors/Property.tsx` — a **single droppable control** that
|
||
is, depending on binding state, _either_:
|
||
|
||
- a **literal value** editor (number / color / range / select / text — `FormInputProperty`), **or**
|
||
- a **field chip** (drag a column onto it; chip shows source-vs-derived, click to unbind), **or**
|
||
- a **scale chip** (the scale the field flows through; click to unbind).
|
||
|
||
Every mark inspector is then just a **declarative list** of these — e.g. `Symbol.tsx`
|
||
declares Position/Geometry/Fill/Stroke groups as ~10 `<Property>` lines. One primitive, one
|
||
drop-to-bind gesture, one unbind gesture, reused for _everything_ including the transform
|
||
expression fields (`Filter.tsx`, `Formula.tsx` reuse the same `Property` in autocomplete
|
||
mode).
|
||
|
||
For Astrolabe this is the conceptual model for a **richer channel row**: a channel is "a field
|
||
**or** a constant **or** (future) a datum," and Vega-Lite encodes exactly that distinction
|
||
(`field` vs `value` vs `datum`). Today our channel row is field-only (column dropdown +
|
||
type segmented control). Adopting the Property model would let a channel also hold a **constant
|
||
value** (e.g. a fixed color/size) with one consistent control and a chip showing the binding
|
||
kind. This is a medium lift and a genuine capability gain, and it generalizes cleanly if we
|
||
ever add channels beyond X/Y/Color/Size.
|
||
|
||
### 3.4 — Field-first interaction: a **field shelf** you drag onto channels (Lyra drop-zones + Voyager)
|
||
|
||
Both UW tools converge on **fields as the primary objects**: Lyra drags fields onto mark
|
||
properties; Voyager has a field list with type chips you add to channel shelves. Astrolabe is
|
||
**channel-first** (pick a channel, then choose its column). A **field shelf** — the dataset's
|
||
columns listed with type chips, dragged or clicked onto channels — inverts that to match how
|
||
people actually think ("I have these fields; where do they go?").
|
||
|
||
This is the largest interaction shift here and overlaps our **deferred Tier-C / faceting**
|
||
work (`chart-builder-research.md` §8 B8/C). Recommend treating it as the **interaction
|
||
substrate for Tier C**, not a standalone task: when we build the intent-first front door and
|
||
add Row/Column facet channels, a field shelf is the natural way to assign many fields across
|
||
many channels. Note now; sequence with Tier C.
|
||
|
||
### 3.5 — One-click **type cycling** on the field chip (Lyra's `FieldType`)
|
||
|
||
Lyra's `FieldType.tsx` is a tiny, nice touch: the field's **type icon is the control** —
|
||
click it and it cycles N→O→Q→T (within the field's valid set). We already show a type
|
||
indicator next to each column option and a full `N|O|Q|T` segmented control per channel. The
|
||
cheap win is making the **per-option type chip itself interactive** (and, in a future field
|
||
shelf, the chip on each field), so type is a property of the _field_ the user can toggle in
|
||
place — fewer controls, the same guard (only valid types, via our `validFieldTypes`).
|
||
|
||
### 3.6 — Inline **expression validation** via Vega's own parser (Lyra's transform technique)
|
||
|
||
Lyra validates a filter/calculate expression with `parseExpr` from `vega-parser` **before
|
||
committing** it to the model (`Filter.tsx`, `Formula.tsx`), catching malformed expressions at
|
||
the keystroke. We don't expose raw expressions in the builder, so this isn't an immediate
|
||
builder feature — but the **technique** (validate Vega/VL expression strings with the
|
||
library's parser and surface the error inline) is directly reusable for our Monaco editor's
|
||
diagnostics and for any future calculate/filter affordance. Worth recording in
|
||
`architecture/08` (vega-editor techniques) as a known approach; Lyra even left the
|
||
"_indicate error in parsing_" TODO unfinished, so we'd be completing the idea, not copying it.
|
||
|
||
### 3.7 — **Starter examples** gallery (Lyra's `ExampleGallery`) — _cheap, optional_
|
||
|
||
Lyra's toolbar has an `ExampleGallery` modal that `hydrate()`s a curated example into the
|
||
editor — a "start from something" affordance. Astrolabe's snippet library is the natural home
|
||
for a small set of **curated starter snippets** (one per FT intent category we cover —
|
||
Magnitude/Bar, Change-over-Time/Line, Correlation/Point…). Low effort, improves first-run and
|
||
doubles as living documentation of what the app does well. Lower priority than 3.1–3.2.
|
||
|
||
### 3.8 — **Per-chart export: image + standalone spec** (Lyra's `Export` menu) — _biggest miss; cheap_
|
||
|
||
The one squarely-useful idea I almost overlooked. Lyra's toolbar `Export` (`components/toolbar/Export.tsx`)
|
||
offers **PNG**, **SVG**, **JSON spec**, and a **standalone HTML** scaffold — using
|
||
`view.toImageURL(type)` on the live Vega view.
|
||
|
||
Astrolabe's export today is **workspace-JSON backup only** (`docs/spec/08`) — there is **no way
|
||
to get a single chart out** as an image, as its own spec file, or onto the clipboard. Yet our
|
||
renderer already holds exactly the Vega `view` Lyra exports from (`services/chart-renderer.ts`,
|
||
`renderer: 'svg'`), so this is a handful of lines:
|
||
|
||
- **Copy spec** (clipboard) and **Download `.vl.json`** for the active snippet — the most-asked
|
||
"get my chart out" actions, both trivial given the snippet _is_ the spec.
|
||
- **Download PNG / SVG** of the rendered chart via `view.toImageURL('png'|'svg')` — drop a chart
|
||
into a doc/slide without a screenshot.
|
||
- (Optional) **standalone HTML** — a self-contained vega-embed page; ties to the
|
||
share/"private-move" direction in `monetization-and-sync-exploration.md`.
|
||
|
||
Home: a snippet-level "Export / Share" affordance (library row action or editor toolbar),
|
||
distinct from the workspace Export. High value-to-effort; should jump near the top of the queue.
|
||
|
||
### 3.9 — Scale / axis / legend as **auto-derived _override_ panels** (Lyra's `bindChannel` pipeline)
|
||
|
||
If we ever promote scale/axis/legend controls (§2.1), Lyra shows the right _editing model_.
|
||
Binding a field in Lyra runs a pipeline (`actions/bindChannel/`): `parseScales` + `parseGuides`
|
||
**auto-materialize** the scale and axis/legend, `aggregateDependencies` re-points them when the
|
||
data is aggregated, and **`cleanupUnused`** garbage-collects scales/datasets no longer
|
||
referenced. The user edits a scale/axis only to _override_ an auto-derived default.
|
||
|
||
Vega-Lite already auto-derives scales/axes/legends from encodings, so we get the inference for
|
||
free — the borrow is purely UX: surface scale/axis/legend as **optional override panels that
|
||
start empty (inheriting VL defaults) and only emit JSON when touched**, and **clearing a channel
|
||
drops its overrides** (the `cleanupUnused` lesson — no orphaned `scale`/`axis` config left behind).
|
||
This is the discipline that keeps §2.1's promoted controls from bloating the output spec.
|
||
|
||
### 3.10 — Expression **autocomplete with field/signal chips** (Lyra's `AutoComplete`)
|
||
|
||
`components/inspectors/AutoComplete.tsx` is a `contenteditable` expression field that
|
||
autocompletes **dataset field names** (and signals), rendering each `datum.field` reference as a
|
||
styled, non-editable **chip**. For any future filter/calculate affordance — and for Monaco
|
||
completions in expression positions — autocompleting the dataset's own column names (we already
|
||
have the schema from `profile.ts`) is the high-value half; the chip rendering is polish. Niche
|
||
until we expose expressions, but cheap to remember.
|
||
|
||
### 3.11 — **Builder-state undo/redo** (Lyra's `redux-undo`) — _low priority_
|
||
|
||
Lyra makes the whole design undoable (`redux-undo`, global `vis.present`/`past`). Our builder
|
||
modal has **no undo** (Monaco covers the _editor_, not builder config). A session-scoped
|
||
undo/redo — or even a lighter **"reset to smart defaults"** — would soften experimentation in
|
||
the builder. Low priority: the builder is a short-lived modal and the smart default already
|
||
gives a sane starting point. Noted for completeness, not urgency.
|
||
|
||
### 3.12 — **Dataset-level transforms: Filter / Calculate** (Lyra's data pipeline) — _the cleanest gap_
|
||
|
||
The transform dimension where Astrolabe and Lyra differ most. The key fact is that **Vega-Lite
|
||
has two transform layers**:
|
||
|
||
1. **Inline encoding transforms** — `aggregate` / `bin` / `timeUnit` / `sort` / `stack` written
|
||
straight onto a channel. **This is the layer Astrolabe's builder uses**, and uses _well_ —
|
||
per-channel and guarded, arguably cleaner than Lyra (whose Aggregate is a separate pipeline step).
|
||
2. **Top-level `transform: []`** — dataset-level operations applied in sequence before encoding:
|
||
**`filter`**, **`calculate`**, **`lookup`**, window, fold, pivot, regression… **Astrolabe's
|
||
builder exposes _none_ of these.** Lyra's data pipeline is built almost entirely _around_ them
|
||
(`components/pipelines/transforms/`: Filter, Formula, Lookup, Sort — each an ordered card in a
|
||
`TransformList` on `datasets.<id>.transform`).
|
||
|
||
The operations in layer 2 have **no encoding-level shorthand** — there is no way to express them
|
||
except by hand-editing the JSON. The two worth borrowing:
|
||
|
||
- **Filter _(highest value)_** — row filtering (`transform: [{filter: …}]`). The single most
|
||
common data-shaping need, and the builder is _already telling users to do it_ — three existing
|
||
`builderWarnings` literally say "filter to fewer categories / to non-negative values"
|
||
(`chart-builder.ts:475,476,511`) while offering no way to. Guarded form: a **field + operator +
|
||
value** predicate shelf (Voyager-style), no expression required for the common case; power form:
|
||
a raw `datum.…` expression validated with Lyra's `parseExpr` technique (§3.6) + field
|
||
autocomplete (§3.10). VL applies top-level transforms _before_ encoding aggregation, so "filter
|
||
raw rows, then the chart aggregates" is the natural — and usually wanted — default; filtering on
|
||
an _aggregated_ value (HAVING) is the advanced case, defer it.
|
||
- **Calculate / derived field** — `transform: [{calculate: "datum.a / datum.b", as: "ratio"}]`,
|
||
exactly Lyra's `Formula`. The new field then appears in the column dropdowns like any other.
|
||
High value, second to Filter.
|
||
- **Lookup (join a second dataset)** — Lyra's `Lookup` joins one pipeline's fields into another by
|
||
key. Astrolabe references **one dataset per snippet**, so this is a larger data-model change —
|
||
note and **defer**.
|
||
|
||
**Where it lives:** Lyra keeps these in a separate _dataset_ pane, distinct from the encoding
|
||
inspector — the right instinct. In Astrolabe they belong in a **"Data" section in the builder's
|
||
left pane, above the channels**, paired with the §3.2 data-table preview: _here are your rows
|
||
[+ Filter] [+ Calculate] → now encode them_, with the preview table updating live as filters
|
||
apply. This composes cleanly and keeps the channel rows about encoding, not data prep.
|
||
|
||
**Scope fit:** a field+operator+value filter shelf is squarely Tier-B "smart + guarded," closes
|
||
the loop on warnings the builder already emits, and is arguably **higher value than faceting (B8)**
|
||
— it should enter the §8 backlog as a new first-class item (it is absent today).
|
||
|
||
---
|
||
|
||
## 4. Anti-recommendations — what to deliberately **not** take from Lyra
|
||
|
||
These are as load-bearing as the adopt list; Lyra is a research prototype and several of its
|
||
defining features are traps for us.
|
||
|
||
- **The decomposed GUI-as-document architecture** (`store/factory/*` + one-way `export.ts`).
|
||
It forbids the spec→builder round-trip our product is built on. Our JSON-source-of-truth +
|
||
emit model is correct; keep it.
|
||
- **Interaction-by-demonstration** (`ctrl/demonstrations.ts` — **1454 lines** that synthesize
|
||
raw Vega signals/selections from brush/click/hover, plus `signals.ts`/`listeners.ts`).
|
||
Impressive research, wrong layer for us: **Vega-Lite's `params`/selections** are the right
|
||
abstraction. _If_ we ever add interactivity, expose VL selections as a small guarded builder
|
||
action — never port Lyra's signal generator.
|
||
- **The direct-manipulation canvas** (handles, connectors, `manipulators.ts`,
|
||
`transforms/manipulators/*`). Enormous surface tied to the Vega-runtime model, and it
|
||
competes with — rather than complements — our JSON-first-with-guardrails value proposition.
|
||
- **The stack**: d3 v3, Redux, Immutable.js, `react-modal`, class components, `datalib`. We're
|
||
React + Zustand + CSS Modules with our own modal registry (`architecture/03`). **Take ideas,
|
||
read no code into the repo.** (Per `AGENTS.md`: no shared lib, patterns adapted not imported.)
|
||
- **The full data-pipeline editor** (lookup/aggregate/formula transform _chains_ as a
|
||
first-class pipeline UI). Our per-channel transforms cover the in-scope need; a general
|
||
pipeline editor is a different, much larger product.
|
||
|
||
---
|
||
|
||
## 5. Recommended sequence (mapped to the existing backlog)
|
||
|
||
> **Consolidated (2026-06-10):** this sequence is now merged with
|
||
> `chart-builder-research.md` §8 into the forward plan at
|
||
> [`chart-builder-enhancement-scope.md`](./chart-builder-enhancement-scope.md) (Tier-C
|
||
> target). The list below is the original Lyra-side reasoning; the scope doc §4 is the
|
||
> authoritative build order.
|
||
|
||
Slot these into `chart-builder-research.md` §8 rather than inventing a new track:
|
||
|
||
1. **Actionable hints** (§3.1) — extend `BuilderWarning` with an optional pure `fix`; wire the
|
||
four obvious ones; `/council` the copy. _Small, high-value, pure-core + thin UI._ **Do first.**
|
||
2. **Per-chart export** (§3.8) — Copy spec / Download `.vl.json` / PNG / SVG for a snippet, from
|
||
the `view` the renderer already holds. _Highest value-to-effort; near the top._
|
||
3. **Filter (+ Calculate) dataset transforms** (§3.12) — a guarded field/operator/value filter
|
||
shelf (then a derived-field calculate) in a "Data" section above the channels. _Closes the gap
|
||
the builder's own warnings point at; not in the backlog today; arguably > faceting._
|
||
4. **Data preview** (§3.2) — read-only row sample + header type chips in the builder (and
|
||
Datasets manager); reuse `profile.ts` stats. _Closes a confirmed gap; mostly UI; pairs with #3._
|
||
5. **Value-or-field channels via the Property model** (§3.3) — let a channel hold a constant
|
||
`value`, not just a `field`; one consistent control. _Capability gain; medium._ Pairs with the
|
||
§2.1 control-promotion shortlist (colour scheme, `zero`/`log`, axis/legend titles).
|
||
6. **Inline expression validation + field autocomplete** (§3.6, §3.10) — record the `parseExpr`
|
||
technique in `architecture/08`; apply when the Filter/Calculate expression mode (#3) lands. _Build with the feature._
|
||
7. **Field shelf + in-place type chip** (§3.4, §3.5) — design as the **Tier-C / faceting +
|
||
added-channels substrate** (§2.1: shape/opacity/theta/facets), not standalone. _Largest; with Tier C._
|
||
8. **Starter examples** (§3.7) — curated seed snippets, one per covered FT intent. _Cheap, optional._
|
||
9. **Scale/axis override panels + builder undo** (§3.9, §3.11) — only alongside control promotion;
|
||
override-panels-that-emit-only-when-touched, with orphan cleanup. _Deferred polish._
|
||
|
||
Verification for each lands the usual way: pure rules get `chart-builder.test.ts` cases;
|
||
UI/affordance changes get a manual pass against the live builder (a green build proves nothing
|
||
about what the user sees — `AGENTS.md`). Items 1 and 3 are mostly `src/core/` and squarely fit
|
||
our "core-first, tested hardest" rule.
|
||
|
||
---
|
||
|
||
_Citations are to `reference/lyra/src/js/…` (cloned, branch `lyra2019`). Lyra and Voyager are
|
||
both UW IDL; Voyager is already seated in `chart-builder-research.md`, so this review focuses
|
||
on the no-code-authoring ideas Voyager doesn't cover. Lyra is an engineering/interaction
|
||
source, not a council seat._
|