Files
astrolabe/docs/exploration/visual-composition-editing-exploration.md
T
oleh 26b383ff33 Editor: render a layer as one framed row of mark glyphs
A layer is one plotting space with several marks stacked in z-order, so it now renders as a single frame holding its child marks as a row of glyphs, badged with a new 'layers' glyph — instead of offset stacked cards that read as separate spaces and hid the lower glyphs. Each mark stays an individual treeitem, so selection and Alt+up/down z-order reorder are unchanged.
2026-06-29 14:59:01 +03:00

169 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Visual Composition Editing — Exploration
> **Status:** research recorded 2026-06-29. Point-in-time record of a feasibility study for a
> **visual, drag-editable view of multi-view composition** (`vconcat`/`hconcat`/`concat`/
> `layer`/`facet`/`repeat`). Two surfaces were weighed: a **schematic wireframe panel** and an
> **on-chart overlay** aligned to the real rendered chart.
>
> **Decision (2026-06-29):** build the **schematic wireframe first** — it carries ~90% of the
> value with a deterministic spec↔box mapping and no coupling to Vega runtime internals. The
> **on-chart overlay** is a feasible later "geometry skin" over the same edit core, deferred
> because its risk (compiled-name↔source-path correlation, and an edit-vs-interact pointer
> conflict) is real and isolated. First build step: a **read-only Phase A spike** —
> `viewTree(spec)` + a static nested-box renderer with click-to-cursor sync.
>
> **Shipped (2026-06-29):** Phases AC — the wireframe, mark-type leaf glyphs, in-container
> reorder (drag + `Alt+↑/↓`), and cross-container drag-to-restructure (`core/spec-restructure`
> `wrapViews`). The live contract is now arch 08 (transforms) + arch 10 (interaction). Phase D
> (on-chart overlay) and Phase E (size editing) remain deferred — size deferred by choice.
---
## 1. The brief
A visual, interactive representation of a spec's multi-view structure — boxes for the views,
showing arrangement and nesting (what contains what), draggable to rearrange, with the spec
reacting. Explicitly **not** a chart preview: a wireframe of blocks. Reference feel: a Tableau
dashboard's GUI.
## 2. The framing realization: a tree of tiled containers, not a canvas
Vega-Lite composition is a **nested tree**, not a free 2D plane:
- `layer` / `hconcat` / `vconcat` / `concat` hold an **array** of child views.
- `facet` / `repeat` hold a **single, data-generated** child (`spec`), not an array.
So the apt analogy is Tableau's **tiled containers** (nested horizontal/vertical), not Tableau's
**floating** layout. Every drag must resolve to a discrete tree operation — reorder within a
container, move across containers, wrap siblings into a new container, flip orientation, unwrap —
never an arbitrary `(x, y)` drop. Communicating that constraint _as_ the design (snap-to-zones,
not free placement) is the central UX problem.
## 3. Two surfaces, one edit core
Both surfaces feed the **same mutation core** and differ only in where the boxes come from.
| | **Schematic wireframe** | **On-chart overlay** |
| -------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| Box geometry | Computed from the source tree | Read from Vega's rendered scenegraph |
| Vega-runtime dependency | None | Hard (`view.scenegraph()`) |
| Path ↔ box mapping | **Deterministic** (we own every path) | **Brittle** — correlate compiled group names (`concat_0_group`, `child__a_group`) back to source paths |
| Reflects true rendered sizes | No (schematic; equal-weight unless explicit `width`/`height`) | Yes (pixel-accurate) |
| Works on mid-edit / invalid JSON | Yes (error-tolerant parse) | No (needs a successful render) |
**Geometry sync fidelity (overlay).** High. `view.scenegraph()` exposes each sub-view as a
`SceneGroup` with exact `bounds`/`width`/`height`; the default **SVG** renderer also yields real
DOM `<g name="…">` nodes measurable via `getBoundingClientRect()`. The app already holds the live
`view` handle (the Inspector reads live data from it), so exposing the scenegraph is a small
`RenderHandle` extension. The fidelity ceiling is not geometry — it is the three overlay risks in
§6.
## 4. Existing infrastructure vs. new work
**Reusable today** (much of it from the cursor-scoped CodeLens work):
- Mutations & paths: `compositionTargetAt` / `insertView` / `moveView` / `elementOffset`
(`core/spec-insert`), wrap/unwrap with property-partition rules (`core/spec-transforms`:
`SHARED_TOP`/`LAYER_TOP`, `placeholderView`, `unwrapSingleton`, `ARRAY_COMPOSITIONS`).
- Data model: `dataBindingAtPath` (`core/spec-data`) for data inheritance.
- Write-back: the whole-document reformat + paired `pushUndoStop` path (`spec-transform-actions`),
so a drag is one ⌘Z.
- UI scaffolding: pointer-drag hook `useResizeDrag` (no DnD library in use), resizable-panel
scaffolding (`PanesStore`/`AppStore`), the preview pane's stacked layout (header / chart /
`DataInspector`).
- Render handle: `chart-renderer` keeps `result.view`; SVG by default.
**New work, roughly in build order:**
- **`viewTree(spec)`** — recursive source-spec → tree of `{ kind, path, label, children, sizeHint }`.
Small, pure core. No tree builder exists today; `inspectableViews` walks the _compiled_ vg spec
for data tables, which is the wrong layer for structure.
- **Cross-container mutations** — `moveViewTo(from, toContainer, index)`, `wrapSiblings(...)`
(drop-creates-container), `removeView` + collapse. **Medium-large and correctness-sensitive**
the real cost and the subtle bugs live here (§6).
- **Wireframe renderer** — nested flex boxes from `viewTree`, plus drag + drop-zones +
create-container zones, and selection↔cursor sync (reuses cursor plumbing).
- **(Overlay only)** scenegraph→path correlation, an absolutely-positioned overlay with
coordinate transforms, re-sync on every render, and an edit⇄interact mode toggle.
## 5. Effort & phasing
- **Phase A — read-only wireframe.** `viewTree` + static nested-box renderer + click-to-cursor
sync. Small. De-risks the model with zero mutation risk.
- **Phase B — reorder within a container.** Drag → `moveView` (exists). Small.
- **Phase C — cross-container move + wrap-on-drop + delete/collapse.** Medium-large. The core
value and the correctness work.
- **Phase D — on-chart overlay (optional).** Geometry skin over the proven core. Medium; risk
isolated to correlation + mode conflict.
- **Phase E — resize handles → `width`/`height` (optional).** Medium.
## 6. Edge cases & hidden problems
**Composition model**
- **Facet/repeat cells are data-generated** — count depends on data cardinality (unknown without
running), and individual cells are not arrangeable (they do not exist in the source). Render as
one "grid" placeholder with a badge.
- **`layer` is z-order, not spatial** — children coincide in one box; needs a depth/stack
metaphor, and in the overlay the layer rectangles overlap (ambiguous hit-testing).
- **`concat` + `columns: N`** is a wrap-grid — a third layout mode beside pure h/v.
- Deep nesting → tiny boxes (min-size + zoom/scroll); mixed orientations recurse.
**Mutation correctness (the subtle traps)**
- **Property migration across container types.** `width`/`height` live on the _child_ in concat
but on the _wrapper_ in layer; `data`/`resolve` on the wrapper. A cross-type move must relocate
these or the view silently renders wrong. The partition rules exist for wrap; cross-move needs
the analogue.
- **Data-inheritance breakage.** A child with no explicit `data` inherits its nearest ancestor's.
Moved under a different data source it silently rebinds; detect via `dataBindingAtPath` and
pin the effective data onto the moved view (a real decision, not free).
- **Degenerate drops** — onto itself, into its own descendant (cycle), or a move that empties /
single-childs a container (collapse via `unwrapSingleton`, which can strand a `resolve`/`spacing`
that no longer has a composition to apply to).
**Round-trip & sync**
- Specs are **plain JSON; reformat strips comments** and rewrites the whole document — already
true of the existing transforms, so consistent.
- Source of truth is the draft text; wireframe and editor both mutate it → reuse the atomic
write-back + undo-stop path.
- Mid-edit invalid JSON: wireframe degrades to last-valid; overlay has no fresh render to track.
**Overlay-specific**
- **Compiled-name ↔ source-path correlation is an undocumented compiler contract** — can shift
across Vega-Lite versions and is ambiguous for layers and facet internals. The overlay's biggest
risk.
- **Edit-overlay vs. the chart's own interactivity.** With `params`/brush/pan-zoom selections, an
editing overlay steals the pointer events those selections need → requires an explicit
**edit ⇄ interact** mode toggle, an interaction split the wireframe avoids.
- The view is **finalized and recreated each render**, so the overlay re-measures every time (brief
flicker) and must track scroll/resize/DPR and `autosize`-container coordinate transforms.
**Accessibility**
- Drag-and-drop needs a keyboard path (Move up/down exist; cross-container needs a keyboard
equivalent), per the WAI-ARIA APG drag-and-drop pattern, plus reduced-motion. A `/council`
item before the interaction is built.
## 6a. Deferred polish (Phase A follow-ons)
- **Mark-type glyph per leaf.** _(Shipped.)_ A simplified glyph of each unit view's mark inside
its box (the `mark-*` icon sub-family), so which-is-which reads at a glance.
- **A more legible `layered` primitive.** _(Shipped.)_ A layer renders as **one frame** holding
its child marks as a row of glyphs, badged as layered (the `layers` glyph) — not separate boxes,
so it reads as one space and stays distinct from a concat (which is box-per-view). The
overlapping/stacked-planes options weighed here were rejected: at glyph scale, overlapping
line-art muddies the very marks the glyphs exist to show; legibility beat the z-order-depth cue.
- **Hide the affordance for single-view specs.** _(Shipped.)_ The toolbar glyph appears only when
the spec has a composition.
## 7. Recommendation
Build the schematic wireframe (Phase A → C) first: deterministic mapping, no Vega-internal
coupling, works mid-edit, no pointer conflict with chart interactivity. Treat the on-chart overlay
as an optional later skin over the same proven edit core. Start with the **Phase A spike**
(`viewTree` + static nested boxes + click-to-cursor) to make the model concrete before committing
to the mutation work.