Files
astrolabe/docs/exploration/multi-view-data-model-scope.md
T

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/renderingprepareSpecForRender); 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-level data block.
  • Data inspector (DataInspector) surfaces one input + one resolved table; a composition produces several source_<n>/data_<n>.
  • deriveSnippetName (core/snippet) reads top-level mark/encoding only, 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 null for composed specs, so they stay Monaco-only (correct).

Vega-Lite fidelity clashes

  1. 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 (spurious DatasetNotFoundError, or clobbered inline values). Resolved by the core/spec-data classifier (M1).
  2. Shadowing — a library dataset whose name equals a self-defined datasets key is silently ignored (self-defined wins). Documented precedence; candidate for a user-facing note, no code change required.
  3. 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.
  4. 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-data classifier mirroring isNamedData; spec-refs + rendering routed through it. Closes clash 1.
  • M2 — view-scoped editor context dataBindingAtPath (climb the cursor's JSON path to the nearest enclosing data, honoring Vega-Lite's parent→child inheritance) + derivedFieldNamesAtPath (ancestor-chain as outputs). active-dataset is cursor-scoped (dataInfoAt(text, offset)), resolving columns for every form (library ref case-insensitive, inline, named-inline, self-defined datasets, 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-views enumerates the distinct tables the marks draw (from the compiled Vega spec's from.data + data[].source lineage), each with its input + resolved ends; RenderHandle.inspectData() returns those tables with rows; DataInspector adds a SelectControl view picker (hidden for the single-table case), labels via inspectViewLabel (named dataset or "View N", never compiler names) + a columns · rows cue. Enumerating by drawn table (not authored view) is forced by Vega-Lite desugaring (a point: true line compiles to two layers). Replaced the single-pair core/result-data.
  • M5 — live / interactive inspection — make the inspector react to interactive selections. A selection-as-filter (filter: {param}) recomputes a downstream view's data_N live, so the inspector should re-read on selection change to show the brushed result ("what am I visualizing now"); a selection-as-highlight (a condition encoding) changes no data, so nothing to react to. Needs a refresh model beyond the per-render renderEpoch: subscribe to the live view (view.addDataListener / selection signals), debounced (a brush drag pulses continuously — latency/interaction /council pass), and a default-on-vs-toggle choice. Selection *_store tables 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.