Editor: Vega-Lite expression intelligence; single-home render errors

This commit is contained in:
2026-07-01 05:00:47 +03:00
parent 9b2618cac6
commit da6a675982
20 changed files with 1001 additions and 260 deletions
@@ -538,11 +538,12 @@ blank mid-edit.
### A second preview surface: the Chart Builder
The editor's `LivePreview` is **bound to the snippet editor** — it reads `SnippetStore`
(shown spec), `AppStore` (fit mode/theme), and `PreviewStore` (shared error). The
**Chart Builder modal** needs a preview of a _different_ spec source (its config), so it
does **not** reuse `LivePreview`; it runs its own small debounced render over the same
`chart-renderer.renderSpec` + `prepareSpecForRender`, with **local** error state (never the
shared `PreviewStore`, which would cross-talk with the editor). Two preview surfaces, one
(shown spec) and `AppStore` (fit mode/theme), and keeps its render status (`error`/`busy`) in
its own local state. The **Chart Builder modal** needs a preview of a _different_ spec source
(its config), so it does **not** reuse `LivePreview`; it runs its own small debounced render
over the same `chart-renderer.renderSpec` + `prepareSpecForRender`, with its own **local**
error state. Each preview surface owns its render status locally — no shared store to
cross-talk. Two preview surfaces, one
renderer service. Builder flow: `chart-builder.ts` (pure spec assembler) → `ChartBuilderStore`
(config + create) → `ChartBuilderModal`'s `BuilderPreview`. Reach for a reusable preview
component only if a _third_ surface appears.
@@ -603,13 +604,13 @@ the existing view is re-measured via a `ResizeObserver`-driven event — see §8
A spec that cannot be rendered must produce a **readable** message in the preview
area and recover on its own once the spec is valid again. Errors arise at three
stages, all funneled to one error field the preview reads:
stages, all funneled to the one error state the preview owns:
| Stage | Failure | Surfaced as |
| -------------------------------- | -------------------------------------- | ---------------------- |
| Parse | Invalid JSON | "Invalid JSON: …" |
| Prepare (`prepareSpecForRender`) | Referenced dataset missing/unfetchable | "Dataset not found: …" |
| Embed (`vega-embed`) | Vega-Lite compile / data error | "Rendering error: …" |
| Stage | Failure | Surfaced as |
| -------------------------------- | -------------------------------------- | --------------------------------- |
| Parse | Invalid JSON | `Invalid JSON · …` |
| Prepare (`prepareSpecForRender`) | Referenced dataset missing/unfetchable | `Dataset "x" not found · …` |
| Embed (`vega-embed`) | Vega-Lite compile, or a bad expression | `Line N · …` / `Render error · …` |
```ts
// inside render(), driven by the debounced renderer
@@ -620,7 +621,7 @@ async function render(): Promise<void> {
if (!text) {
current?.destroy();
current = null;
usePreviewStore.getState().setError(null);
setError(null); // `error`/`busy` are the pane's local state, not a store
return;
}
@@ -628,7 +629,7 @@ async function render(): Promise<void> {
try {
parsed = JSON.parse(text);
} catch (e) {
usePreviewStore.getState().setError(`Invalid JSON: ${(e as Error).message}`);
setError(`Invalid JSON · ${(e as Error).message}`);
return; // keep the last good chart underneath the error, or show the message
}
@@ -638,14 +639,9 @@ async function render(): Promise<void> {
const config = chartConfigFor(uiTheme);
current?.destroy();
current = await renderSpec(node, prepared, config);
usePreviewStore.getState().setError(null); // success clears any prior error
setError(null); // success clears any prior error
} catch (e) {
usePreviewStore
.getState()
.setError(
`Rendering error: ${(e as Error).message}. ` +
`Check your JSON syntax and that the spec is valid Vega-Lite.`,
);
setError(`Render error · ${(e as Error).message}`);
}
}
```
@@ -660,10 +656,10 @@ manual retry, no reload.
- **Do** treat empty/blank spec text as "render nothing" — finalize the current
view, clear the error, show a clean empty pane.
- **Do** clear the error state on every successful render.
- **Do** make messages legible and actionable (the underlying reason plus a hint
to check JSON/Vega-Lite validity), never a raw stack trace dump.
- **Do** distinguish the failing stage in the message (Invalid JSON vs Dataset
not found vs Rendering error).
- **Do** make messages legible and actionable — lead with the location or the failing
stage, then the underlying reason — never a raw stack trace dump.
- **Do** distinguish the failing stage in the message (invalid JSON vs missing dataset
vs a bad expression or render error).
- **Don't** show a broken/partial chart — replace the chart area with the
message.
- **Don't** require a manual "retry"; validity restores the chart on its own.
@@ -729,5 +725,5 @@ bookkeeping. Gate the observer to responsive modes (Original needs no re-fit).
| Field names | `escapeVegaField` on every data-derived `field:` | `src/core/rendering.ts` |
| Debounce | Inline timer; `0` on buffer-load/view-switch, `renderDebounce` on keystroke (§5) | `LivePreview.tsx` (service not yet extracted) |
| Spec prep | `prepareSpecForRender` (pure, on a copy) | `src/core/rendering.ts` (see _Live Preview_) |
| Errors | One error field, cleared on success, empty = nothing | `PreviewStore.error` |
| Errors | One error field, cleared on success, empty = nothing | `LivePreview` local state (`error`/`busy`) |
| Container fit | Inner host + frame (out-specify `.vega-embed`); resize via synthetic `window:resize` | §8 (`LivePreview` + `chart-renderer`) |