import { useMemo, useState, type ReactNode } from 'react';
import {
applyIntent,
activeIntent,
buildChartSpec,
CHART_INTENTS,
defaultBuilderConfig,
defaultFieldType,
intentApplicable,
type BuilderConfig,
type ChannelMapping,
type ChannelName,
type ChartIntent,
type MarkType,
} from '@core/chart-builder';
import { CHART_EXAMPLES } from '@core/examples';
import {
chartConfigForSelection,
customThemeSelection,
type ChartThemeSelection,
} from '@core/vega-themes';
import { THEME_PREVIEW_SPECS } from '@core/theme-preview-specs';
import {
SAMPLE_DATASET_COLUMNS,
SAMPLE_DATASET_NAME,
SAMPLE_DATASET_ROWS,
} from '@core/sample-dataset';
import { DEMO_CUSTOM_THEMES } from './demo-themes';
import { LandingChart } from './LandingChart';
import styles from './Landing.module.css';
// TODO: import { UiTheme } from '@core/theme' instead of redeclaring it — the
// learn entry already uses the canonical one.
type UiTheme = 'light' | 'dark';
// Minimal JSON syntax highlighter for the read-only spec view: keys, strings,
// numbers, and literals get a token class; punctuation and whitespace pass
// through untouched. A read-only marketing snippet doesn't warrant a highlighter
// dependency. `matchAll` over a global regex is stateless per call.
const JSON_TOKEN =
/("(?:[^"\\]|\\.)*"(?=\s*:))|("(?:[^"\\]|\\.)*")|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|\b(?:true|false|null)\b/g;
function JsonCode({ spec }: { spec: unknown }): ReactNode {
const text = JSON.stringify(spec, null, 2);
const out: ReactNode[] = [];
let last = 0;
let key = 0;
for (const m of text.matchAll(JSON_TOKEN)) {
const idx = m.index ?? 0;
if (idx > last) out.push(text.slice(last, idx));
const cls = m[1] ? styles.tokKey : m[2] ? styles.tokStr : styles.tokNum;
out.push(
{m[0]}
,
);
last = idx + m[0].length;
}
if (last < text.length) out.push(text.slice(last));
return
{out}
;
}
/** A snippet-style filename from an example name. */
function specFilename(name: string): string {
return `${name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')}.vl.json`;
}
/** The mark name of an example spec (string mark or `{ type }` form). */
function markLabel(spec: Record): string {
const mark = spec.mark;
if (typeof mark === 'string') return mark;
if (mark && typeof mark === 'object' && 'type' in mark) return String(mark.type);
return 'chart';
}
function Brand(): ReactNode {
return (
{' '}
Astrolabe
);
}
// ── Hero: switch snippets, the editor + chart follow ────────────────────────────
function HeroAppWindow({ theme }: { theme: UiTheme }): ReactNode {
const [selected, setSelected] = useState(0);
const example = CHART_EXAMPLES[selected];
const config = useMemo(() => chartConfigForSelection('astrolabe', theme), [theme]);
return (
{CHART_EXAMPLES.length} snippetslive preview
Library
Search snippets…
{CHART_EXAMPLES.map((ex, i) => (
))}
{specFilename(example.name)}
Preview
);
}
// ── Chart Builder demo: pick fields, a mark, or an intent; the chart rebuilds ────
const MARK_OPTIONS: ReadonlyArray<{ type: MarkType; label: string }> = [
{ type: 'bar', label: 'Bar' },
{ type: 'line', label: 'Line' },
{ type: 'point', label: 'Point' },
{ type: 'area', label: 'Area' },
];
const INTENT_LABELS: Record = {
compare: 'Compare',
ranking: 'Ranking',
time: 'Over time',
correlation: 'Correlation',
distribution: 'Distribution',
partToWhole: 'Part-to-whole',
heatmap: 'Heatmap',
};
const BUILDER_TYPES = SAMPLE_DATASET_COLUMNS.columnTypes;
const ALL_COLUMNS = SAMPLE_DATASET_COLUMNS.columns;
const MEASURE_COLUMNS = BUILDER_TYPES.filter((c) => c.type === 'number').map((c) => c.name);
const CATEGORY_COLUMNS = BUILDER_TYPES.filter(
(c) => c.type === 'string' || c.type === 'boolean',
).map((c) => c.name);
const COUNT = '__count__';
const NONE = '__none__';
function columnType(name: string): (typeof BUILDER_TYPES)[number]['type'] {
return BUILDER_TYPES.find((c) => c.name === name)?.type ?? 'string';
}
/** A field mapping with the field type the builder would derive for that column. */
function fieldMapping(name: string): ChannelMapping {
return { field: name, type: defaultFieldType(columnType(name)) };
}
function BuilderDemo({ theme }: { theme: UiTheme }): ReactNode {
const columns = SAMPLE_DATASET_COLUMNS;
const [config, setConfig] = useState(() =>
defaultBuilderConfig(SAMPLE_DATASET_NAME, columns),
);
const active = activeIntent(config, columns);
const chartConfig = useMemo(() => chartConfigForSelection('astrolabe', theme), [theme]);
const spec = useMemo(() => {
const built = buildChartSpec(config);
// Inline the sample rows so the spec renders standalone (the builder otherwise
// emits a by-name dataset reference the app resolves from its library).
built.data = { values: SAMPLE_DATASET_ROWS };
return built;
}, [config]);
const setEncoding = (channel: ChannelName, mapping: ChannelMapping | null): void =>
setConfig({ ...config, encodings: { ...config.encodings, [channel]: mapping } });
const yValue =
config.encodings.y?.aggregate === 'count' ? COUNT : (config.encodings.y?.field ?? COUNT);
return (
<>