6.0 KiB
Multi-view data model — scope & plan
Astrolabe authors arbitrary Vega-Lite, including composed specs (layer,
hconcat/vconcat/concat, facet, repeat). Most of the spec-structure
machinery already handles composition; the data-facing features carried
single-view assumptions. This memo records the assessment, the data-model
contract that anchors the work, and the milestone plan to make multi-view support
durable. The guiding constraint: extend Vega-Lite, never break it — every
native data form must keep working.
The data-model contract
A dataset reference is exactly Vega-Lite named data: a data block with a
string name and no values/url/generator key, whose name the spec does not
self-define via top-level datasets. This mirrors Vega-Lite's isNamedData
(reference/vega-lite/src/data.ts). The classification is owned by
core/spec-data (classifyData, libraryRefName); reference extraction
(spec-refs), rename (spec-refs), and render-time resolution (rendering) all
route through it. See docs/architecture/07 §3.1.
Library references resolve to inline data before embedding
(core/rendering → prepareSpecForRender); a self-defined datasets name is
left for Vega-Lite to resolve natively.
Assessment: already multi-view vs. single-view assumptions
Already composition-aware (recurse all view operators): ref extraction/rename
(spec-refs), reference resolution + fit-mode (rendering), structural wrap/
unwrap/add-view (spec-transforms, spec-cursor, spec-insert), derived-field
collection (spec-fields), config baking (spec-config), standalone export
(chart-export, reuses prepareSpecForRender).
Single-view assumptions (the work):
- Editor data context (
app/services/active-dataset) resolves one dataset for the whole draft (first ref, or first inline data), with no notion of which view the cursor sits in. Completion/hover/inlay (spec-dataset-hints) and the facet/repeat field defaults (spec-transform-actions) therefore offer the wrong view's columns in a composition whose views bind different datasets. - Extract inline data (
app/stores/ExtractStore) lifts only the top-leveldatablock. - Data inspector (
DataInspector) surfaces one input + one resolved table; a composition produces severalsource_<n>/data_<n>. deriveSnippetName(core/snippet) reads top-levelmark/encodingonly, so a composed spec falls back to the default name (graceful, not a bug).- Chart builder is single-view by design; its strict round-trip hydration
returns
nullfor composed specs, so they stay Monaco-only (correct).
Vega-Lite fidelity clashes
- Named inline/url data misread as a reference — classifying on "has a
string
name" alone caught named-inline ({ name, values }) and named-url, breaking valid specs (spuriousDatasetNotFoundError, or clobbered inline values). Resolved by thecore/spec-dataclassifier (M1). - Shadowing — a library dataset whose name equals a self-defined
datasetskey is silently ignored (self-defined wins). Documented precedence; candidate for a user-facing note, no code change required. - Case-rule split — library matching is case-insensitive (
naming.ts); self-defined exclusion and Vega-Lite's own named-data lookup are case-sensitive. These are distinct namespaces, so the split is defensible; minor. - Runtime-injected named data — Vega-Lite allows binding
{ name }at runtime; Astrolabe always pre-resolves, so an imported spec relying on runtime injection won't render. Out of scope.
Milestone plan
- M1 — data-model foundation ✅ —
core/spec-dataclassifier mirroringisNamedData;spec-refs+renderingrouted through it. Closes clash 1. - M2 — view-scoped editor context ✅ —
dataBindingAtPath(climb the cursor's JSON path to the nearest enclosingdata, honoring Vega-Lite's parent→child inheritance) +derivedFieldNamesAtPath(ancestor-chainasoutputs).active-datasetis cursor-scoped (dataInfoAt(text, offset)), resolving columns for every form (library ref case-insensitive, inline, named-inline, self-defineddatasets, url/generator → none); the three Monaco providers and the facet/repeat defaults pass the cursor offset. - M4 — multi-view inspection ✅ (pulled ahead of M3) —
core/inspect-viewsenumerates the distinct tables the marks draw (from the compiled Vega spec'sfrom.data+data[].sourcelineage), each with its input + resolved ends;RenderHandle.inspectData()returns those tables with rows;DataInspectoradds aSelectControlview picker (hidden for the single-table case), labels viainspectViewLabel(named dataset or "View N", never compiler names) + acolumns · rowscue. Enumerating by drawn table (not authored view) is forced by Vega-Lite desugaring (apoint: trueline compiles to two layers). Replaced the single-paircore/result-data. - M5 — live / interactive inspection — make the inspector react to interactive
selections. A selection-as-filter (
filter: {param}) recomputes a downstream view'sdata_Nlive, so the inspector should re-read on selection change to show the brushed result ("what am I visualizing now"); a selection-as-highlight (aconditionencoding) changes no data, so nothing to react to. Needs a refresh model beyond the per-renderrenderEpoch: subscribe to the live view (view.addDataListener/ selection signals), debounced (a brush drag pulses continuously — latency/interaction/councilpass), and a default-on-vs-toggle choice. Selection*_storetables are not drawn, so the M4 enumeration already ignores them. - M3 — view-scoped extract — seed Extract from the focused view's inline data
(reusing the cursor-scope machinery) and rewrite that view's
data.
Delivery is incremental, one milestone per commit, verified against real behavior.
The consolidated data-model contract write-up into docs/architecture (05/08)
lands once the shape is final.