mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Chart theming: custom named themes + Theme Builder
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Custom chart theme — a user-named Vega-Lite config saved in the library
|
||||
* (docs/chart-theming-scope.md §4.4; spec §04 → Chart theme).
|
||||
*
|
||||
* Portable core: record shape, factory, and the pure config transforms the
|
||||
* Theme Builder runs. A custom theme is "whatever config the user saved" — it
|
||||
* is injected at embed time exactly like a preset (vega-embed `opt.config`),
|
||||
* so a snippet's own `config` still overrides it property by property.
|
||||
*/
|
||||
|
||||
import { isJsonObject, type JsonObject } from './spec-config';
|
||||
|
||||
/** Current schema version for a CustomTheme record (read-time migration target). */
|
||||
export const CURRENT_THEME_VERSION = 1;
|
||||
|
||||
export interface CustomTheme {
|
||||
/** Unique numeric identifier (IndexedDB key). */
|
||||
id: number;
|
||||
/** Record schema version, for read-time migration. */
|
||||
version: number;
|
||||
/** Unique, human-readable name shown in the chart-theme picker. */
|
||||
name: string;
|
||||
/** The Vega-Lite config injected when this theme is selected. */
|
||||
config: JsonObject;
|
||||
/** ISO timestamp — when first created. */
|
||||
created: string;
|
||||
/** ISO timestamp — when last changed. */
|
||||
modified: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new CustomTheme. The default id is provisional — the store's id
|
||||
* authority reassigns it on insert (same contract as `createDataset`).
|
||||
*/
|
||||
export function createCustomTheme(opts: {
|
||||
name: string;
|
||||
config: JsonObject;
|
||||
now?: Date;
|
||||
}): CustomTheme {
|
||||
const iso = (opts.now ?? new Date()).toISOString();
|
||||
return {
|
||||
id: Date.now(),
|
||||
version: CURRENT_THEME_VERSION,
|
||||
name: opts.name,
|
||||
config: opts.config,
|
||||
created: iso,
|
||||
modified: iso,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply one font family across a config: sets the top-level `font` (Vega-Lite's
|
||||
* default for every text mark, label, and title) AND rewrites every explicit
|
||||
* font slot already present anywhere in the config — `font`, `labelFont`,
|
||||
* `titleFont`, `subtitleFont`, … at any nesting depth (`axis`, `axisX`,
|
||||
* `legend`, `header`, `title`, mark configs). The explicit slots must be
|
||||
* rewritten because they would otherwise keep overriding the new top-level
|
||||
* default — this is exactly the "populate the font in many places" job the
|
||||
* Theme Builder's font control does. Returns a new object; input not mutated.
|
||||
*/
|
||||
export function applyFontToConfig(config: JsonObject, family: string): JsonObject {
|
||||
const walk = (obj: JsonObject): JsonObject => {
|
||||
const out: JsonObject = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if ((key === 'font' || key.endsWith('Font')) && typeof value === 'string') {
|
||||
out[key] = family;
|
||||
} else if (isJsonObject(value)) {
|
||||
out[key] = walk(value);
|
||||
} else {
|
||||
out[key] = value;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
return { ...walk(config), font: family };
|
||||
}
|
||||
|
||||
/** A font choice the Theme Builder's font control offers. */
|
||||
export interface ThemeFontOption {
|
||||
/** The CSS family stack written into the config. */
|
||||
value: string;
|
||||
/** Display name. */
|
||||
label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fonts the builder can apply today: the two self-hosted Plex faces the app
|
||||
* already loads, plus web-safe/system stacks that need no loading at all. Every
|
||||
* entry is render-safe without a `document.fonts.load` gate — Plex is loaded by
|
||||
* the UI before any chart renders, the rest resolve to locally installed faces.
|
||||
* The self-hosted roster (scope doc §4.5) extends this list and brings the
|
||||
* pre-render loading gate with it.
|
||||
*/
|
||||
export const THEME_FONT_OPTIONS: ReadonlyArray<ThemeFontOption> = [
|
||||
{ value: '"IBM Plex Sans", system-ui, -apple-system, sans-serif', label: 'IBM Plex Sans' },
|
||||
{ value: '"IBM Plex Mono", ui-monospace, monospace', label: 'IBM Plex Mono' },
|
||||
{ value: 'system-ui, -apple-system, sans-serif', label: 'System UI' },
|
||||
{ value: 'Helvetica, Arial, sans-serif', label: 'Helvetica / Arial' },
|
||||
{ value: 'Georgia, "Times New Roman", serif', label: 'Georgia' },
|
||||
{ value: '"Courier New", Courier, monospace', label: 'Courier' },
|
||||
];
|
||||
Reference in New Issue
Block a user