mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Theme Builder: Layout/Axes/Legend/Type panels, scheme-render fix, fail-loud gallery
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
THEME_SCHEMES,
|
||||
asBoolean,
|
||||
asNumber,
|
||||
asString,
|
||||
getConfigValue,
|
||||
normalizeRangeSchemes,
|
||||
schemeColors,
|
||||
schemesByKind,
|
||||
setConfigValue,
|
||||
@@ -100,6 +104,63 @@ describe('THEME_SCHEMES catalog', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeRangeSchemes', () => {
|
||||
it('wraps a bare scheme string in the { scheme } object form', () => {
|
||||
expect(normalizeRangeSchemes({ range: { category: 'category20b' } })).toEqual({
|
||||
range: { category: { scheme: 'category20b' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizes every range family and preserves siblings', () => {
|
||||
const out = normalizeRangeSchemes({
|
||||
background: 'transparent',
|
||||
range: { category: ['#111'], heatmap: 'viridis', ramp: 'viridis', diverging: 'redblue' },
|
||||
});
|
||||
expect(out).toEqual({
|
||||
background: 'transparent',
|
||||
range: {
|
||||
category: ['#111'],
|
||||
heatmap: { scheme: 'viridis' },
|
||||
ramp: { scheme: 'viridis' },
|
||||
diverging: { scheme: 'redblue' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('is idempotent and returns the same object when nothing changes', () => {
|
||||
const config = { range: { category: { scheme: 'category10' }, ordinal: ['#000'] } };
|
||||
expect(normalizeRangeSchemes(config)).toBe(config);
|
||||
});
|
||||
|
||||
it('is a no-op when there is no range object', () => {
|
||||
const config = { font: 'Inter' };
|
||||
expect(normalizeRangeSchemes(config)).toBe(config);
|
||||
});
|
||||
});
|
||||
|
||||
describe('leaf coercion', () => {
|
||||
it('asString accepts strings only', () => {
|
||||
expect(asString('x')).toBe('x');
|
||||
expect(asString(1)).toBeUndefined();
|
||||
expect(asString(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('asNumber accepts finite numbers only', () => {
|
||||
expect(asNumber(0)).toBe(0);
|
||||
expect(asNumber(-45)).toBe(-45);
|
||||
expect(asNumber(NaN)).toBeUndefined();
|
||||
expect(asNumber(Infinity)).toBeUndefined();
|
||||
expect(asNumber('11')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('asBoolean accepts booleans only', () => {
|
||||
expect(asBoolean(false)).toBe(false);
|
||||
expect(asBoolean(true)).toBe(true);
|
||||
expect(asBoolean('true')).toBeUndefined();
|
||||
expect(asBoolean(undefined)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('schemeColors', () => {
|
||||
it('returns the full fixed palette for a categorical scheme (count ignored)', () => {
|
||||
const colors = schemeColors('tableau10', 3);
|
||||
|
||||
@@ -76,6 +76,43 @@ function omitKey(obj: JsonObject, key: string): JsonObject {
|
||||
return rest;
|
||||
}
|
||||
|
||||
// ── Range scheme normalization ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Convert any bare scheme-name string under `config.range.*` to Vega's
|
||||
* range-scheme object `{ scheme: name }`. A bare string passes vega-lite compile
|
||||
* but Vega rejects it at render ("Unrecognized scale range value"), silently
|
||||
* blanking the chart; the object form is the one Vega accepts. Idempotent —
|
||||
* arrays, existing `{ scheme }` objects, and every other key pass through
|
||||
* untouched. Applied where a theme config becomes a render config, so a config
|
||||
* authored, imported, or saved by an earlier build with the bare form still
|
||||
* renders. Returns the same object when nothing needed changing.
|
||||
*/
|
||||
export function normalizeRangeSchemes(config: JsonObject): JsonObject {
|
||||
const range = config.range;
|
||||
if (!isJsonObject(range)) return config;
|
||||
let changed = false;
|
||||
const out: JsonObject = { ...range };
|
||||
for (const [family, value] of Object.entries(range)) {
|
||||
if (typeof value === 'string') {
|
||||
out[family] = { scheme: value };
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed ? { ...config, range: out } : config;
|
||||
}
|
||||
|
||||
// ── Leaf coercion ─────────────────────────────────────────────────────────────
|
||||
// Read a config leaf back into the type a structured control binds to, or
|
||||
// `undefined` when it holds something else (a hand-edit, a preset's richer
|
||||
// form) — the control then shows its default rather than misrendering.
|
||||
|
||||
export const asString = (v: unknown): string | undefined => (typeof v === 'string' ? v : undefined);
|
||||
export const asNumber = (v: unknown): number | undefined =>
|
||||
typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
||||
export const asBoolean = (v: unknown): boolean | undefined =>
|
||||
typeof v === 'boolean' ? v : undefined;
|
||||
|
||||
// ── Named color schemes ─────────────────────────────────────────────────────
|
||||
|
||||
type SchemeKind = 'categorical' | 'sequential' | 'diverging';
|
||||
|
||||
@@ -29,6 +29,7 @@ import type { Config } from 'vega-lite';
|
||||
// tree via vega-embed). Pure data — config objects only — so portable for core.
|
||||
import * as presets from 'vega-themes';
|
||||
import type { CustomTheme } from './custom-theme';
|
||||
import { normalizeRangeSchemes } from './theme-controls';
|
||||
import type { UiTheme } from './theme';
|
||||
|
||||
const PLEX = '"IBM Plex Sans", system-ui, -apple-system, sans-serif';
|
||||
@@ -298,7 +299,10 @@ export function chartConfigForSelection(
|
||||
const customId = customThemeIdOf(selection);
|
||||
if (customId !== null) {
|
||||
const theme = customThemes.find((t) => t.id === customId);
|
||||
return theme ? theme.config : CHART_CONFIG[uiTheme];
|
||||
// Heal a custom config saved/imported with a bare range scheme string, which
|
||||
// Vega rejects at render (see normalizeRangeSchemes); built-in configs below
|
||||
// are already well-formed.
|
||||
return theme ? normalizeRangeSchemes(theme.config) : CHART_CONFIG[uiTheme];
|
||||
}
|
||||
if (selection === 'astrolabe') return CHART_CONFIG[uiTheme];
|
||||
if (selection === 'stock') return STOCK_CONFIG;
|
||||
|
||||
Reference in New Issue
Block a user