Per-chart export: copy/download spec and PNG/SVG from the preview header

This commit is contained in:
2026-06-10 18:30:31 +03:00
parent cad19445b0
commit 1791ee9f8d
15 changed files with 921 additions and 33 deletions
+7 -4
View File
@@ -194,10 +194,13 @@ ephemeral request state, not durable data:
- **`useConfirmStore`** — the blocking confirm dialog (the `window.confirm` replacement).
- **`useNotificationStore`** — non-blocking toasts (failed saves, etc.).
- **`useSettingsPopoverStore`** — which per-pane settings disclosure is open (one
at a time); its imperative `openSettingsPopover(id)` lets the Cmd/Ctrl+, shortcut
open the editor cluster. The disclosure widget contract (gear + non-modal popover,
not an ARIA menu; Esc/focus rules) is [10 · Interaction & Feedback](10-interaction-and-feedback.md) §5.
- **`useSettingsPopoverStore`** — the single-open registry for **all** pane-header
disclosures (the per-pane settings clusters and the per-chart Export control), keyed
by id so at most one is open at once; its imperative `openSettingsPopover(id)` lets the
Cmd/Ctrl+, shortcut open the editor cluster. Header disclosures share this registry rather
than each carrying their own open-state. The disclosure widget contract (trigger +
non-modal popover, not an ARIA menu; Esc/focus rules) is
[10 · Interaction & Feedback](10-interaction-and-feedback.md) §5.
Each pairs its store with a thin **imperative trigger** exported alongside the hook —
`confirm(opts): Promise<boolean>` and `notify(opts): string` — so orchestration/services can
@@ -101,6 +101,20 @@ async function rerender(node: HTMLElement, spec: TopLevelSpec, config: Config) {
- **Do** pass `actions: false`. Astrolabe owns its own export/copy affordances;
the library's overlay menu does not belong on the preview.
- **The per-chart export goes through the handle, not the raw view.** `RenderHandle`
exposes `toImageURL('png' | 'svg', { scale, background })` so the preview's Export
control can rasterize/serialize the live chart without any component importing
`vega-embed` or touching the `View` directly — the embedding boundary holds. PNG goes
via `view.toCanvas``blob:` URL (revoked after the download); SVG via `view.toSVG`
`data:` URL. Renderer-agnostic: both work from the SVG-backed LivePreview view, since
Vega draws to its own off-screen surface here. Two non-obvious details live in the
handle, not the caller: **(1) dpr-aware scale** — the PNG is drawn at
`scale × devicePixelRatio`, so a `1×` export is as crisp as the chart on a Retina
screen (raw `toImageURL` scaleFactor ignores dpr, so a naive 1× looks half-resolution
on a 2× display). **(2) background fill** — the chart config renders a transparent
background (so the on-screen chart shows the pane colour), which would make a naive
export transparent; an opaque colour is composited under the PNG canvas and added as a
full-bleed `<rect>` to the SVG. The spec-text exports (copy / `.vl.json`) need no view.
- **SVG is the default renderer, canvas is an opt-in for many-mark previews.** SVG is
crisp/inspectable/copyable and stays the default for the editor's LivePreview. But an
SVG chart renders one DOM node per mark, so a many-mark chart (e.g. the Chart Builder's
@@ -395,9 +395,15 @@ gear carries `aria-expanded` + `aria-controls`; Enter/Space toggle; **Esc closes
focus to the gear**; an outside click closes; at most one is open at a time; focus moves to
the first control on open (so `Cmd/Ctrl+,`, which opens the editor cluster, lands inside it).
Non-modal — **no focus trap** (unlike the feature modal above). The panel is portaled to
`<body>` and positioned `fixed` because the panes clip their content. _(Consulted via /council
→ NN/g #4 consistency, #6 recognition-over-recall, #8 minimalist; WAI-ARIA APG disclosure +
menu-and-menubar; Carbon popover/overflow-menu/text-toolbar. This bullet is the contract.)_
`<body>` and positioned `fixed` because the panes clip their content. The same primitive and
single-open registry serve any pane-header disclosure, not only settings: the per-chart
**Export** control (preview header — _Import & Export → Per-chart export_) is a disclosure
whose `group` holds a few **action buttons** (Copy / Download) plus option controls. A small
set of action buttons in a disclosure stays a `group` — an ARIA menu is reserved for true
`menuitem`/`menuitemcheckbox`/`menuitemradio` command lists, which this app does not use.
_(Consulted via /council → NN/g #4 consistency, #6 recognition-over-recall, #8 minimalist;
WAI-ARIA APG disclosure + menu-and-menubar; Carbon popover/overflow-menu/text-toolbar. This
bullet is the contract.)_
**Resolved — an error names the right fix, not a boilerplate one.** Don't staple a generic
remedy onto every failure. A missing dataset reference is **not** a JSON/spec syntax problem,