mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Docs: explore visual composition editing (wireframe + on-chart overlay)
This commit is contained in:
@@ -0,0 +1,164 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
|
||||||
|
Built but parked for a later pass:
|
||||||
|
|
||||||
|
- **Mark-type glyph per leaf.** A simplified icon of each unit view's mark (bar/line/point/…)
|
||||||
|
inside its box, so which-is-which reads at a glance without hovering. Needs a small mark→glyph
|
||||||
|
set (its own icon-ledger sub-family).
|
||||||
|
- **A more legible `layered` primitive.** Layers currently render as offset stacked rectangles.
|
||||||
|
A dedicated "stacked planes" metaphor — overlapping sheets/disks, like the database glyph's
|
||||||
|
stacked cylinders — would convey "one space, several layers" more elegantly than nudged boxes.
|
||||||
|
- **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.
|
||||||
Reference in New Issue
Block a user