mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 10:12:34 +00:00
Chart fonts: self-hosted roster + document.fonts.load render gate
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
import vegaEmbed, { type Result as EmbedResult } from 'vega-embed';
|
||||
import type { VisualizationSpec } from 'vega-embed';
|
||||
import type { Config } from 'vega-lite';
|
||||
import { collectFontFamilies } from '@core/custom-theme';
|
||||
|
||||
/** Options for `RenderHandle.toImageURL` (spec §08 → Per-chart export). */
|
||||
interface ImageExportOptions {
|
||||
@@ -145,6 +146,44 @@ function canvasObjectUrl(canvas: HTMLCanvasElement): Promise<string> {
|
||||
});
|
||||
}
|
||||
|
||||
/** Weights to preload per family — the roster ships 400/600 (700 for the mono). */
|
||||
const FONT_LOAD_WEIGHTS = ['400', '600', '700'];
|
||||
/**
|
||||
* Cap on waiting for fonts before rendering anyway. A precached/cached face
|
||||
* resolves near-instantly; this only bounds the first fetch of an uncached
|
||||
* subset so a slow network never freezes the preview — it renders with fallback
|
||||
* metrics and sharpens on a later re-render once the face is cached.
|
||||
*/
|
||||
const FONT_LOAD_TIMEOUT_MS = 3000;
|
||||
|
||||
/**
|
||||
* Load the font faces a spec/config will use before rendering. Vega measures
|
||||
* every text label via canvas `measureText` regardless of renderer (even the
|
||||
* 'none' probe runs layout), so a face that finishes loading *after* embed
|
||||
* leaves the whole chart laid out with fallback metrics.
|
||||
*
|
||||
* Font loading is a non-critical enhancement, and an individual face failing
|
||||
* (offline, a 404, or a system family with no `@font-face`) is *expected* — the
|
||||
* correct response is to render with fallback metrics, not to fail the chart. So
|
||||
* we wait on `allSettled` (which never rejects; a rejected load is ignored by
|
||||
* design) and bound a slow first fetch with the timeout. This is the sanctioned
|
||||
* "safe to swallow" case from arch 02's fail-loud rule — the failure mode is
|
||||
* graceful fallback, not a buried bug or lost data — kept explicit rather than
|
||||
* hidden in a catch-all. No-op where the Font Loading API is absent (tests).
|
||||
*/
|
||||
async function ensureFontsLoaded(spec: VisualizationSpec, config: Config): Promise<void> {
|
||||
if (typeof document === 'undefined' || !document.fonts?.load) return;
|
||||
const families = new Set<string>([...collectFontFamilies(config), ...collectFontFamilies(spec)]);
|
||||
if (families.size === 0) return;
|
||||
const loads = [...families].flatMap((family) =>
|
||||
FONT_LOAD_WEIGHTS.map((weight) => document.fonts.load(`${weight} 16px ${family}`)),
|
||||
);
|
||||
await Promise.race([
|
||||
Promise.allSettled(loads),
|
||||
new Promise((resolve) => setTimeout(resolve, FONT_LOAD_TIMEOUT_MS)),
|
||||
]);
|
||||
}
|
||||
|
||||
/** Embed a prepared spec into `node`. Always: no actions menu. Renderer per `options`. */
|
||||
export async function renderSpec(
|
||||
node: HTMLElement,
|
||||
@@ -154,6 +193,9 @@ export async function renderSpec(
|
||||
): Promise<RenderHandle> {
|
||||
const renderer = options.renderer ?? 'svg';
|
||||
|
||||
// Load the chart's fonts before any layout pass measures text (see the helper).
|
||||
await ensureFontsLoaded(spec, config);
|
||||
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user