mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 10:12:34 +00:00
Chart builder: data-aware defaults, one-click hint fixes, canvas preview, fullscreen modal
This commit is contained in:
@@ -27,15 +27,85 @@ export interface RenderHandle {
|
||||
resize(): void;
|
||||
}
|
||||
|
||||
/** Embed a prepared spec into `node`. Non-negotiable: no actions menu, SVG output. */
|
||||
export interface RenderOptions {
|
||||
/**
|
||||
* Renderer backend. **Default `'svg'`** — crisp at any zoom, themeable, the
|
||||
* contract default for the editor's LivePreview (docs/architecture/05 §2). The
|
||||
* Chart Builder preview passes **`'canvas'`**: an SVG chart with thousands of
|
||||
* marks (e.g. one bar per row of a 10k-row dataset) costs *seconds* of
|
||||
* main-thread layout/paint per render — measured ~6.5s paint on 9994 rows —
|
||||
* because each mark is a DOM node; canvas is a single node and paints in
|
||||
* milliseconds. Canvas is raster (not crisp on zoom) but that's invisible for an
|
||||
* ephemeral preview, and image export (`view.toImageURL`) is renderer-agnostic.
|
||||
*/
|
||||
renderer?: 'svg' | 'canvas';
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum canvas side length, in CSS px before the device-pixel-ratio multiplier.
|
||||
* Browsers cap a canvas backing store at ~32767px per side (Chrome/Firefox; Safari
|
||||
* is lower and area-bound); past that the canvas fails to allocate and draws
|
||||
* nothing. SVG has no such cap. `renderSpec` measures a canvas chart's resolved
|
||||
* size against this (÷ dpr, since the backing store is dpr× the CSS size) and
|
||||
* throws `ChartTooLargeError` rather than handing back a blank canvas.
|
||||
*/
|
||||
export const MAX_CANVAS_PX = 32767;
|
||||
|
||||
/**
|
||||
* Thrown by `renderSpec` when a **canvas**-backed chart resolves to a height larger
|
||||
* than the browser can allocate (see `MAX_CANVAS_PX`). Carries the measured size and
|
||||
* the limit so the caller can explain the *actual* cause — the chart is physically
|
||||
* too large to draw — rather than guessing at "too many categories". This is a
|
||||
* render-backend limit, distinct from the readability cardinality warnings.
|
||||
*/
|
||||
export class ChartTooLargeError extends Error {
|
||||
/** The chart's resolved height, in CSS px. */
|
||||
readonly heightPx: number;
|
||||
/** The per-side limit at the current device-pixel-ratio, in CSS px. */
|
||||
readonly limitPx: number;
|
||||
constructor(heightPx: number, limitPx: number) {
|
||||
super(
|
||||
`Chart is ${Math.round(heightPx)}px tall — over the ~${Math.round(limitPx)}px canvas limit`,
|
||||
);
|
||||
this.name = 'ChartTooLargeError';
|
||||
this.heightPx = heightPx;
|
||||
this.limitPx = limitPx;
|
||||
}
|
||||
}
|
||||
|
||||
/** The canvas side limit in CSS px at the current display's device-pixel-ratio. */
|
||||
function canvasLimitPx(): number {
|
||||
const dpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1;
|
||||
return MAX_CANVAS_PX / dpr;
|
||||
}
|
||||
|
||||
/** Embed a prepared spec into `node`. Always: no actions menu. Renderer per `options`. */
|
||||
export async function renderSpec(
|
||||
node: HTMLElement,
|
||||
spec: VisualizationSpec,
|
||||
config: Config,
|
||||
options: RenderOptions = {},
|
||||
): Promise<RenderHandle> {
|
||||
const renderer = options.renderer ?? 'svg';
|
||||
|
||||
// Canvas can't allocate past the browser's max dimension, and an oversized canvas
|
||||
// fails *silently* (a blank/broken surface, sometimes a null 2d context). So for
|
||||
// canvas we first run a headless ('none') layout pass — no canvas allocated — read
|
||||
// the chart's resolved height, and throw with the real numbers if it won't fit.
|
||||
// SVG renders any size (just slowly), so it skips this. The probe uses a detached
|
||||
// node and is finalized immediately; only its computed `height` signal is read.
|
||||
if (renderer === 'canvas') {
|
||||
const probeHost = document.createElement('div');
|
||||
const probe = await vegaEmbed(probeHost, spec, { actions: false, renderer: 'none', config });
|
||||
const height = probe.view.height();
|
||||
probe.view.finalize();
|
||||
const limit = canvasLimitPx();
|
||||
if (typeof height === 'number' && height > limit) throw new ChartTooLargeError(height, limit);
|
||||
}
|
||||
|
||||
const result: EmbedResult = await vegaEmbed(node, spec, {
|
||||
actions: false, // Astrolabe owns its own export/copy affordances
|
||||
renderer: 'svg',
|
||||
renderer,
|
||||
config,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user