@opendata-ai/openchart-core 8.5.1 → 8.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +123 -3
- package/dist/index.js +44 -3
- package/dist/index.js.map +1 -1
- package/dist/styles.css.map +1 -1
- package/package.json +1 -1
- package/schema/chart.schema.json +1486 -71
- package/schema/vizspec.schema.json +6342 -3475
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types/encoding.ts","../src/types/events.ts","../src/types/spec.ts","../src/colors/colorblind.ts","../src/colors/contrast.ts","../src/styles/token-definitions.ts","../src/colors/neutral.ts","../src/colors/palettes.ts","../src/theme/dark-mode.ts","../src/layout/text-measure.ts","../src/responsive/breakpoints.ts","../src/responsive/metrics.ts","../src/theme/defaults.ts","../src/theme/presets.ts","../src/theme/resolve.ts","../src/layout/chrome.ts","../src/layout/text-wrap.ts","../src/labels/collision.ts","../src/locale/format.ts","../src/accessibility/alt-text.ts","../src/accessibility/aria.ts","../src/helpers/spec-builders.ts","../src/helpers/deep-merge.ts"],"sourcesContent":["/**\n * Per-mark-type encoding validation rules.\n *\n * Defines which encoding channels are required vs optional for each mark type.\n * The engine compiler uses these rules to validate specs at runtime (TypeScript\n * catches compile-time errors; these catch runtime JSON from Claude or APIs).\n */\n\nimport type { FieldType, MarkType } from './spec';\n\n// ---------------------------------------------------------------------------\n// Encoding rule types\n// ---------------------------------------------------------------------------\n\n/** Constraint on what field types are valid for an encoding channel. */\nexport interface ChannelRule {\n /** Whether this channel is required for the chart type. */\n required: boolean;\n /** Allowed field types. If empty, any field type is accepted. */\n allowedTypes: FieldType[];\n}\n\n/** Encoding rules for a single chart type: which channels are required/optional. */\nexport interface EncodingRule {\n x: ChannelRule;\n y: ChannelRule;\n x2?: ChannelRule;\n y2?: ChannelRule;\n color: ChannelRule;\n size: ChannelRule;\n opacity?: ChannelRule;\n strokeDash?: ChannelRule;\n text?: ChannelRule;\n tooltip?: ChannelRule;\n theta?: ChannelRule;\n detail: ChannelRule;\n}\n\n// ---------------------------------------------------------------------------\n// Mark encoding rules\n// ---------------------------------------------------------------------------\n\n/** Helper to create a required channel rule. */\nfunction required(...types: FieldType[]): ChannelRule {\n return { required: true, allowedTypes: types };\n}\n\n/** Helper to create an optional channel rule. */\nfunction optional(...types: FieldType[]): ChannelRule {\n return { required: false, allowedTypes: types };\n}\n\n/**\n * Encoding rules per mark type.\n *\n * Defines which channels are required and what field types they accept.\n * The compiler uses this map to validate user specs at runtime.\n *\n * Key design decisions:\n * - line/area: x is temporal/ordinal (the axis), y is quantitative (the value)\n * - bar: covers both horizontal and vertical orientations, so both axes accept\n * all relevant types. The engine validates the combination later (one axis\n * must be quantitative).\n * - arc: no x axis; y is the value (quantitative), color is the category\n * - circle: y is the category, x is quantitative\n * - point: both axes are quantitative\n * - text/rule: fully optional positioning\n * - tick/rect: both axes required, any type\n * - beeswarm: both axes optional at the channel level; the compiler enforces\n * the combination (exactly one quantitative value axis, the other axis an\n * optional nominal/ordinal lane channel)\n * - waffle: no positional axes; y is the share value (quantitative, with\n * theta accepted as an alias like arc), color is the category\n * - calendar: x is the daily date (temporal), color the per-day value\n * (quantitative); no y channel (the calendar owns its own geometry)\n * - parliament: no positional axes; theta (aliased to y) is the seat count\n * (quantitative), color the party\n */\nexport const MARK_ENCODING_RULES: Record<MarkType, EncodingRule> = {\n bar: {\n x: required('quantitative', 'nominal', 'ordinal', 'temporal'),\n y: required('quantitative', 'nominal', 'ordinal'),\n x2: optional('quantitative'),\n y2: optional('quantitative'),\n color: optional('nominal', 'ordinal', 'quantitative'),\n size: optional(),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n line: {\n x: required('temporal', 'ordinal'),\n y: required('quantitative'),\n color: optional('nominal', 'ordinal', 'quantitative'),\n size: optional(),\n opacity: optional('quantitative'),\n strokeDash: optional('nominal', 'ordinal'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n area: {\n x: required('temporal', 'ordinal'),\n y: required('quantitative'),\n y2: optional('quantitative'),\n color: optional('nominal', 'ordinal', 'quantitative'),\n size: optional(),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n point: {\n x: required('quantitative', 'temporal', 'nominal', 'ordinal'),\n y: required('quantitative', 'temporal', 'nominal', 'ordinal'),\n color: optional('nominal', 'ordinal', 'quantitative'),\n size: optional('quantitative'),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n circle: {\n x: required('quantitative'),\n y: required('nominal', 'ordinal'),\n color: optional('nominal', 'ordinal', 'quantitative'),\n size: optional('quantitative'),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n lollipop: {\n x: required('quantitative'),\n y: required('nominal', 'ordinal'),\n color: optional('nominal', 'ordinal', 'quantitative'),\n size: optional('quantitative'),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n beeswarm: {\n x: optional('quantitative', 'nominal', 'ordinal'),\n y: optional('quantitative', 'nominal', 'ordinal'),\n color: optional('nominal', 'ordinal', 'quantitative'),\n size: optional('quantitative'),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n // Range: x+x2 with nominal y (horizontal) or y+y2 with nominal x (vertical).\n // The orientation-dependent x2/y2 requirement is enforced by a dedicated\n // check in the validator; the static table can only mark them optional.\n range: {\n x: required('quantitative', 'nominal', 'ordinal'),\n y: required('quantitative', 'nominal', 'ordinal'),\n x2: optional('quantitative'),\n y2: optional('quantitative'),\n color: optional('nominal', 'ordinal'),\n size: optional(),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n arc: {\n x: optional(),\n y: optional('quantitative'),\n theta: required('quantitative'),\n color: required('nominal', 'ordinal'),\n size: optional(),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n waffle: {\n x: optional(),\n y: required('quantitative'),\n color: required('nominal', 'ordinal'),\n size: optional(),\n opacity: optional('quantitative'),\n tooltip: optional(),\n theta: optional('quantitative'),\n detail: optional('nominal'),\n },\n // parliament: hemicycle seat chart. Like arc/waffle, no positional axes;\n // color is the party (required), theta the seat count (aliased to y in the\n // sugar pass). y is accepted so the theta alias resolves before the rules\n // check runs.\n parliament: {\n x: optional(),\n y: required('quantitative'),\n color: required('nominal', 'ordinal'),\n size: optional(),\n opacity: optional('quantitative'),\n tooltip: optional(),\n theta: optional('quantitative'),\n detail: optional('nominal'),\n },\n text: {\n x: optional(),\n y: optional(),\n color: optional(),\n size: optional('quantitative'),\n opacity: optional('quantitative'),\n text: required(),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n rule: {\n x: optional(),\n y: optional(),\n x2: optional(),\n y2: optional(),\n color: optional(),\n size: optional(),\n opacity: optional('quantitative'),\n strokeDash: optional('nominal', 'ordinal'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n tick: {\n x: required(),\n y: required(),\n color: optional(),\n size: optional(),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n rect: {\n x: required(),\n y: required(),\n x2: optional(),\n y2: optional(),\n color: optional(),\n size: optional(),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n // calendar: x is the daily date (temporal), color the per-day value\n // (quantitative). No y channel: the calendar computes its own\n // weeks-x-weekdays geometry, so y is left optional here and the dedicated\n // validator rejects it with a pointed message.\n calendar: {\n x: required('temporal'),\n y: optional(),\n color: required('quantitative'),\n size: optional(),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n};\n\n// ---------------------------------------------------------------------------\n// Graph encoding rules\n// ---------------------------------------------------------------------------\n\n/** Encoding rule for a single graph visual channel. */\nexport interface GraphChannelRule {\n /** Whether this channel is required. */\n required: boolean;\n /** Allowed field types. Empty means any type. */\n allowedTypes: FieldType[];\n}\n\n/**\n * Encoding rules for graph visualizations.\n *\n * All graph encoding channels are optional since a graph can be rendered\n * with just nodes and edges (uniform appearance). Encoding channels add\n * visual differentiation based on data fields.\n */\nexport const GRAPH_ENCODING_RULES: Record<string, GraphChannelRule> = {\n nodeColor: { required: false, allowedTypes: ['nominal', 'ordinal'] },\n nodeSize: { required: false, allowedTypes: ['quantitative'] },\n edgeColor: { required: false, allowedTypes: ['nominal', 'ordinal'] },\n edgeWidth: { required: false, allowedTypes: ['quantitative'] },\n nodeLabel: { required: false, allowedTypes: [] },\n};\n","/**\n * Chart interaction event types.\n *\n * These types define the callback signatures for user interactions with\n * chart elements: clicking marks, hovering, legend toggles, and annotation clicks.\n *\n * Event handlers are optional and passed through MountOptions (vanilla) or\n * ChartProps (React). The vanilla adapter wires DOM event listeners to\n * mark elements and constructs these typed events from the raw browser events.\n */\n\nimport type {\n Annotation,\n AnnotationOffset,\n DataRow,\n RangeAnnotation,\n RefLineAnnotation,\n TextAnnotation,\n} from './spec';\n\n// ---------------------------------------------------------------------------\n// Chrome key identifiers\n// ---------------------------------------------------------------------------\n\n/** Identifies a specific chrome text element (title, subtitle, source, byline, footer). */\nexport type ChromeKey = 'title' | 'subtitle' | 'source' | 'byline' | 'footer';\n\n// ---------------------------------------------------------------------------\n// Element references (identity for selection/edit callbacks)\n// ---------------------------------------------------------------------------\n\n/**\n * Reference to an editable chart element.\n * Carries enough info to find the element in both the spec and the DOM.\n * The `annotation` variant uses two-tier identity: `id` (when the consumer provides one)\n * and `index` (always available, position in the spec's annotations array).\n */\nexport type ElementRef =\n | {\n type: 'annotation';\n index: number;\n id?: string;\n /** RFC 6901 JSON Pointer to the edited property in the spec. */\n path?: string;\n }\n | {\n type: 'chrome';\n key: ChromeKey;\n /** RFC 6901 JSON Pointer to the edited property in the spec. */\n path?: string;\n }\n | {\n type: 'series-label';\n series: string;\n /** RFC 6901 JSON Pointer to the edited property in the spec. */\n path?: string;\n }\n | {\n type: 'legend';\n /** RFC 6901 JSON Pointer to the edited property in the spec. */\n path?: string;\n }\n | {\n type: 'legend-entry';\n series: string;\n index: number;\n /** RFC 6901 JSON Pointer to the edited property in the spec. */\n path?: string;\n };\n\n/** Helper constructors for ergonomic ElementRef creation. */\nexport const elementRef = {\n annotation: (index: number, id?: string, path?: string): ElementRef => ({\n type: 'annotation',\n index,\n ...(id !== undefined ? { id } : {}),\n ...(path !== undefined ? { path } : {}),\n }),\n chrome: (key: ChromeKey, path?: string): ElementRef => ({\n type: 'chrome',\n key,\n ...(path !== undefined ? { path } : {}),\n }),\n seriesLabel: (series: string, path?: string): ElementRef => ({\n type: 'series-label',\n series,\n ...(path !== undefined ? { path } : {}),\n }),\n legend: (path?: string): ElementRef => ({\n type: 'legend',\n ...(path !== undefined ? { path } : {}),\n }),\n legendEntry: (series: string, index: number, path?: string): ElementRef => ({\n type: 'legend-entry',\n series,\n index,\n ...(path !== undefined ? { path } : {}),\n }),\n} as const;\n\n// ---------------------------------------------------------------------------\n// Element edit events\n// ---------------------------------------------------------------------------\n\n/**\n * Discriminated union of all element edit events.\n * Fired by the `onEdit` callback when any editable chart element is modified.\n * Covers repositioning (drag), deletion (Delete key), and text editing (double-click).\n * Selection events (onSelect/onDeselect) are separate callbacks, not part of this union.\n *\n * For the `annotation` variant:\n * - `annotation` is the ORIGINAL spec annotation (may have a stale or missing offset)\n * - `offset` is the NEW accumulated position after the drag completes\n * - To persist a drag edit back into your spec: `{ ...edit.annotation, offset: edit.offset }`\n */\nexport type ElementEdit =\n | {\n type: 'annotation';\n element: ElementRef;\n annotation: TextAnnotation;\n offset: AnnotationOffset;\n }\n | {\n type: 'annotation-connector';\n element: ElementRef;\n annotation: TextAnnotation;\n endpoint: 'from' | 'to';\n offset: AnnotationOffset;\n }\n | {\n type: 'range-label';\n element: ElementRef;\n annotation: RangeAnnotation;\n labelOffset: AnnotationOffset;\n }\n | {\n type: 'refline-label';\n element: ElementRef;\n annotation: RefLineAnnotation;\n labelOffset: AnnotationOffset;\n }\n | {\n type: 'annotation-anchor';\n element: ElementRef;\n annotation: TextAnnotation;\n x: string | number;\n y: string | number;\n }\n | { type: 'chrome'; key: ChromeKey; text: string; offset: AnnotationOffset }\n | { type: 'series-label'; series: string; offset: AnnotationOffset }\n | { type: 'legend'; offset: AnnotationOffset }\n /**\n * Transient legend visibility toggle. Reports the same runtime show/hide\n * a reader triggers; not persisted by openchart. To author default-hidden\n * series, set `spec.hiddenSeries` directly. Hosts building spec\n * persistence should generally not write legend-toggle edits back into\n * the authored spec. `hidden: true` means the series was just hidden.\n */\n | { type: 'legend-toggle'; series: string; hidden: boolean }\n | { type: 'add'; annotation: TextAnnotation }\n | { type: 'delete'; element: ElementRef }\n | { type: 'text-edit'; element: ElementRef; oldText: string; newText: string };\n\n// ---------------------------------------------------------------------------\n// Mark events\n// ---------------------------------------------------------------------------\n\n/**\n * Event fired when a user interacts with a data mark (bar, point, line segment, etc.).\n *\n * Contains the underlying data row, the series it belongs to (if multi-series),\n * the position within the chart container, and the raw browser MouseEvent.\n */\nexport interface MarkEvent {\n /** The data row associated with the mark that was interacted with. */\n datum: DataRow;\n /** Series identifier, if the chart has multiple series (e.g. multi-line). */\n series?: string;\n /** Position of the interaction relative to the chart container. */\n position: { x: number; y: number };\n /** The raw browser MouseEvent. */\n event: MouseEvent;\n}\n\n// ---------------------------------------------------------------------------\n// Chart event handler interface\n// ---------------------------------------------------------------------------\n\n/**\n * Event handler callbacks for chart interactions.\n *\n * All handlers are optional. Pass these through MountOptions (vanilla adapter)\n * or ChartProps (React component) to receive interaction callbacks.\n */\nexport interface ChartEventHandlers {\n /**\n * Explicitly enable or disable edit interactions (drag, select, keyboard\n * delete/edit). When true, the interaction layer is wired even without\n * onEdit. When false, all edit interactions are suppressed even when\n * onEdit is provided. When omitted, each interaction block uses its\n * existing callback-presence gate.\n */\n editable?: boolean;\n /** Called when a data mark is clicked. */\n onMarkClick?: (event: MarkEvent) => void;\n /** Called when the mouse enters a data mark. */\n onMarkHover?: (event: MarkEvent) => void;\n /** Called when the mouse leaves a data mark. */\n onMarkLeave?: () => void;\n /** Called when a legend entry is toggled (clicked to show/hide a series). */\n onLegendToggle?: (series: string, visible: boolean) => void;\n /**\n * Called when the series search (`seriesSearch`) changes the highlight set.\n * Receives the full set of highlighted values after the change, including\n * the authored `encoding.color.highlight` baseline.\n */\n onHighlightChange?: (values: string[]) => void;\n /** Called when an annotation element is clicked. */\n onAnnotationClick?: (annotation: Annotation, event: MouseEvent) => void;\n /** Called when a text annotation label is dragged to a new position. */\n onAnnotationEdit?: (annotation: TextAnnotation, updatedOffset: AnnotationOffset) => void;\n /** Unified edit callback. Fires for any spec-modifying edit (repositioning, deletion, text editing). */\n onEdit?: (edit: ElementEdit) => void;\n /** Fired when an element is selected via click or programmatic select(). */\n onSelect?: (element: ElementRef) => void;\n /** Fired when the current element is deselected (click empty, Escape, or new selection replaces old). */\n onDeselect?: (element: ElementRef) => void;\n /** Fired when inline text editing commits. Also flows through onEdit as a 'text-edit' event. */\n onTextEdit?: (element: ElementRef, oldText: string, newText: string) => void;\n /**\n * Fired when a \"you draw it\" (`youDrawIt`) drawing is revealed (button click\n * or programmatic reveal), with the reader's guess in data coordinates,\n * ordered by x. For newsrooms aggregating reader guesses.\n */\n onReveal?: (guess: Array<{ x: string | number; y: number }>) => void;\n}\n","/**\n * Spec types: the user-facing input contract.\n *\n * These types define what a user (or Claude) writes to describe a visualization.\n * The engine validates, normalizes, and compiles specs into layout objects.\n *\n * Encoding vocabulary follows Vega-Lite conventions (field/type/aggregate/mark)\n * with editorial extensions for chrome, annotations, responsive, and dark mode.\n */\n\n// Re-import for use in LegendConfig and overrides (avoids circular by importing from sibling)\nimport type { Breakpoint, LegendPosition } from '../responsive/breakpoints';\nimport type { SortState } from './layout';\nimport type { ColumnConfig } from './table';\nimport type { SeriesStrategy, TokenValue } from './theme';\n\n// ---------------------------------------------------------------------------\n// Mark type union (Vega-Lite aligned)\n// ---------------------------------------------------------------------------\n\n/**\n * Supported mark types, following Vega-Lite conventions.\n *\n * Mapping from previous OpenChart chart types:\n * - 'bar' covers both horizontal bars (old 'bar') and vertical columns (old 'column')\n * - 'arc' covers both pie (old 'pie') and donut (old 'donut')\n * - 'point' replaces old 'scatter'\n * - 'circle' replaces old 'dot'\n * - 'line' and 'area' unchanged\n *\n * New mark types not in previous OpenChart:\n * - 'text': data-positioned text labels\n * - 'rule': reference lines as data marks\n * - 'tick': strip/rug plot marks\n * - 'rect': heatmaps and 2D binned plots\n * - 'range': dumbbell / arrow / range-bar plots spanning two values per category\n * - 'waffle': unit grids (\"x of 100\") for part-to-whole counts\n * - 'calendar': GitHub-style calendar heatmap (weeks x weekdays, daily dates)\n * - 'parliament': hemicycle seat-dot chart for election/legislature results\n */\nexport type MarkType =\n | 'bar'\n | 'line'\n | 'area'\n | 'point'\n | 'circle'\n | 'arc'\n | 'text'\n | 'rule'\n | 'tick'\n | 'rect'\n | 'lollipop'\n | 'beeswarm'\n | 'range'\n | 'waffle'\n | 'calendar'\n | 'parliament';\n\n// ---------------------------------------------------------------------------\n// Gradient definitions (Vega-aligned)\n// ---------------------------------------------------------------------------\n\n/** A single color stop in a gradient definition. */\nexport interface GradientStop {\n /** Position along the gradient, 0 to 1. */\n offset: number;\n /** CSS color string at this stop. */\n color: string;\n /** Opacity at this stop, 0 to 1. Maps to SVG stop-opacity. */\n opacity?: number;\n}\n\n/**\n * Linear gradient definition.\n * Coordinates are in [0,1] normalized space relative to the mark's bounding box\n * (maps to SVG gradientUnits=\"objectBoundingBox\").\n */\nexport interface LinearGradient {\n gradient: 'linear';\n /** Color stops from start to end. */\n stops: GradientStop[];\n /** Start x coordinate (0-1). Default: 0. */\n x1?: number;\n /** Start y coordinate (0-1). Default: 0. */\n y1?: number;\n /** End x coordinate (0-1). Default: 0. */\n x2?: number;\n /** End y coordinate (0-1). Default: 1 (top-to-bottom). */\n y2?: number;\n}\n\n/**\n * Radial gradient definition.\n * Coordinates are in [0,1] normalized space relative to the mark's bounding box.\n */\nexport interface RadialGradient {\n gradient: 'radial';\n /** Color stops from inner to outer. */\n stops: GradientStop[];\n /** Inner circle center x (0-1). Default: 0.5. */\n x1?: number;\n /** Inner circle center y (0-1). Default: 0.5. */\n y1?: number;\n /** Inner circle radius (0-1). Default: 0. */\n r1?: number;\n /** Outer circle center x (0-1). Default: 0.5. */\n x2?: number;\n /** Outer circle center y (0-1). Default: 0.5. */\n y2?: number;\n /** Outer circle radius (0-1). Default: 0.5. */\n r2?: number;\n}\n\n/** A gradient definition, either linear or radial. */\nexport type GradientDef = LinearGradient | RadialGradient;\n\n/** Type guard: check if a value is a GradientDef object. */\nexport function isGradientDef(value: unknown): value is GradientDef {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'gradient' in value &&\n 'stops' in value &&\n ((value as GradientDef).gradient === 'linear' || (value as GradientDef).gradient === 'radial')\n );\n}\n\n/**\n * Extract a single representative color from a fill value.\n * Returns the fill directly if it's a string, or the last stop color\n * if it's a gradient. Used by tooltips, labels, legends, and voronoi\n * overlays that need a flat color.\n */\nexport function getRepresentativeColor(fill: string | GradientDef): string {\n if (typeof fill === 'string') return fill;\n const stops = fill.stops;\n return stops.length > 0 ? stops[stops.length - 1].color : '#000000';\n}\n\n// ---------------------------------------------------------------------------\n// Mark definition (Vega-Lite aligned)\n// ---------------------------------------------------------------------------\n\n/**\n * Configuration for the scatter regression trend line. Passed as the object\n * form of `MarkDef.trendline`.\n */\nexport interface TrendlineConfig {\n /**\n * Stacking order relative to the point marks. `'below'` (default) draws the\n * line behind the dots; `'above'` draws it on top so it stays visible over a\n * dense cloud.\n */\n layer?: 'above' | 'below';\n /**\n * Stroke color. Defaults to the theme's high-contrast text color so the line\n * reads on both light and dark grounds. Set an explicit CSS color to override.\n */\n stroke?: string;\n /**\n * Stroke width in pixels. Defaults to 1.5. Over a dense cloud with `layer:\n * 'above'`, a heavier line (e.g. 2.5) stays legible where the thin default\n * disappears into the dots.\n */\n strokeWidth?: number;\n}\n\n/**\n * Mark definition object with visual properties.\n *\n * When `mark` is a string, it's shorthand for `{ type: markString }`.\n * When it's an object, it can include properties like interpolation,\n * point markers, orientation, and more.\n */\nexport interface MarkDef {\n /** The mark type. */\n type: MarkType;\n /**\n * Show point markers on line/area marks.\n * - true: filled circles at each data point\n * - 'transparent': invisible hover targets (legacy behavior)\n * - 'endpoints': show only first and last point per series (hollow dots)\n * - 'last' / 'first': show only the last (or first) point — filled dot,\n * no white halo. Useful for highlighting the latest reading on a line\n * chart and the default for sparkline mode.\n * - false: no point marks (default; uses voronoi overlay for tooltips)\n */\n point?: boolean | 'transparent' | 'endpoints' | 'last' | 'first';\n /**\n * Curve interpolation for line/area marks.\n * Maps to d3-shape curve factories.\n */\n interpolate?:\n | 'linear'\n | 'monotone'\n | 'step'\n | 'step-before'\n | 'step-after'\n | 'basis'\n | 'cardinal'\n | 'natural';\n /** Explicit orientation override for bar marks. */\n orient?: 'horizontal' | 'vertical';\n /** Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie. */\n innerRadius?: number;\n /** Outer radius for arc marks. */\n outerRadius?: number;\n /**\n * Start angle in radians for arc marks. Defaults to 0 (full pie/donut).\n * Combined with `endAngle`, restricts the pie/donut to a partial sweep,\n * e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut\n * (election-style results donut). Angles follow d3's convention: 0 is\n * straight up, increasing clockwise. Only meaningful when `type` is `'arc'`.\n */\n startAngle?: number;\n /**\n * End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut).\n * See `startAngle`. Only meaningful when `type` is `'arc'`.\n */\n endAngle?: number;\n /**\n * Donut center stat. A string renders one line; the object form adds a\n * caption under it. Decorative (the number is already in the slices), and\n * only meaningful when `type` is `'arc'` with an inner radius.\n */\n centerLabel?: string | { text: string; subtitle?: string };\n /** Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness. */\n cornerRadius?: number | 'pill';\n /**\n * Bar/column marks: fixed bar thickness in pixels. When set, bars are this\n * height (horizontal) or width (vertical), centered within the band.\n *\n * Point marks: the dot RADIUS in pixels (default 5, typical 2-12). This is\n * NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of\n * 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation\n * rejects values above 50 with the conversion.\n */\n size?: number;\n /** Whether the mark is filled (vs stroked only). */\n filled?: boolean;\n /** Default opacity (0-1). */\n opacity?: number;\n /** Default fill color or gradient. */\n fill?: string | GradientDef;\n /** Default stroke color. */\n stroke?: string;\n /** Default stroke width. */\n strokeWidth?: number;\n /** Tooltip behavior. null disables tooltips. */\n tooltip?: boolean | null;\n /** Clip marks to the chart area. */\n clip?: boolean;\n /**\n * Overlay a least-squares regression line on scatter (point) marks.\n * Defaults to true. Set `mark: { type: 'point', trendline: false }` to\n * suppress it when the chart already carries its own reference line (e.g. a\n * manual x=y diagonal in a separate layer), which would otherwise produce\n * two competing diagonals.\n *\n * Pass an object to control stacking order: the line renders `below` the\n * points by default, but a dense scatter (thousands of overlapping dots)\n * fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the\n * reference line over the cloud.\n */\n trendline?: boolean | TrendlineConfig;\n /**\n * Horizontal pixel offset from the data anchor. Only meaningful when `type`\n * is `'text'`. Use it to lift a label clear of the mark it annotates instead\n * of shifting the underlying data value.\n */\n dx?: number;\n /**\n * Vertical pixel offset from the data anchor (negative = up). Only meaningful\n * when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten\n * pixels above its point.\n */\n dy?: number;\n /** Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`. */\n align?: 'left' | 'center' | 'right';\n /** Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`. */\n baseline?: 'top' | 'middle' | 'bottom';\n /**\n * Font size in pixels for text marks. Ignored when the `size` encoding channel\n * is present (which scales font size from a data field). `size` is already\n * taken by bar thickness, hence the distinct name.\n */\n fontSize?: number;\n /**\n * Pattern-fill reinforcement for filled marks (bar, area, arc).\n *\n * - `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots,\n * crosshatch, vertical lines) layered over each series color so series\n * remain distinguishable without color vision. Marks too small to show\n * a legible pattern keep their solid fill.\n * - `'none'` (default): solid fills only.\n */\n fillPattern?: 'auto' | 'none';\n /**\n * Visual style for range marks. Only meaningful when `type` is `'range'`.\n * - 'dumbbell' (default): a dot at each end joined by a connector line.\n * The start dot is muted, the end dot carries the accent color.\n * - 'arrow': a line with an arrowhead at the x2/y2 end, the strongest\n * \"change between two points in time\" form.\n * - 'bar': a plain floating range bar spanning start to end.\n */\n style?: 'dumbbell' | 'arrow' | 'bar';\n /**\n * Color range marks by direction of change. Only meaningful when `type` is\n * `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's\n * `positive` semantic color and decreases use `negative`. A field-based\n * `encoding.color` takes precedence over direction coloring.\n */\n colorByDirection?: boolean;\n /**\n * Total number of cells in a waffle grid. Only meaningful when `type` is\n * `'waffle'`. Category values normalize to this many cells via\n * largest-remainder rounding so the grid always sums exactly. Default 100.\n */\n units?: number;\n /**\n * Number of columns in a waffle grid. Only meaningful when `type` is\n * `'waffle'`. Rows derive from `units / columns`. Default 10.\n */\n columns?: number;\n /**\n * Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is\n * `'waffle'`. Categories whose share of the total falls below `threshold`\n * (a fraction, so `0.05` is 5%) merge into one trailing category. Off by\n * default: bucketing is an editorial decision, not a layout default. Same\n * shape as `SankeySpec.other`.\n */\n other?: number | { threshold: number; label?: string };\n /**\n * First day of the week for calendar marks. Only meaningful when `type` is\n * `'calendar'`. Controls which weekday occupies the top row of each band.\n * Defaults to 'monday' (ISO week convention).\n */\n weekStart?: 'monday' | 'sunday';\n /**\n * Corner radius in pixels for calendar day cells. Only meaningful when\n * `type` is `'calendar'`. Defaults to 2.\n */\n cellRadius?: number;\n /**\n * Seat layout shape for parliament marks. Only meaningful when `type` is\n * `'parliament'`. `'hemicycle'` (default, and currently the only shape)\n * packs seats into concentric semicircular arcs, party-grouped left to\n * right.\n */\n shape?: 'hemicycle';\n /**\n * Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes\n * dots to fill the available hemicycle rings for the given seat count.\n * Only meaningful when `type` is `'parliament'`.\n */\n seatRadius?: number | 'auto';\n /**\n * The majority-threshold line and \"N to win\" label on parliament marks. Only\n * meaningful when `type` is `'parliament'`. `true` (default) draws the line at\n * a simple majority (half the seats plus one); `false` hides it. The object\n * form overrides the threshold and label: `{ seats }` sets a custom seat count\n * (e.g. a two-thirds supermajority) and `{ label }` overrides the default\n * \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls\n * back to the simple-majority default.\n */\n majorityLine?: boolean | { seats?: number; label?: string };\n}\n\n// ---------------------------------------------------------------------------\n// Encoding\n// ---------------------------------------------------------------------------\n\n/** Data field type, following Vega-Lite conventions. */\nexport type FieldType = 'quantitative' | 'temporal' | 'nominal' | 'ordinal';\n\n/** Aggregate function applied to a field before encoding. */\nexport type AggregateOp =\n | 'count'\n | 'sum'\n | 'mean'\n | 'median'\n | 'min'\n | 'max'\n | 'variance'\n | 'stdev'\n | 'distinct'\n | 'q1'\n | 'q3';\n\n/** Axis configuration for an encoding channel. */\nexport interface AxisConfig {\n /** Axis title text. If omitted, the field name is used. */\n title?: string;\n /**\n * Number format: d3-format string (e.g. \",.0f\"), or semantic keyword\n * `'percent'` / `'currency'`. When omitted, compact notation (1k, 2.5M)\n * is applied automatically.\n */\n format?: string;\n /** Override tick count. Engine picks a sensible default if omitted. */\n tickCount?: number;\n /** Whether to show gridlines for this axis. */\n grid?: boolean;\n /** Rotation angle in degrees for tick labels. Common values: -45, -90, 90. */\n labelAngle?: number;\n /** Axis orientation override. */\n orient?: 'top' | 'bottom' | 'left' | 'right';\n /** Explicit tick values. */\n values?: unknown[];\n /** How to handle overlapping labels. */\n labelOverlap?: boolean | 'parity' | 'greedy';\n /** Whether to flush labels to the axis edges. */\n labelFlush?: boolean;\n /** Whether to show the axis domain line. */\n domain?: boolean;\n /** Whether to show tick marks. */\n ticks?: boolean;\n /** Axis position offset in pixels. */\n offset?: number;\n /** Padding between axis title and axis. */\n titlePadding?: number;\n /** Padding between tick labels and axis. */\n labelPadding?: number;\n /** Color override for axis tick labels and title. Useful in dual-axis charts to match axis color to its series. */\n labelColor?: string;\n /** Literal string appended to every formatted tick label. e.g. \"B\" gives \"$4.5B\" when format is \"$,.1~f\". */\n labelSuffix?: string;\n /** Secondary data field to display alongside each tick label. Renders in lighter weight/color. Only effective on categorical y-axis labels (horizontal bar charts). */\n labelField?: string;\n /**\n * Where tick labels render relative to the chart area. Editorial line/area\n * y-axes default to `'inline'`: labels sit above their gridlines at the\n * chart's left edge, with no left gutter, axis line, or tick marks. Other\n * axis types default to `'gutter'` (the classic placement outside the chart\n * area).\n */\n tickPosition?: 'inline' | 'gutter';\n}\n\n/** Scale configuration for an encoding channel. */\nexport interface ScaleConfig {\n /**\n * Explicit domain override. Auto-derived from data if omitted.\n * Continuous scales take `[min, max]`; threshold scales take the full\n * list of class break values; categorical scales take the value list.\n */\n domain?: number[] | string[];\n /** Scale type override. Usually inferred from field type. */\n type?: ScaleType;\n /** Whether to nice-ify the domain for clean tick values. Defaults to true. */\n nice?: boolean;\n /** Whether the domain should include zero. Defaults to true for quantitative. */\n zero?: boolean;\n /** When true and domain is set, filter out data rows with values outside the domain range. */\n clip?: boolean;\n /** Explicit range override. */\n range?: unknown[];\n /**\n * Named color scheme (Vega-Lite aligned). Resolved against the core palette\n * registry (sequential: blue, green, orange, purple, teal; diverging:\n * redBlue, brownTeal; categorical) during the pre-validation sugar\n * expansion; common VL names (blues, greens, category10, ...) are accepted\n * as aliases. Unknown names fail validation with the supported list.\n * An explicit `range` wins over `scheme`.\n */\n scheme?: string;\n /** Reverse the range direction. */\n reverse?: boolean;\n /** Clamp output to the range. */\n clamp?: boolean;\n /**\n * Padding for band/point scales. Point-scale default (a categorical x/y on a\n * line/area/point mark) is 0 for line and area marks (the trend runs flush\n * to the plot edges) and 0.5 for other marks (e.g. scatter, so points don't\n * collide with the axis). `paddingOuter` is accepted as an alias here since\n * point scales have no inner bands.\n */\n padding?: number;\n /** Inner padding for band scales. */\n paddingInner?: number;\n /** Outer padding for band scales. */\n paddingOuter?: number;\n /** Exponent for pow scales. */\n exponent?: number;\n /** Base for log scales (default 10). */\n base?: number;\n /** Constant for symlog scales. */\n constant?: number;\n}\n\n/** Scale type, following Vega-Lite conventions. */\nexport type ScaleType =\n | 'linear'\n | 'log'\n | 'pow'\n | 'sqrt'\n | 'symlog'\n | 'time'\n | 'utc'\n | 'ordinal'\n | 'band'\n | 'point'\n | 'quantile'\n | 'quantize'\n | 'threshold';\n\n/**\n * Sort a categorical domain by the aggregated values of another field\n * (Vega-Lite EncodingSortField).\n */\nexport interface SortByField {\n /** Data field whose values determine the category order. */\n field: string;\n /** Aggregate applied per category before comparing. Defaults to 'sum'. */\n op?: AggregateOp;\n /** Sort direction. Defaults to 'ascending'. */\n order?: 'ascending' | 'descending';\n}\n\n/**\n * A constant value definition for an encoding channel (Vega-Lite aligned).\n * The VL idiom for a fixed visual property, e.g. `color: { value: '#1b7fa3' }`.\n * Expanded to the matching mark-level property (fill/stroke, size, opacity)\n * during the pre-validation sugar expansion.\n */\nexport interface ValueDef {\n /** The constant value: CSS color string, size number, opacity (0-1), or gradient. */\n value: string | number | boolean | null | GradientDef;\n}\n\n/**\n * A single encoding channel mapping a data field to a visual property.\n *\n * Follows the Vega-Lite encoding model: field identifies the column,\n * type determines how the engine interprets values, aggregate applies\n * a transformation before encoding.\n *\n * @template TData - The shape of a data row. When `ChartSpec<TData>` is used\n * with a typed data array, `field` is constrained to `keyof TData & string`,\n * giving IDE autocomplete and compile-time typo detection. Defaults to `DataRow`\n * which degrades to plain `string` (no constraint), keeping untyped specs working.\n */\nexport interface EncodingChannel<TData extends DataRow = DataRow> {\n /**\n * Data field name (column in the data array).\n *\n * When using `ChartSpec<TData>` with a typed data array, this is constrained\n * to the actual column names of `TData`. Typos fail at compile time and IDEs\n * autocomplete your column names.\n */\n field: keyof TData & string;\n /**\n * How to interpret the field values.\n * - quantitative: continuous numbers (scale: linear)\n * - temporal: dates/times (scale: time)\n * - nominal: unordered categories (scale: ordinal)\n * - ordinal: ordered categories (scale: ordinal)\n */\n type: FieldType;\n /** Optional aggregate to apply before encoding. */\n aggregate?: AggregateOp;\n /**\n * Axis configuration. Set to `false` to suppress axis entirely (no space\n * reserved). `null` is accepted as a VL-aligned alias for `false`.\n */\n axis?: AxisConfig | false | null;\n /** Scale configuration. */\n scale?: ScaleConfig;\n /**\n * Stacking behavior for quantitative channels (Vega-Lite aligned).\n *\n * Vega-Lite accepts `'zero' | 'normalize' | 'center' | null`. OpenChart adds\n * `true` (sugar for `'zero'`) and `false` (sugar for `null`) so callers can\n * write `stack: true` / `stack: false` without learning the string forms.\n * The string forms are still the canonical input — the boolean shorthand is\n * normalized to the matching string before reaching layout.\n *\n * - undefined: chart-type default (see below) -- stacked for bar/column/area\n * - true | 'zero': stack from zero baseline\n * - 'normalize': stack and normalize to fraction of total (0-1 per category)\n * - 'center': center stacks around zero (streamgraph style)\n * - null | false: no stacking -- renders grouped/dodged (bar) or overlap (area)\n *\n * **Defaults differ by chart type:**\n * - **Bar/Column**: defaults to stacked (`'zero'`), Vega-Lite aligned. Use\n * `stack: null` (or `false`) for grouped/side-by-side bars.\n * - **Area**: defaults to stacked (`'zero'`), Vega-Lite aligned. Use\n * `stack: null` (or `false`) to opt into overlap rendering, where each\n * series renders as a translucent gradient band anchored at the\n * y-domain baseline.\n * - **Line**: stacking is not applied (lines always overlap).\n *\n * @example\n * // Grouped horizontal bars (opt-out; default is stacked):\n * \"x\": { \"field\": \"revenue\", \"type\": \"quantitative\", \"stack\": null }\n *\n * @example\n * // Overlap area (opt-out; default is stacked):\n * \"y\": { \"field\": \"value\", \"type\": \"quantitative\", \"stack\": null }\n */\n stack?: boolean | 'zero' | 'normalize' | 'center' | null;\n /**\n * Encoding-level bin shorthand (Vega-Lite aligned).\n * When set, auto-generates a BinTransform during normalization and updates\n * the field reference to the binned output (convention: `bin_<fieldName>`).\n * - true: bin with default params\n * - BinParams: bin with custom params (maxbins, step, etc.)\n */\n bin?: boolean | BinParams;\n /**\n * Encoding-level timeUnit shorthand (Vega-Lite aligned).\n * When set, auto-generates a TimeUnitTransform during normalization and\n * updates the field reference to the output (convention: `<timeUnit>_<fieldName>`).\n */\n timeUnit?: TimeUnit;\n /**\n * Sort order for categorical (nominal/ordinal) scale domains (Vega-Lite aligned).\n * - 'ascending': sort domain values ascending (default VL behavior)\n * - 'descending': sort domain values descending\n * - null: use data order (no sorting)\n * - undefined: ascending (VL default)\n * - 'x' | '-x' | 'y' | '-y': sort by another channel's values ('-' prefix\n * for descending). The VL ranked-bar idiom: `sort: '-y'` orders categories\n * by value, largest first. Resolved into an explicit domain during the\n * pre-validation sugar expansion.\n * - string[]: explicit value order. Listed values come first (in the given\n * order); unlisted data values follow in data order.\n * - SortByField: sort by the aggregated values of another field.\n */\n sort?: 'ascending' | 'descending' | null | 'x' | '-x' | 'y' | '-y' | string[] | SortByField;\n /**\n * Legend configuration for this channel (Vega-Lite aligned).\n * Only meaningful on the `color` channel. `null` hides the legend (the VL\n * idiom for `legend.show = false`); a config object merges into the\n * top-level `legend` (top-level keys win on conflict).\n */\n legend?: LegendConfig | null;\n /**\n * Display title override (Vega-Lite aligned).\n * Used as the label in tooltips instead of the raw field name.\n * Also usable for axis titles, but `axis.title` takes precedence there.\n */\n title?: string;\n /**\n * Format string for values (Vega-Lite aligned).\n * For quantitative fields: d3-format string (e.g. \",.0f\", \"$,.2f\"),\n * or semantic keyword `'percent'` / `'currency'`.\n * For temporal fields: d3-time-format string (e.g. \"%Y\", \"%b %d\").\n * Used in tooltips; `axis.format` takes precedence for axis tick labels.\n */\n format?: string;\n /**\n * Emphasis highlight for the color channel. Names one or more series values\n * to visually emphasize; all other series are muted to a neutral gray.\n *\n * Only meaningful on the `color` encoding channel. On other channels this\n * property is ignored.\n *\n * @example\n * encoding: {\n * color: { field: 'region', type: 'nominal', highlight: 'West' }\n * }\n */\n highlight?: string | string[];\n}\n\n/**\n * Facet channel definition: partitions data into small-multiple panels.\n *\n * Unlike positional channels, facet only needs a field, a categorical type,\n * and optional layout hints (columns, sort). It does not carry scale/axis\n * config because it produces a grid, not a visual encoding.\n */\nexport interface FacetChannel<TData extends DataRow = DataRow> {\n /** Data field to partition by. Each unique value produces one panel. */\n field: keyof TData & string;\n /** Must be categorical. */\n type: 'nominal' | 'ordinal';\n /** Number of columns in the wrap grid. Auto-computed when omitted. */\n columns?: number;\n /** Sort order for panel values. Default ascending. */\n sort?: 'ascending' | 'descending' | null;\n}\n\n/**\n * Encoding object mapping visual channels to data fields.\n * Which channels are required depends on the mark type — see the per-mark\n * encoding interfaces (ArcEncoding, LineEncoding, etc.) and MARK_ENCODING_RULES\n * in encoding.ts for the full requirements table.\n *\n * @template TData - Propagated from ChartSpec<TData>. Constrains `field` in\n * every channel to `keyof TData & string` when a typed data row is provided.\n */\nexport interface Encoding<TData extends DataRow = DataRow> {\n /**\n * Horizontal position channel. Required for: bar, line, area, point, tick, rect, lollipop.\n * Maps a field to the x-axis. Use `type: 'temporal'` for dates, `'nominal'` for categories,\n * `'quantitative'` for numbers.\n */\n x?: EncodingChannel<TData>;\n /**\n * Vertical position channel. Required for: bar, line, area, point, tick, rect, arc, lollipop.\n * For arc marks, this is the value field (slice size). For all others it's the y-axis position.\n */\n y?: EncodingChannel<TData>;\n /**\n * Color channel. Required for arc marks (determines pie/donut slice coloring).\n * Optional for all other marks -- used for series differentiation on multi-series charts,\n * or heatmap intensity. Accepts a conditional definition to apply colors based on data predicates,\n * or a bare `{ value }` constant (VL aligned; expanded to the mark-level fill/stroke).\n */\n color?: EncodingChannel<TData> | ConditionalValueDef<TData> | ValueDef;\n /**\n * Size channel. Used by point/bubble charts to scale dots by a quantitative\n * field, area-proportionally (sqrt curve). The units are dot RADII in px:\n * `scale.range` defaults to [3, 30] for scatter, and a bare `{ value }`\n * constant expands to `mark.size`, also a radius. This differs from\n * Vega-Lite, where point size is an area in px^2 -- VL-scale values\n * (60-900) are rejected by validation with the conversion.\n * Accepts a conditional definition to vary size based on data predicates.\n */\n size?: EncodingChannel<TData> | ConditionalValueDef<TData> | ValueDef;\n /**\n * Detail channel. Groups data into multiple series without mapping to a visual property.\n * Useful when you want separate lines per category but don't need the color to differ.\n */\n detail?: EncodingChannel<TData>;\n /**\n * Identity channel for data-update transitions. Maps a field whose value\n * uniquely identifies each datum across updates. Mirrors Vega-Lite's key channel.\n * Not visually encoded. Optional: charts derive sensible default keys.\n */\n key?: EncodingChannel<TData>;\n /**\n * Secondary x position. Used with `x` to define a horizontal span (rect marks, error bars).\n * Both `x` and `x2` must be quantitative.\n */\n x2?: EncodingChannel<TData>;\n /**\n * Secondary y position. Used with `y` to define a vertical span (rect marks, error bands).\n * Both `y` and `y2` must be quantitative.\n */\n y2?: EncodingChannel<TData>;\n /**\n * Data-driven opacity (0-1 range). Accepts a conditional definition to vary opacity\n * based on data predicates (e.g., highlight selected points), or a bare `{ value }`\n * constant (VL aligned; expanded to `mark.opacity`).\n */\n opacity?: EncodingChannel<TData> | ConditionalValueDef<TData> | ValueDef;\n /**\n * Stroke dash pattern encoding. Maps a nominal/ordinal field to different\n * dash patterns on line and rule marks. Useful when color alone doesn't\n * distinguish series well.\n */\n strokeDash?: EncodingChannel<TData>;\n /** Rotation angle encoding for point marks. Maps a quantitative field to 0-360 degrees. */\n angle?: EncodingChannel<TData>;\n /**\n * Text content for `text` marks. Required when mark is `'text'`.\n * Not meaningful for other mark types.\n */\n text?: EncodingChannel<TData>;\n /**\n * Tooltip field(s). Shown on hover. Can be a single channel or an array for\n * multi-field tooltips. Independent of the x/y/color encoding.\n */\n tooltip?: EncodingChannel<TData> | EncodingChannel<TData>[];\n /**\n * Angular position (slice value) for arc marks (pie/donut), the canonical\n * value channel in v8. Waffle and parliament marks accept it too (the same\n * part-to-whole value channel). `y` is still accepted on these marks as a\n * deprecated alias for backward compatibility (a compile warning is emitted\n * and `y` is populated from `theta` internally). Not used by any other mark\n * type.\n */\n theta?: EncodingChannel<TData>;\n /**\n * Facet channel: partitions data into a grid of small-multiple panels.\n * Each unique value of the facet field produces one panel. Panels share\n * scales by default; use `resolve: { scale: { y: 'independent' } }` for\n * per-panel axes.\n */\n facet?: FacetChannel<TData>;\n /**\n * Row facet channel: partitions data into vertically-stacked panels.\n * Each unique value of the row field produces one panel, arranged in a\n * single column. Shares the x-axis scale by default; y-axis is independent.\n * Mutually exclusive with `facet` and `column`.\n */\n row?: FacetChannel<TData>;\n /**\n * Column facet channel: partitions data into side-by-side panels.\n * Each unique value of the column field produces one panel, arranged in a\n * single row. Shares the y-axis scale by default; x-axis is independent.\n * Mutually exclusive with `facet` and `row`.\n */\n column?: FacetChannel<TData>;\n}\n\n// ---------------------------------------------------------------------------\n// Graph-specific encoding\n// ---------------------------------------------------------------------------\n\n/** Encoding channel for graph nodes and edges. */\nexport interface GraphEncodingChannel {\n /** Data field name on the node/edge object. */\n field: string;\n /** How to interpret the field values. */\n type?: FieldType;\n /** Scale configuration. Auto-derived from data if omitted. */\n scale?: ScaleConfig;\n /**\n * Category ordering for the color domain and legend. Graph channels default\n * `'ascending'` (VL-aligned, deterministic); `string[]` pins an explicit\n * order; `null` keeps data order.\n *\n * NOTE: chart encoding channels keep data-order — this graph default is a\n * deliberate divergence this release (see docs/migrating-v8.md).\n */\n sort?: 'ascending' | 'descending' | null | string[];\n /**\n * Category values to emphasize on load; the rest dim. Mirrors the chart\n * `encoding.color.highlight` value list. Distinct from the `highlight()`\n * runtime verb and `interaction.hover.mode`.\n */\n highlight?: string | string[];\n}\n\n/** Graph-specific encoding mapping visual properties to node/edge data fields. */\nexport interface GraphEncoding {\n /** Color mapping for nodes. */\n nodeColor?: GraphEncodingChannel;\n /**\n * Size mapping for nodes. `scale.type` defaults to `'sqrt'` (area-perceptual,\n * today's behavior); set `scale.type: 'linear'` for a linear radius ramp.\n * `scale.range` sets the pixel radius extent (default `[3, 12]`).\n */\n nodeSize?: GraphEncodingChannel;\n /**\n * Opacity mapping for nodes (VL opacity channel). Quantitative fields map\n * linearly to `[0.25, 1]`; `scale.range` overrides.\n *\n * NOTE: there is deliberately no `edgeOpacity` channel — edge alpha is owned\n * by the interaction alpha-tier machinery (hover dimming, search).\n */\n nodeOpacity?: GraphEncodingChannel;\n /** Color mapping for edges. */\n edgeColor?: GraphEncodingChannel;\n /** Width mapping for edges. */\n edgeWidth?: GraphEncodingChannel;\n /** Style mapping for edges (solid, dashed, dotted). */\n edgeStyle?: GraphEncodingChannel;\n /** Label field for nodes. */\n nodeLabel?: GraphEncodingChannel;\n /**\n * Label priority mapping for nodes. Maps a quantitative field to a 0-1\n * priority range; higher values are shown first as the user zooms in.\n * Omit to use the default degree-based priority.\n * `nodeOverrides[id].alwaysShowLabel` still wins as the hard override.\n */\n nodeLabelPriority?: GraphEncodingChannel;\n}\n\n/** Layout algorithm for graph visualization. */\nexport interface GraphLayoutConfig {\n /** Layout algorithm type. */\n type: 'force' | 'radial' | 'hierarchical';\n /** Optional clustering configuration. */\n clustering?: {\n /** Field to group nodes by for cluster forces. */\n field: string;\n };\n /** Charge strength for force layout. Negative values create repulsion. */\n chargeStrength?: number;\n /** Target distance between linked nodes. */\n linkDistance?: number;\n /** Extra px added to node radius for collision detection (default 2). */\n collisionPadding?: number;\n /** Link force strength override. */\n linkStrength?: number;\n /** Whether to apply center force (default true). */\n centerForce?: boolean;\n /**\n * Deterministic layout seed. Same spec + seed ⇒ identical settled layout\n * within a given execution path (worker vs sync parity is not guaranteed).\n */\n seed?: number;\n /**\n * Repulsion preset (plain-language, no d3 jargon):\n * `gentle`/`balanced`/`energetic` → chargeStrength −150/−300/−600 and\n * velocityDecay 0.5/0.4/0.3. A raw `chargeStrength` wins over the preset.\n */\n energy?: 'gentle' | 'balanced' | 'energetic';\n /**\n * Settle-speed preset: `quick`/`balanced`/`thorough` → alphaDecay\n * 0.05/0.0228/0.01. Higher = faster cool-down, less final refinement.\n */\n settle?: 'quick' | 'balanced' | 'thorough';\n /**\n * Headless settle ticks run before the first paint. `true` → 100 ticks under\n * a 250ms budget; a number sets an explicit tick count; `false` disables.\n *\n * Lives in `layout`, NOT `animation`, on purpose: warmup *reduces* motion, so\n * it must survive `animation: false` (opt-out consumers get less settling\n * chaos, not more).\n */\n warmup?: boolean | number;\n}\n\n// ---------------------------------------------------------------------------\n// Chrome (editorial text elements)\n// ---------------------------------------------------------------------------\n\n/** Style overrides for a chrome text element. */\nexport interface ChromeTextStyle {\n /** Font size in pixels. */\n fontSize?: number;\n /** Font weight (400 = normal, 600 = semibold, 700 = bold). */\n fontWeight?: number;\n /** Font family override. */\n fontFamily?: string;\n /** Text color (CSS color string). */\n color?: string;\n /**\n * Maximum number of lines this chrome text may wrap to. Additional lines are\n * dropped and the last kept line is truncated with an ellipsis. Bounds the\n * chrome's contribution to layout height at any container width. Applies\n * whether the font size comes from the theme default or an explicit override.\n * Caps height only, not horizontal overflow of a single unbreakable word.\n */\n maxLines?: number;\n}\n\n/** A chrome text element with optional style overrides. */\nexport interface ChromeText {\n /** The text content to display. */\n text: string;\n /** Optional style overrides. Theme defaults are used for any omitted property. */\n style?: ChromeTextStyle;\n /** Pixel offset for fine-tuning position. */\n offset?: AnnotationOffset;\n}\n\n/**\n * Editorial chrome elements: eyebrow, title, subtitle, source attribution, byline, footer.\n * These are first-class structural elements, not string-only afterthoughts.\n * Each element can be a simple string or a ChromeText object with style overrides.\n */\nexport interface Chrome {\n /**\n * Editorial kicker/category label rendered above the title. Typically\n * uppercase, tracked, and tinted with the accent color (e.g.\n * \"Equities · Single Ticker\"). Term follows IBM Carbon and Atlassian\n * Design System conventions; not part of Vega-Lite's title model.\n */\n eyebrow?: string | ChromeText;\n /** Main title displayed above the visualization. */\n title?: string | ChromeText;\n /** Subtitle displayed below the title, typically providing context. */\n subtitle?: string | ChromeText;\n /** Data source attribution, displayed below the chart area. */\n source?: string | ChromeText;\n /** Author or organization byline. */\n byline?: string | ChromeText;\n /** Footer text, displayed at the very bottom. */\n footer?: string | ChromeText;\n /**\n * Right-anchored brand block on the footer row, paired with a small accent\n * dot to its left. Visually balances the source/byline left-anchored text.\n * When set, suppresses the default `OpenData` watermark for this chart.\n */\n brand?: string | ChromeText;\n}\n\n// ---------------------------------------------------------------------------\n// Annotations\n// ---------------------------------------------------------------------------\n\n/** Pixel offset for fine-grained annotation positioning. */\nexport interface AnnotationOffset {\n /** Horizontal pixel offset. */\n dx?: number;\n /** Vertical pixel offset. */\n dy?: number;\n}\n\n/** Anchor direction for annotation label placement relative to the data point. */\nexport type AnnotationAnchor = 'top' | 'bottom' | 'left' | 'right' | 'auto';\n\n/** Connector type for annotation labels. */\nexport type ConnectorType = 'straight' | 'curve' | 'drop-line';\n\n/** Object form of the connector config, allowing arrow control. */\nexport interface ConnectorConfig {\n /** Connector style. */\n type: ConnectorType;\n /** Whether to draw an arrowhead at the data-point end. Default: true for curve, false for straight. Drop-line never takes a head. */\n arrow?: boolean;\n}\n\n/** Style overrides for the dot marker drawn at the data point. */\nexport interface AnnotationDot {\n /** Circle radius in pixels. Default 4. */\n radius?: number;\n /** Fill color. Defaults to theme background for an \"open ring\" look. */\n fill?: string;\n /** Stroke color. Defaults to the connector's resolved stroke, so marker and leader read as one system. */\n stroke?: string;\n /** Stroke width in pixels. Default 1.5. */\n strokeWidth?: number;\n}\n\n/** Base properties shared by all annotation types. */\ninterface AnnotationBase {\n /** Stable identifier for selection and edit callbacks. When provided, edit events include this ID for reliable element matching. */\n id?: string;\n /** Human-readable label for the annotation. */\n label?: string;\n /** Fill color for the annotation element. */\n fill?: string;\n /** Stroke color for the annotation element. */\n stroke?: string;\n /** Opacity from 0 to 1. */\n opacity?: number;\n /** Z-index for render ordering. Higher values render on top. */\n zIndex?: number;\n /** When false, the annotation is always shown even at compact breakpoints. Default true. */\n responsive?: boolean;\n}\n\n/**\n * Text annotation positioned at a data coordinate.\n * Shows a callout label at a specific point in the chart.\n */\nexport interface TextAnnotation extends AnnotationBase {\n type: 'text';\n /** X-axis data value or position. */\n x: string | number;\n /** Y-axis data value or position. */\n y: string | number;\n /**\n * The annotation text. Required for text annotations.\n *\n * Supports inline `**bold**` spans, so emphasis lands on the key phrase rather\n * than the whole block: `'Inflation peaked at **8.5%**'`. Unmatched `**` (and\n * empty `****`) render literally. `\\n` starts a new line.\n */\n text: string;\n /** Thinning priority (lower = kept longer at narrow widths). When omitted, spec order is used. */\n priority?: number;\n /**\n * Optional muted second-tone text rendered below the primary `text`.\n * Used for supporting context (e.g. methodology, source). Newlines in\n * `text` still produce multi-line primary; subtitle is a separate block.\n *\n * Supports the same inline `**bold**` spans as `text`. When a subtitle is\n * present and no `fontWeight` is set, the primary `text` resolves to weight\n * 700 — the lede + context stack.\n */\n subtitle?: string;\n /**\n * Open-ring marker drawn on the data point itself (not on the pulled-back\n * connector tip).\n *\n * Left unset, a default marker appears whenever a connector is enabled and\n * carries no arrowhead — the leader points, the ring lands. An arrowed\n * connector gets no marker unless you ask for one: the head already marks the\n * spot. `dot: false` is always bare; an explicit `dot` always wins.\n *\n * `true` uses the default style; pass an object to override radius, fill,\n * stroke, or strokeWidth.\n */\n dot?: boolean | AnnotationDot;\n /** Font size override. */\n fontSize?: number;\n /** Font weight override. */\n fontWeight?: number;\n /** Pixel offset from the computed position. */\n offset?: AnnotationOffset;\n /** Anchor direction for label placement relative to the data point. */\n anchor?: AnnotationAnchor;\n /**\n * Connector from label to anchor point.\n * - `true` (default): straight line, no arrow\n * - `'straight'`: straight line, no arrow\n * - `'curve'`: curved arrow with arrowhead\n * - `'drop-line'`: vertical line through the data point's x\n * - `{ type, arrow? }`: object form for explicit arrow control\n * - `false`: no connector\n *\n * The connector leaves the text block on the side facing the data point, with\n * a small standoff gap. A leader shorter than 8px is dropped as noise (the\n * marker alone reads better), as is a connector whose target sits inside the\n * text block. Widen `offset` if you want the line back.\n */\n connector?: boolean | ConnectorType | ConnectorConfig;\n /** Per-endpoint offsets for the connector line. Allows fine-tuning where the connector starts and ends. */\n connectorOffset?: {\n /** Offset for the label-end of the connector. */\n from?: AnnotationOffset;\n /** Offset for the data-point-end of the connector. */\n to?: AnnotationOffset;\n };\n /**\n * Background plate behind the text, for readability over chart lines.\n *\n * `true` uses the theme surface, so the plate follows light/dark mode. A\n * color string sets it explicitly. Prefer `true`: a hardcoded '#ffffff' stays\n * white in dark mode and the text turns light-gray-on-white.\n */\n background?: string | true;\n /** Whether to show the paint-order stroke halo behind text. Default true. Set false for white text on colored backgrounds. */\n halo?: boolean;\n}\n\n/**\n * Range annotation highlighting a region of the chart.\n * Defined by x1/x2 (vertical band) or y1/y2 (horizontal band) or both (rectangle).\n */\nexport interface RangeAnnotation extends AnnotationBase {\n type: 'range';\n /** Start of the range on the x-axis. */\n x1?: string | number;\n /** End of the range on the x-axis. */\n x2?: string | number;\n /** Start of the range on the y-axis. */\n y1?: string | number;\n /** End of the range on the y-axis. */\n y2?: string | number;\n /**\n * For band/point (ordinal) x or y scales, whether the range extends from the\n * data point's center out to the band/step edge. Default `true`: a range on a\n * bar chart spans full columns, and a range on a line chart extends half a step\n * past the first/last point so the band reaches the plot edge. Set `false` to\n * anchor the range exactly at the data point centers instead — useful when you\n * want the band inset from the axis (e.g. starting at the first data point\n * rather than flush against the y-axis guide). No effect on linear/time scales.\n */\n extendToEdges?: boolean;\n /** Pixel offset for the range label. */\n labelOffset?: AnnotationOffset;\n /** Anchor direction for the range label. */\n labelAnchor?: AnnotationAnchor;\n /** Font size override for the range label (px). Default: 11. */\n fontSize?: number;\n /** Font weight override for the range label. Default: 500. */\n fontWeight?: number;\n}\n\n/**\n * Reference line annotation: a horizontal or vertical line at a data value.\n * Useful for baselines (zero), targets, or thresholds.\n */\nexport interface RefLineAnnotation extends AnnotationBase {\n /**\n * Discriminant. `'refline'` is canonical and the only value in the type.\n * `'rule'` (it collides with the `rule` mark type) is still accepted at\n * runtime for backward compatibility -- the spec-sugar pass rewrites it to\n * `'refline'` with a deprecation warning before validation -- but it is not\n * part of this type. Use `'refline'`.\n */\n type: 'refline';\n /** X-axis value for a vertical reference line. */\n x?: string | number;\n /** Y-axis value for a horizontal reference line. */\n y?: string | number;\n /** Line style. */\n style?: 'solid' | 'dashed' | 'dotted';\n /** Raw SVG dash pattern override, e.g. [4, 4]. Takes precedence over style. */\n strokeDash?: number[];\n /** Line width in pixels. */\n strokeWidth?: number;\n /** Pixel offset for the reference line label. */\n labelOffset?: AnnotationOffset;\n /** Anchor direction for the reference line label. */\n labelAnchor?: AnnotationAnchor;\n /** Label font size in pixels. Default: 11. */\n fontSize?: number;\n /** Label font weight. Default: 400. */\n fontWeight?: number;\n}\n\n/** Discriminated union of all annotation types. */\nexport type Annotation = TextAnnotation | RangeAnnotation | RefLineAnnotation;\n\n// ---------------------------------------------------------------------------\n// Theme + Dark Mode\n// ---------------------------------------------------------------------------\n\n/**\n * Dark mode behavior.\n *\n * - `\"auto\"` - Checks the `prefers-color-scheme` media query to detect the\n * user's system-level preference. This does NOT detect class-based dark mode\n * toggles (e.g. `document.documentElement.classList.toggle('dark')`). If your\n * app uses class-based dark mode, use VizThemeProvider with `\"force\"` or\n * `\"off\"` instead of `\"auto\"` and toggle based on your app's state.\n * - `\"force\"` - Always render in dark mode regardless of system preference.\n * - `\"off\"` - Always render in light mode (default).\n *\n * All components (Chart, Sankey, Graph) inherit darkMode from VizThemeProvider\n * when no explicit prop is passed.\n */\nexport type DarkMode = 'auto' | 'force' | 'off';\n\n/**\n * Per-element chrome theme override. Pass a plain string (color only,\n * backward-compatible) or an object with full typography control.\n */\nexport interface ChromeThemeOverride {\n /** Text color. Accepts a TokenValue for light/dark pairs. */\n color?: TokenValue;\n /** Font size in pixels. */\n fontSize?: number;\n /** Font weight. */\n fontWeight?: number;\n /** Line height multiplier. */\n lineHeight?: number;\n}\n\n/**\n * User-facing theme configuration for overriding defaults.\n * All fields are optional. The engine deep-merges these onto the default theme.\n */\nexport interface ThemeConfig {\n /**\n * Color palette overrides.\n * Pass a flat string[] as shorthand for categorical colors,\n * or an object for full control over categorical, sequential, diverging, etc.\n */\n colors?:\n | string[]\n | {\n /** Categorical stroke palette for nominal data (array of CSS color strings). */\n categorical?: string[];\n /**\n * Categorical palette for area-filling marks (bar, area, arc, waffle,\n * calendar, rect). Defaults to `categorical` when only that is set.\n */\n categoricalFill?: string[];\n /** Hairline color for axis lines and separators. Accepts a TokenValue for light/dark pairs. */\n hairline?: TokenValue;\n /** Sequential palettes keyed by name. Each is an array of color stops. */\n sequential?: Record<string, string[]>;\n /** Diverging palettes keyed by name. Each is an array of color stops with a neutral midpoint. */\n diverging?: Record<string, string[]>;\n /** Background color. Accepts a TokenValue for light/dark pairs. */\n background?: TokenValue;\n /** Default text color. Accepts a TokenValue for light/dark pairs. */\n text?: TokenValue;\n /** Gridline color. Accepts a TokenValue for light/dark pairs. */\n gridline?: TokenValue;\n /** Axis line and tick color. Accepts a TokenValue for light/dark pairs. */\n axis?: TokenValue;\n /** Annotation fill color. Accepts a TokenValue for light/dark pairs. */\n annotationFill?: TokenValue;\n /** Annotation text color. Accepts a TokenValue for light/dark pairs. */\n annotationText?: TokenValue;\n /** Semantic color for positive/up-trend values. Accepts a TokenValue for light/dark pairs. */\n positive?: TokenValue;\n /** Semantic color for negative/down-trend values. Accepts a TokenValue for light/dark pairs. */\n negative?: TokenValue;\n };\n /** Font overrides. */\n fonts?: {\n /** Primary font family. */\n family?: string;\n /** Monospace font family (for tabular numbers). */\n mono?: string;\n /** Font size overrides in pixels. Partial — only specified keys are overridden. */\n sizes?: {\n /** Chart title. Default: 26. */\n title?: number;\n /** Subtitle below the title. Default: 14. */\n subtitle?: number;\n /** Body text (tooltips, legend labels). Default: 13. */\n body?: number;\n /** Small text (source line, footer). Default: 11. */\n small?: number;\n /** Axis tick labels. Default: 11. */\n axisTick?: number;\n /** KPI metric uppercase label. Default: 10. */\n metricLabel?: number;\n /** KPI metric primary value. Default: 22. Delta/secondary derive from this. */\n metricValue?: number;\n };\n /** Font weight overrides. Partial — only specified keys are overridden. */\n weights?: {\n /** Normal text weight. Default: 400. */\n normal?: number;\n /** Medium text weight. Default: 500. */\n medium?: number;\n /** Semibold text weight. Default: 600. */\n semibold?: number;\n /** Bold text weight. Default: 700. */\n bold?: number;\n };\n };\n /** Spacing overrides in pixels. */\n spacing?: {\n /** Padding inside the chart container. */\n padding?: number;\n /** Gap between chrome elements (title to subtitle, etc.). */\n chromeGap?: number;\n /** Gap between the last chrome element and the chart area. */\n chromeToChart?: number;\n /** Gap between chart area and source/footer below. */\n chartToFooter?: number;\n /** Internal padding within the chart area (axes margins). */\n axisMargin?: number;\n /** Height reserved below chart area for x-axis tick labels. Increase when large axisTick font sizes cause label clipping. */\n xAxisHeight?: number;\n /** Gap in pixels between the x-axis line and tick label text. Increase when larger axisTick fonts sit too close to the axis line. */\n xAxisLabelPadding?: number;\n };\n /** Border radius for chart container and tooltips. */\n borderRadius?: number;\n /**\n * Per-element chrome style overrides. Pass a plain string for color-only\n * (backward-compatible), or a ChromeThemeOverride object for full\n * typography control (color, fontSize, fontWeight, lineHeight).\n */\n chrome?: {\n /** Eyebrow (kicker) style override. */\n eyebrow?: string | ChromeThemeOverride;\n /** Title style override. */\n title?: string | ChromeThemeOverride;\n /** Subtitle style override. */\n subtitle?: string | ChromeThemeOverride;\n /** Source/attribution style override. */\n source?: string | ChromeThemeOverride;\n /** Byline style override. */\n byline?: string | ChromeThemeOverride;\n /** Footer style override. */\n footer?: string | ChromeThemeOverride;\n };\n /**\n * Editorial rule above the chrome block: a short colored bar (the\n * Economist/FT masthead device). Omit for no rule.\n */\n rule?: {\n /** Rule color. Accepts a TokenValue for light/dark pairs. */\n color: TokenValue;\n /** Rule length in pixels. */\n width: number;\n /** Rule height in pixels. */\n thickness: number;\n };\n /**\n * Series color assignment strategy.\n * Default: 'palette' (full categorical palette always, zero visual drift).\n */\n seriesStrategy?: SeriesStrategy;\n}\n\n// ---------------------------------------------------------------------------\n// Label configuration\n// ---------------------------------------------------------------------------\n\n/**\n * Label density mode controlling how many data labels are shown.\n * - 'all': show every label, skip collision detection\n * - 'auto': show labels with collision detection (default)\n * - 'endpoints': show only first and last per series (useful for line charts)\n * - 'none': hide all labels (rely on tooltips and legend)\n */\nexport type LabelDensity = 'all' | 'auto' | 'endpoints' | 'none';\n\n/** Label display configuration for chart data labels. */\nexport interface LabelConfig {\n /** How many labels to show. Defaults to 'auto'. */\n density?: LabelDensity;\n /** Number format override for label values (d3-format string, e.g. \",.0f\"). */\n format?: string;\n /** Literal string prepended to each formatted label value (e.g. \"-\" or \"$\"). */\n prefix?: string;\n /** Literal string appended to each formatted label value (e.g. \"%\" or \"x\"). */\n suffix?: string;\n /** Fixed CSS color for all labels. Overrides the default fill-derived color. */\n color?: string;\n /**\n * Per-series pixel offsets for fine-tuning label positions, keyed by series name.\n * @deprecated Per-series pixel offsets are a workaround for collision bugs\n * now fixed by the layout engine. Will be removed in a future version.\n */\n offsets?: Record<string, AnnotationOffset>;\n /** Font size in pixels for bar/column value labels. */\n fontSize?: number;\n}\n\n/** Shorthand: `false` disables all labels, `true` uses defaults, or pass a full config object. */\nexport type LabelSpec = boolean | LabelConfig;\n\n// ---------------------------------------------------------------------------\n// Legend configuration\n// ---------------------------------------------------------------------------\n\n/** Legend display configuration. Overrides the responsive-default position. */\nexport interface LegendConfig {\n /** Override the legend position. If omitted, the responsive strategy decides. */\n position?: LegendPosition;\n /** Pixel offset for fine-tuning legend position. */\n offset?: AnnotationOffset;\n /** Whether to show the legend. Defaults to true. Set to false to hide. */\n show?: boolean;\n /** Number of columns for horizontal legend layout. Overrides the default row limit. */\n columns?: number;\n /** Max number of legend entries before truncation. Remaining entries show as \"+N more\". */\n symbolLimit?: number;\n /** Maximum number of rows for top-positioned legends before truncation. Defaults to 2. */\n maxRows?: number;\n /** Series names to exclude from the legend. Excluded series still render in the chart. */\n exclude?: string[];\n}\n\n/**\n * Configuration for the searchable series highlight input (`seriesSearch`).\n * The boolean form (`seriesSearch: true`) uses the defaults.\n */\nexport interface SeriesSearchConfig {\n /** Placeholder text for the search input. Defaults to \"Find a series\". */\n placeholder?: string;\n}\n\n/**\n * Configuration for the \"you draw it\" interactive engagement format\n * (`youDrawIt`). The reader draws their guess of the trend from `from` to the\n * right edge before the real line reveals. Line marks only, single-series only.\n */\nexport interface YouDrawItConfig {\n /**\n * The x value (matching `encoding.x`'s domain) where drawing starts. Data\n * before this point renders normally; data at or after it is hidden behind\n * a hatched \"draw here\" region until reveal.\n */\n from: string | number;\n /** Prompt text shown in the hatched drawing region. Defaults to \"Draw your guess\". */\n prompt?: string;\n /** Label for the skip-to-reveal button. Defaults to \"Show me\". */\n revealLabel?: string;\n /**\n * Optional extra comparison line (e.g. \"what everyone else guessed\"),\n * supplied by the host app. Rendered in a third, visually distinct style\n * alongside the reader's guess and the real line after reveal.\n */\n comparisonLine?: Array<{ x: string | number; y: number }>;\n}\n\n/**\n * Content composition for an endpoint-label entry: the series name, the\n * formatted value, or both joined on one line. See\n * {@link EndpointLabelsConfig.content}.\n */\nexport type EndpointLabelContent = 'label' | 'value' | 'label value';\n\n/**\n * Configuration for the endpoint labels column rendered at the chart's right edge\n * for multi-series line/area charts. Each entry pairs the series name with its\n * last formatted value, optionally anchored to the line by an open-circle marker.\n *\n * The column is independent of the traditional `legend` and the legacy\n * end-of-line labels. Together with `legend.show`, the three suppression toggles\n * follow this truth table for ≥2-series line/area charts:\n *\n * | `legend.show` | `endpointLabels` | Traditional legend | Endpoint column | End-of-line labels |\n * |--|--|--|--|--|\n * | unset | unset | hidden (auto-suppressed) | shown (default) | hidden |\n * | true | unset | shown | shown | hidden |\n * | unset | false | shown (auto-suppress revoked) | hidden | hidden |\n * | false | false | hidden | hidden | shown (last-resort) |\n * | true | false | shown | hidden | hidden |\n * | false | true | hidden | shown | hidden |\n * | true | true | shown | shown | hidden |\n *\n * Single-series charts: column is hidden by default (nothing to identify).\n *\n * The implementation is in `packages/engine/src/legend/suppression.ts` —\n * `resolveSuppression` is the single source of truth. The table above is\n * a user-facing mirror; if the two ever diverge, the engine wins. Tests\n * in `legend/__tests__/suppression.test.ts` enforce every cell.\n */\nexport interface EndpointLabelsConfig {\n /** Explicit on/off. When undefined, the chart auto-decides based on series count. */\n show?: boolean;\n /** Maximum number of series before endpoint labels switch to a legend. Default 8. */\n maxSeries?: number;\n /**\n * Which ends of the chart get label columns.\n * - `'end'` (default): labels on the right (trailing) edge only.\n * - `'both'`: labels on both left (leading) and right (trailing) edges.\n */\n ends?: 'end' | 'both';\n /**\n * What each entry renders.\n * - `'label'`: series name only.\n * - `'value'`: formatted value only.\n * - `'label value'`: series name and value joined on one line.\n *\n * When omitted, entries render the series name with the formatted value on\n * a second line (the classic endpoint column). Pass an object to set the\n * leading and trailing columns independently, e.g. the classic slope-chart\n * look is `{ leading: 'label value', trailing: 'value' }`.\n */\n content?:\n | EndpointLabelContent\n | { leading?: EndpointLabelContent; trailing?: EndpointLabelContent };\n /** Field to read the displayed value from. Defaults to `encoding.y.field`. */\n valueField?: string;\n /** d3-format string for the value. Defaults to `encoding.y.axis.format`. */\n format?: string;\n /** Max wrap width in pixels for long series names. Default 96. */\n width?: number;\n /** Render an open-circle marker on the line at the right edge. Default true. */\n showMarker?: boolean;\n /** Override the marker style. */\n markerStyle?: {\n fill?: string;\n stroke?: string;\n strokeWidth?: number;\n radius?: number;\n };\n /**\n * Render a thin leader line from the swatch row back to the line endpoint\n * when collision-sweep displaces a label off its data point. Default false:\n * the marker on the line plus the swatch in the column already pair label\n * to data; the connector tends to add visual noise for small displacements.\n * Opt in when labels collide hard and you need the explicit tie-back.\n */\n showLeader?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Spec types (the top-level discriminated union)\n// ---------------------------------------------------------------------------\n\n/** Data row: a plain object with string keys. */\nexport type DataRow = Record<string, unknown>;\n\n/** Per-series visual style overrides for line/area charts. */\nexport interface SeriesStyle {\n /** Line dash style. Defaults to 'solid'. */\n lineStyle?: 'solid' | 'dashed' | 'dotted';\n /** Whether to show data point markers. Defaults to true. */\n showPoints?: boolean;\n /** Stroke width override. */\n strokeWidth?: number;\n /** Opacity override (0-1). */\n opacity?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Animation\n// ---------------------------------------------------------------------------\n\n/**\n * Named easing presets for entrance animations.\n * Uses CSS linear() curves. Named 'ease' (not 'easing') to match Vega convention.\n */\nexport type AnimationEase = 'smooth' | 'snappy';\n\n/** Stagger configuration for sequential element reveal. */\nexport interface AnimationStagger {\n /** Delay between each element in ms. Default: 80 */\n delay?: number;\n /** Ordering strategy. Default: 'index' (DOM order) */\n order?: 'index' | 'value' | 'reverse';\n}\n\n/** Fine-grained responsive behavior configuration. */\nexport interface ResponsiveConfig {\n /**\n * Automatic annotation thinning at narrow widths. When true (default),\n * annotations that can't be placed without overlap are demoted to numbered\n * dot markers with footnote text below the chart. Set false to restore\n * the pre-auto-thinning behavior (annotations hidden at compact breakpoints).\n */\n autoThin?: boolean;\n}\n\n/**\n * Animation phase config. Follows Vega's enter/update/exit model.\n * Each phase can be true (use defaults) or a config object.\n */\nexport interface AnimationPhaseConfig {\n /** Duration in ms. Default: 500 for enter. */\n duration?: number;\n /** Easing preset. Default: 'smooth'. */\n ease?: AnimationEase;\n /** Stagger config. true = defaults, false = no stagger. Default: true for enter. */\n stagger?: AnimationStagger | boolean;\n}\n\n/**\n * Data-update phase config. Adds `maxMarks` on top of the shared phase\n * options; the cap is meaningless for enter/exit, which animate via CSS.\n */\nexport interface UpdatePhaseConfig extends AnimationPhaseConfig {\n /**\n * Maximum mark count that still runs a tweened data-update transition.\n * Default: 500. Above the cap the chart swaps instantly instead, because\n * per-frame SVG attribute writes on thousands of elements drop frames on\n * low-end devices. Raise it deliberately when the audience is desktop-heavy\n * and you have measured the frame budget.\n */\n maxMarks?: number;\n}\n\n/**\n * Full animation config object. Structured as enter/update/exit phases\n * following Vega's encoding set model.\n */\nexport interface AnimationConfig {\n /** Entrance animation when chart first renders. */\n enter?: AnimationPhaseConfig | boolean;\n /** Transition animation when data updates. */\n update?: UpdatePhaseConfig | boolean;\n /** Exit animation when marks are removed. */\n exit?: AnimationPhaseConfig | boolean;\n /** Delay before annotations animate in (ms after marks). Default: 200. */\n annotationDelay?: number;\n}\n\n/**\n * Animation spec property.\n * - true: enable entrance animation with sensible defaults\n * - false/omitted: no animation (current behavior)\n * - AnimationConfig: full control via enter/update/exit phases\n */\nexport type AnimationSpec = boolean | AnimationConfig;\n\n/**\n * Chart display mode.\n * - `'full'` (default): standard chart with chrome, axes, legend, padding.\n * - `'sparkline'`: minimal inline mini-chart. Strips chrome, axes, legend,\n * watermark, animation, and reduces margins to a tiny safety pad. The mark\n * fills the container edge-to-edge. Best with `mark: 'line' | 'area' | 'bar'`.\n * Explicit user fields (chrome, legend, encoding.x.axis, etc.) still render\n * when set, so users can opt in to specific elements.\n */\nexport type Display = 'full' | 'sparkline';\n\n/**\n * Breakpoint-conditional overrides for chart specs.\n *\n * Allows specifying different chrome, labels, legend, or annotations\n * per breakpoint. Merged shallowly into the base spec at compile time\n * when the container matches the breakpoint.\n */\nexport interface ChartSpecOverride {\n /** Override editorial chrome at this breakpoint. */\n chrome?: Chrome;\n /** Override label configuration at this breakpoint. */\n labels?: LabelSpec;\n /** Override legend configuration at this breakpoint. */\n legend?: LegendConfig;\n /** Override annotations at this breakpoint. */\n annotations?: Annotation[];\n /** Override animation at this breakpoint. */\n animation?: AnimationSpec;\n /** Override display mode (`'full'` or `'sparkline'`) at this breakpoint. */\n display?: Display;\n /** Override watermark visibility at this breakpoint. */\n watermark?: boolean;\n /** Override crosshair behavior at this breakpoint. */\n crosshair?: boolean;\n}\n\n/**\n * A KPI/metric cell rendered above the chart in a horizontal row.\n *\n * Used for editorial dashboards (e.g. \"CLOSE $186.10 +1.4%\") where the chart\n * is paired with summary statistics. Cells lay out evenly across the chart\n * width. Hidden in sparkline mode; auto-stripped when the container can't\n * fit the laid-out values.\n */\nexport interface Metric {\n /** Uppercase eyebrow label, e.g. \"CLOSE\". */\n label: string;\n /** Primary numeric value, e.g. \"$186.10\". */\n value: string;\n /** Optional change indicator, e.g. \"+1.4%\". Rendered next to the value. */\n delta?: string;\n /** Tone for the delta. 'up' = positive (green), 'down' = negative (red). Default 'up'. */\n deltaTone?: 'up' | 'down';\n /** Optional secondary value (e.g. multiplier \"10.3×\"). Rendered after the delta. */\n secondary?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Per-mark encoding interfaces (Task 2)\n// Required channels derived directly from MARK_ENCODING_RULES in encoding.ts.\n// TypeScript types must stay in sync with those runtime rules.\n// ---------------------------------------------------------------------------\n\n/**\n * Encoding for arc marks (pie/donut charts).\n * - `theta`: required (quantitative — the canonical slice value channel)\n * - `color`: required (nominal/ordinal — the category)\n * - `y`: deprecated alias for `theta`, accepted for backward compatibility\n */\nexport interface ArcEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n /** Canonical value channel for arc marks (quantitative — the slice value). */\n theta: EncodingChannel<TData>;\n color: EncodingChannel<TData> | ConditionalValueDef<TData>;\n}\n\n/**\n * Encoding for line marks.\n * - `x`: required (temporal or ordinal — the time/category axis)\n * - `y`: required (quantitative — the value axis)\n */\nexport interface LineEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x: EncodingChannel<TData>;\n y: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for bar marks (vertical columns and horizontal bars).\n * - `x`: required\n * - `y`: required\n */\nexport interface BarEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x: EncodingChannel<TData>;\n y: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for area marks.\n * - `x`: required (temporal or ordinal)\n * - `y`: required (quantitative)\n */\nexport interface AreaEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x: EncodingChannel<TData>;\n y: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for point marks (scatter plots).\n * - `x`: required\n * - `y`: required\n */\nexport interface PointEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x: EncodingChannel<TData>;\n y: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for circle marks (dot plots).\n * - `x`: required (quantitative)\n * - `y`: required (nominal/ordinal — the category axis)\n */\nexport interface CircleEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x: EncodingChannel<TData>;\n y: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for lollipop marks.\n * - `x`: required (quantitative)\n * - `y`: required (nominal/ordinal)\n */\nexport interface LollipopEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x: EncodingChannel<TData>;\n y: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for beeswarm marks (dodged distribution dots).\n * - Exactly one positional channel is quantitative: the value axis\n * (x = horizontal swarm, y = vertical swarm).\n * - The other positional channel is optional (nominal/ordinal): grouped\n * swarms, one lane per category. The cross axis has no scale; the dodge\n * layout computes pixel offsets around each lane center.\n */\nexport interface BeeswarmEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x?: EncodingChannel<TData>;\n y?: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for range marks (dumbbell / arrow / range-bar plots).\n * - Horizontal (the common editorial form): `y` nominal/ordinal category,\n * `x` quantitative start, `x2` quantitative end.\n * - Vertical: `x` nominal/ordinal category, `y` quantitative start,\n * `y2` quantitative end.\n */\nexport interface RangeEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x: EncodingChannel<TData>;\n y: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for waffle marks (unit grids for part-to-whole counts).\n * - `color`: required (nominal/ordinal, the category)\n * - `theta`: the canonical quantitative share, the same part-to-whole value\n * channel arc marks use. `y` is a deprecated alias, accepted for backward\n * compatibility.\n */\nexport interface WaffleEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n color: EncodingChannel<TData> | ConditionalValueDef<TData>;\n}\n\n/**\n * Encoding for parliament marks (hemicycle seat-dot charts).\n * - `color`: required (nominal/ordinal, the party)\n * - `theta`: the canonical quantitative seat count, the same part-to-whole\n * value channel arc/waffle marks use. `y` is a deprecated alias, accepted\n * for backward compatibility.\n */\nexport interface ParliamentEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n color: EncodingChannel<TData> | ConditionalValueDef<TData>;\n}\n\n/**\n * Encoding for calendar marks (GitHub-style calendar heatmaps).\n * - `x`: required (temporal, daily dates, one row per day)\n * - `color`: required (quantitative, the per-day value)\n *\n * The calendar computes its own weeks-x-weekdays geometry: no positional\n * scales or axes. Multi-year data partitions into one stacked band per year,\n * all bands sharing a single color scale and legend.\n */\nexport interface CalendarEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x: EncodingChannel<TData>;\n color: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for text marks (data-positioned labels).\n * - `text`: required (the field to render as text)\n * - `x`, `y`: optional positioning\n */\nexport interface TextEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n text: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for tick marks (strip/rug plots).\n * - `x`: required\n * - `y`: required\n */\nexport interface TickEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x: EncodingChannel<TData>;\n y: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for rect marks (heatmaps, 2D binned plots).\n * - `x`: required\n * - `y`: required\n */\nexport interface RectEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x: EncodingChannel<TData>;\n y: EncodingChannel<TData>;\n}\n\n// ---------------------------------------------------------------------------\n// Accessibility configuration\n// ---------------------------------------------------------------------------\n\n/**\n * Author-facing accessibility overrides.\n *\n * Everything here is optional; when omitted, openchart auto-generates alt\n * text, ARIA labels, and a screen-reader data table from the spec and data.\n */\nexport interface A11yConfig {\n /**\n * Alt text announced by screen readers for the chart as a whole. Replaces\n * the auto-generated description. Also settable via the top-level\n * `description` field (Vega-Lite convention); this field wins when both\n * are present.\n */\n description?: string;\n /**\n * Hide the chart from assistive technology entirely (`aria-hidden`).\n * Use only when the chart is decorative or the surrounding content\n * already conveys the same information. Also suppresses the hidden\n * screen-reader data table and keyboard mark navigation.\n */\n hidden?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Shared (non-mark-specific) ChartSpec properties\n// ---------------------------------------------------------------------------\n\n/**\n * Properties shared across all ChartSpec mark variants.\n * Extracted to avoid repeating them in every union member.\n *\n * @internal\n */\ninterface BaseChartSpec<TData extends DataRow = DataRow> {\n /**\n * Data array: each element is a row with field values.\n * The Vega-Lite object form `{ values: [...] }` is also accepted at runtime\n * and unwrapped during the pre-validation sugar expansion. `{ url }` is not\n * supported (openchart does not fetch data).\n */\n data: TData[];\n /** Data transforms applied in order before encoding (filter, bin, calculate, timeUnit). */\n transform?: Transform[];\n /** Editorial chrome (title, subtitle, source, etc.). */\n chrome?: Chrome;\n /**\n * How chrome height interacts with the container height budget.\n *\n * - `'subtract'` (default): the container height is fixed and chrome text\n * (title/subtitle/source) is subtracted from it, shrinking the plot. This\n * is the historical behavior.\n * - `'grow'`: the container height is treated as the *plot* budget, and the\n * rendered SVG grows taller by the measured chrome height. The plot keeps\n * its full budget (minus axis/padding overhead) regardless of how many\n * lines the title wraps to.\n *\n * `'grow'` is a no-op for faceted specs in this version.\n */\n chromeLayout?: 'subtract' | 'grow';\n /**\n * Top-level title sugar (Vega-Lite aligned). Expanded into `chrome.title`\n * (and `chrome.subtitle` for the object form). `chrome.title` wins when\n * both are set.\n */\n title?: string | { text: string; subtitle?: string };\n /** Top-level subtitle sugar. Expanded into `chrome.subtitle`; `chrome.subtitle` wins. */\n subtitle?: string;\n /**\n * Alt text for the rendered chart (Vega-Lite aligned). Sugar for\n * `a11y.description`; `a11y.description` wins when both are set.\n * When absent, alt text is auto-generated from the spec and data.\n */\n description?: string;\n /** Accessibility overrides. See {@link A11yConfig}. */\n a11y?: A11yConfig;\n /**\n * Fixed render width in pixels (Vega-Lite aligned). Overrides the\n * container-derived width. When both `width` and `height` are set,\n * `responsive: false` is implied unless `responsive` is set explicitly.\n * Omit for the default container-driven sizing.\n */\n width?: number;\n /** Fixed render height in pixels (Vega-Lite aligned). See `width`. */\n height?: number;\n /** Accepted for Vega-Lite compatibility and ignored (with a compile warning). */\n $schema?: string;\n /**\n * KPI/metric cells rendered as a horizontal row between subtitle and chart\n * area. Each cell shows a label/value pair with optional delta and secondary\n * value. Hidden in sparkline mode and auto-stripped when the container is\n * too narrow or short, or when value text would overflow its cell.\n */\n metrics?: Metric[];\n /** Data annotations (text callouts, highlighted ranges, reference lines). */\n annotations?: Annotation[];\n /** Label display configuration. `false` disables all labels, `true` uses defaults. */\n labels?: LabelSpec;\n /** Legend display configuration (position override). */\n legend?: LegendConfig;\n /**\n * Right-side endpoint labels column for multi-series line/area charts.\n *\n * - `true` or `EndpointLabelsConfig`: render the column.\n * - `false`: hide the column.\n * - omitted: auto-enable for multi-series line/area charts, hide otherwise.\n *\n * See {@link EndpointLabelsConfig} for the full suppression truth table that\n * relates this flag to `legend.show` and the legacy end-of-line labels.\n */\n endpointLabels?: boolean | EndpointLabelsConfig;\n /**\n * Responsive behavior. `true`/`false` enables/disables container-width\n * adaptation. Pass a `ResponsiveConfig` object for fine-grained control\n * (e.g. `{ autoThin: false }` to disable automatic annotation thinning).\n */\n responsive?: boolean | ResponsiveConfig;\n /** Theme configuration overrides. */\n theme?: ThemeConfig;\n /** Dark mode behavior. Defaults to \"off\". */\n darkMode?: DarkMode;\n /**\n * Whether to show the OpenData watermark. Defaults to true; auto-hides\n * when the container is under 200px tall (the cramped height class) unless\n * explicitly set.\n */\n watermark?: boolean;\n /**\n * Series names to hide from rendering by default. This is the authored\n * persistence target for legend visibility. Runtime legend toggles\n * (legend-toggle edit events) layer on top of this via ephemeral runtime\n * sets but never modify it.\n */\n hiddenSeries?: string[];\n /** Per-series visual overrides, keyed by series name (the color field value). */\n seriesStyles?: Record<string, SeriesStyle>;\n /**\n * Breakpoint-conditional overrides. Keys are breakpoint names.\n * At compile time, if the container matches a breakpoint, its overrides\n * are shallow-merged into the spec before layout computation.\n */\n overrides?: Partial<Record<Breakpoint, ChartSpecOverride>>;\n /**\n * Animation configuration.\n * - true: enable entrance animation with sensible defaults\n * - false/omitted: no animation (current behavior)\n * - AnimationConfig: full control via enter/update/exit phases\n */\n animation?: AnimationSpec;\n /**\n * Show a vertical crosshair line that tracks the nearest data point on\n * line and area charts. Only active when a voronoi overlay is present.\n * Defaults to false. In sparkline display mode, defaults to false unless\n * explicitly set.\n */\n crosshair?: boolean;\n /**\n * Searchable series highlight (\"find your country\"). Renders a compact\n * search input above the chart with typeahead over the color channel's\n * distinct values. Selecting a value adds it to the highlight set\n * (multi-select chips, removable); clearing restores the authored\n * `encoding.color.highlight` baseline. Requires a categorical color\n * encoding. Mutually exclusive with edit mode (search wins).\n */\n seriesSearch?: boolean | SeriesSearchConfig;\n /**\n * \"You draw it\" interactive engagement format: the reader draws their\n * guess of the trend before the real line reveals. Line marks only,\n * single-series only. Mutually exclusive with edit mode (validation\n * warning if both are enabled; youDrawIt is disabled).\n */\n youDrawIt?: YouDrawItConfig;\n /**\n * Display mode controlling how much chart chrome is rendered.\n *\n * - `'full'` (default): full publication chart with chrome, axes, legend, padding.\n * - `'sparkline'`: inline mini-chart for dashboards/KPI cards. Strips chrome,\n * axes, legend, watermark, animation, and crosshair. The mark fills the\n * container edge-to-edge with a small safety margin. Best with\n * `mark: 'line' | 'area' | 'bar' | 'point'`.\n *\n * **Override precedence:** explicit user fields always win, even in sparkline\n * mode. Setting `chrome.title` on a sparkline still renders the title.\n * Setting `legend: { show: true }` still renders the legend. This applies at\n * top-level and per-breakpoint overrides.\n */\n display?: Display;\n /**\n * Resolution strategy for shared/independent scales and axes across facet\n * panels. Only meaningful when `encoding.facet` is present. Shared scales\n * (the default) let readers compare across panels; independent scales give\n * each panel its own domain.\n */\n resolve?: ResolveConfig;\n /**\n * Render order within a LayerSpec. Higher values render on top.\n * When omitted, layers render in array order (later layers paint on top).\n * Axis assignment (left/right y) is always determined by array position,\n * not zIndex.\n */\n zIndex?: number;\n}\n\n/**\n * Chart specification: the primary input for standard chart types.\n *\n * Uses the Vega-Lite `mark` property instead of `type` to specify\n * the visualization mark. The mark can be a string shorthand or an\n * object with additional properties (interpolation, point markers, etc.).\n *\n * This is a discriminated union — the `mark` value determines which encoding\n * channels are required. TypeScript enforces required channels at compile time,\n * matching the runtime rules in `MARK_ENCODING_RULES`.\n *\n * @template TData - The shape of a single data row. When provided, `encoding.*.field`\n * values are constrained to `keyof TData` and IDEs autocomplete your column names.\n * Defaults to `DataRow` (no constraint) — existing untyped specs work unchanged.\n *\n * @example\n * // Typed: field autocomplete + typo detection\n * type SalesRow = { date: string; revenue: number; region: string };\n * const spec: ChartSpec<SalesRow> = {\n * mark: 'line',\n * data: rows,\n * encoding: {\n * x: { field: 'date', type: 'temporal' }, // autocompletes\n * y: { field: 'revenue', type: 'quantitative' }, // typos fail at compile time\n * }\n * };\n *\n * @example\n * // Untyped: works exactly as before (no migration needed)\n * const spec: ChartSpec = {\n * mark: 'bar',\n * data: myData,\n * encoding: { x: { field: 'category', type: 'nominal' }, y: { field: 'value', type: 'quantitative' } }\n * };\n */\nexport type ChartSpec<TData extends DataRow = DataRow> =\n | (BaseChartSpec<TData> & {\n mark: 'arc' | (MarkDef & { type: 'arc' });\n encoding: ArcEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'line' | (MarkDef & { type: 'line' });\n encoding: LineEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'bar' | (MarkDef & { type: 'bar' });\n encoding: BarEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'area' | (MarkDef & { type: 'area' });\n encoding: AreaEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'point' | (MarkDef & { type: 'point' });\n encoding: PointEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'circle' | (MarkDef & { type: 'circle' });\n encoding: CircleEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'lollipop' | (MarkDef & { type: 'lollipop' });\n encoding: LollipopEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'beeswarm' | (MarkDef & { type: 'beeswarm' });\n encoding: BeeswarmEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'range' | (MarkDef & { type: 'range' });\n encoding: RangeEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'waffle' | (MarkDef & { type: 'waffle' });\n encoding: WaffleEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'text' | (MarkDef & { type: 'text' });\n encoding: TextEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'tick' | (MarkDef & { type: 'tick' });\n encoding: TickEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'rect' | (MarkDef & { type: 'rect' });\n encoding: RectEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'rule' | (MarkDef & { type: 'rule' });\n encoding: Encoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'calendar' | (MarkDef & { type: 'calendar' });\n encoding: CalendarEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'parliament' | (MarkDef & { type: 'parliament' });\n encoding: ParliamentEncoding<TData>;\n });\n\n/** Row density for tables. Maps to 40 / 48 / 56px row heights. */\nexport type TableDensity = 'condensed' | 'regular' | 'relaxed';\n\n/**\n * Table specification: input for data table visualizations.\n *\n * Tables are a visualization type, not just an HTML grid. They support\n * heatmap coloring, inline sparklines, sorted columns, search, and pagination.\n */\nexport interface TableSpec {\n /** Discriminant: always \"table\". */\n type: 'table';\n /** Data array: each element is a row. */\n data: DataRow[];\n /** Column definitions controlling display, sorting, formatting, and mini-charts. */\n columns: ColumnConfig[];\n /** Optional field to use as a unique row identifier. */\n rowKey?: string;\n /** Editorial chrome. */\n chrome?: Chrome;\n /** Theme configuration overrides. */\n theme?: ThemeConfig;\n /** Dark mode behavior. */\n darkMode?: DarkMode;\n /** Whether to show the OpenData watermark. Defaults to true. */\n watermark?: boolean;\n /** Enable client-side search/filter. */\n search?: boolean;\n /** Pagination configuration. True for defaults, or an object with pageSize. */\n pagination?: boolean | { pageSize: number };\n /** Whether to stick the first column during horizontal scroll. */\n stickyFirstColumn?: boolean;\n /**\n * Row density: `'condensed'` (40px rows), `'regular'` (48px, default),\n * `'relaxed'` (56px). An explicit value always wins over the responsive\n * auto-condense.\n */\n density?: TableDensity;\n /** Zebra striping on body rows. Defaults to false (hairlines only). */\n striped?: boolean;\n /** Initial sort. Falls back to the first inline-bar column, descending. */\n sort?: SortState;\n /**\n * Sticky footer summing every quantitative column over the filtered rows\n * (the whole result set, not just the current page). `true` uses the label\n * \"Total\".\n */\n totalRow?: boolean | { label?: string };\n /**\n * @deprecated Use `density: 'condensed'`. Still honored, with a warning.\n */\n compact?: boolean;\n /** Whether the table adapts to container width. Defaults to true. */\n responsive?: boolean;\n /** Animation configuration for entrance animations. */\n animation?: AnimationSpec;\n}\n\n/** Graph node: must have an id, plus arbitrary data fields. */\nexport interface GraphNode {\n /** Unique identifier for the node. */\n id: string;\n /** Arbitrary data fields. */\n [key: string]: unknown;\n}\n\n/** Graph edge: connects two nodes by id. */\nexport interface GraphEdge {\n /** Source node id. */\n source: string;\n /** Target node id. */\n target: string;\n /** Arbitrary data fields (weight, type, confidence, etc.). */\n [key: string]: unknown;\n}\n\n/** Per-node visual overrides, keyed by node id. */\nexport interface NodeOverride {\n /** Override fill color. */\n fill?: string;\n /** Override radius. */\n radius?: number;\n /** Override stroke width. */\n strokeWidth?: number;\n /** Override stroke color. */\n stroke?: string;\n /** Force label to always show regardless of zoom/priority. */\n alwaysShowLabel?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Graph animation + interaction\n// ---------------------------------------------------------------------------\n\n/**\n * Per-phase and per-behavior motion config for a graph.\n *\n * DEFAULT-ON semantics (deliberately different from charts, which are opt-in):\n * omit `animation` → the resolver returns full defaults; `animation: false` →\n * no choreography (but `layout.warmup` still applies); a per-phase `false`\n * disables just that phase. Graphs are default-on because physics motion\n * already exists on load and choreography reduces the chaos.\n */\nexport interface GraphAnimationConfig {\n /** Node/edge reveal on first render. Default `{ duration: 500, ease: 'smooth', stagger: true }`. */\n enter?: AnimationPhaseConfig | boolean;\n /** Data-update enter-fade for newly added marks. Default `{ duration: 300, ease: 'smooth' }`. */\n update?: AnimationPhaseConfig | boolean;\n /** Ghost fade-out for removed marks. Default `{ duration: 300, ease: 'smooth' }`. */\n exit?: AnimationPhaseConfig | boolean;\n /** Camera flight easing. Default duration `'auto'` (derived from the zoom distance). */\n camera?: { duration?: number; ease?: AnimationEase } | boolean;\n /** Hover emphasis crossfade. Default `{ duration: 150, ease: 'smooth' }`. */\n hover?: { duration?: number; ease?: AnimationEase } | boolean;\n}\n\n/**\n * Graph motion spec.\n * - omitted → full defaults (default-ON)\n * - `false` → no choreography (warmup still applies)\n * - object → per-phase control\n *\n * `prefers-reduced-motion` is always respected regardless of this value.\n */\nexport type GraphAnimationSpec = boolean | GraphAnimationConfig;\n\n/** Interaction behavior for a graph (hover mode, select-fly, opt-in physics). */\nexport interface GraphInteractionConfig {\n /**\n * Hover emphasis behavior. `neighbors` (default) lights the hovered node and\n * its adjacency; `category` lights same-category nodes; `node` lights only the\n * hovered node; `none` disables. `dimOpacity` (default 0.3) is the node dim\n * tier. Named `mode` (not `highlight`) to avoid colliding with the three other\n * meanings of \"highlight\" (the encoding value list, the `highlight()` verb).\n */\n hover?: { mode?: 'neighbors' | 'category' | 'node' | 'none'; dimOpacity?: number };\n /** Selecting a node flies the camera to it. Default `false`. */\n select?: { flyTo?: boolean };\n /**\n * Opt-in cursor repulsion (nodes drift away from the pointer). Disabled under\n * `prefers-reduced-motion` and above the node-count gate.\n */\n cursorRepulsion?: boolean | { radius?: number; strength?: number };\n /** Opt-in springy node drag (user-initiated; no reduced-motion gate). */\n springyDrag?: boolean;\n}\n\nexport interface GraphSpec {\n /** Discriminant: always \"graph\". */\n type: 'graph';\n /** Node array. Each node must have an id field. */\n nodes: GraphNode[];\n /** Edge array. Each edge connects source and target node ids. */\n edges: GraphEdge[];\n /** Graph-specific encoding mapping visual properties to node/edge fields. */\n encoding?: GraphEncoding;\n /** Layout algorithm configuration. */\n layout?: GraphLayoutConfig;\n /** Per-node visual overrides, keyed by node id. */\n nodeOverrides?: Record<string, NodeOverride>;\n /**\n * Node treated as the graph's seed/root: gets a distinct ring and an\n * always-on label without needing a `nodeOverrides` entry, and stays lit when\n * a category highlight or legend filter dims everything else.\n *\n * String shorthand is the node id; the object form adds styling that layers\n * under any `nodeOverrides` entry for the same id (an explicit override\n * always wins). The seed's radius is deliberately left to the `nodeSize`\n * encoding — set `style.radius` to make it bigger.\n *\n * NOT related to `layout.seed`, which is the deterministic RNG seed for the\n * force layout. This field names a node; `layout.seed` is a number.\n *\n * An id that isn't in `nodes` warns and is ignored (it does not throw), so a\n * filtered or paginated node update can't crash the host.\n */\n seedNode?: string | { id: string; style?: NodeOverride };\n /** Editorial chrome. */\n chrome?: Chrome;\n /** Annotations. */\n annotations?: Annotation[];\n /** Theme configuration overrides. */\n theme?: ThemeConfig;\n /** Dark mode behavior. */\n darkMode?: DarkMode;\n /**\n * Motion configuration. DEFAULT-ON: omit for full defaults, `false` to opt\n * out of choreography (layout warmup still applies).\n */\n animation?: GraphAnimationSpec;\n /** Interaction behavior (hover mode, select-fly, opt-in physics). */\n interaction?: GraphInteractionConfig;\n /**\n * Built-in legend. Interactive by default. Set `false` if you render your own\n * legend, so the built-in filter UI doesn't compete with it.\n */\n legend?: boolean | { interactive?: boolean; counts?: boolean };\n /** Whether to show the OpenData watermark. Defaults to true. */\n watermark?: boolean;\n /**\n * Render in two or three dimensions. Defaults to `2` (the Canvas renderer).\n *\n * `3` renders with WebGL. The 3D renderer ships on its own subpath so\n * three.js never enters the default bundle, and it must be imported before\n * the graph mounts:\n *\n * ```ts\n * import '@opendata-ai/openchart-vanilla/graph-3d';\n * ```\n *\n * (Framework hosts import their own package's mirror of the subpath:\n * `@opendata-ai/openchart-react/graph-3d`,\n * `@opendata-ai/openchart-vue/graph-3d`,\n * `@opendata-ai/openchart-svelte/graph-3d`.) Mounting a `dimensions: 3` spec\n * without that import throws.\n *\n * 3D differs from 2D in these ways:\n * - Labels use a fixed budget re-ranked by camera distance, not the 2D\n * priority declutter, so distant labels drop out.\n * - The simulation runs on the main thread (no worker, no wall-clock warmup\n * budget).\n * - Keyboard navigation, SVG export, `interaction.cursorRepulsion` and\n * `interaction.springyDrag` are unsupported. Each warns and is ignored.\n * (`layout.type` of `'radial'` or `'hierarchical'` is rejected by spec\n * validation in both dimensions, so it never reaches either renderer.)\n * - `getCamera()` returns a pose rather than a zoom transform: `x`/`y` are\n * the look-at point in world units, alongside `position` and `target`. In\n * 2D they are the zoom transform's translate in pixels. `onCameraChange`\n * carries the same payload, so persisted camera state has to be keyed on\n * the dimension it came from. Both are typed `GraphCamera` (vanilla), whose\n * `position`/`target` are optional because only 3D sets them.\n * - A structural `update()` reheats the whole layout globally, so settled\n * nodes drift; 2D applies a local impulse instead.\n * - Above 2000 nodes the spec warns and falls back to 2D.\n * - `nodeOverrides[*].stroke` and `strokeWidth` are ignored (no ring).\n */\n dimensions?: 2 | 3;\n}\n\n// ---------------------------------------------------------------------------\n// Layer spec (multi-layer composition)\n// ---------------------------------------------------------------------------\n\n/**\n * Resolution strategy for shared resources across layers.\n * 'shared' (default): union domains, single axis/legend.\n * 'independent': each layer gets its own scale/axis/legend.\n */\nexport type ResolveMode = 'shared' | 'independent';\n\n/**\n * Per-channel resolution config. Controls whether scales, axes, and legends\n * are shared or independent across layers.\n */\nexport interface ResolveConfig {\n scale?: Partial<Record<'x' | 'y' | 'color' | 'size', ResolveMode>>;\n axis?: Partial<Record<'x' | 'y', ResolveMode>>;\n legend?: Partial<Record<'color' | 'size', ResolveMode>>;\n}\n\n/**\n * Layer specification: composites multiple chart layers into a single view.\n *\n * Each element in `layer` is either a ChartSpec or another LayerSpec (nested).\n * Shared data, encoding, and transforms at the LayerSpec level are inherited\n * by children that don't define their own.\n *\n * @template TData - The shape of a single data row. When all layers share the\n * same data shape, pass it here to get field autocomplete across the layer.\n * For layers with different data shapes per child, omit TData (defaults to\n * `DataRow`) — children can be independently typed.\n */\nexport interface LayerSpec<TData extends DataRow = DataRow> {\n /** Array of child layers (ChartSpec or nested LayerSpec). */\n layer: (ChartSpec<TData> | LayerSpec<TData>)[];\n /** Shared data inherited by children without their own data. */\n data?: TData[];\n /** Shared encoding inherited by children (overridden per-channel by child). */\n encoding?: Encoding<TData>;\n /** Shared transforms. Parent transforms run before child transforms. */\n transform?: Transform[];\n /** Editorial chrome (title, subtitle, source, etc.). */\n chrome?: Chrome;\n /** Top-level title sugar (VL aligned). Expanded into `chrome.title`; `chrome.title` wins. */\n title?: string | { text: string; subtitle?: string };\n /** Top-level subtitle sugar. Expanded into `chrome.subtitle`; `chrome.subtitle` wins. */\n subtitle?: string;\n /** Fixed render width in pixels (VL aligned). Overrides the container-derived width. */\n width?: number;\n /** Fixed render height in pixels (VL aligned). Overrides the container-derived height. */\n height?: number;\n /** Accepted for Vega-Lite compatibility and ignored (with a compile warning). */\n $schema?: string;\n /**\n * Annotations for the layered chart. When present, these are the\n * annotations rendered and available for editing. When absent, falls back\n * to the first leaf layer's annotations. Annotations on second and later\n * leaf layers are not rendered.\n *\n * For edit persistence: the `data-annotation-index` stamped on each\n * rendered annotation maps to this array (or the first leaf's if this is\n * unset). Place annotations here for reliable index-based edit matching.\n */\n annotations?: Annotation[];\n /** Label display configuration. `false` disables all labels, `true` uses defaults. */\n labels?: LabelSpec;\n /** Legend display configuration. */\n legend?: LegendConfig;\n /** Whether the chart adapts to container width. Defaults to true. */\n responsive?: boolean;\n /** Theme configuration overrides. */\n theme?: ThemeConfig;\n /** Dark mode behavior. Defaults to \"off\". */\n darkMode?: DarkMode;\n /** Whether to show the OpenData watermark. Defaults to true. */\n watermark?: boolean;\n /** Resolution strategy for shared scales/axes/legends. */\n resolve?: ResolveConfig;\n /**\n * Series names to hide from rendering by default. This is the authored\n * persistence target for legend visibility. Runtime legend toggles\n * (legend-toggle edit events) layer on top of this via ephemeral runtime\n * sets but never modify it.\n */\n hiddenSeries?: string[];\n /** Endpoint labels column, inherited by child layers that don't set their own. */\n endpointLabels?: boolean | EndpointLabelsConfig;\n /** Animation configuration. */\n animation?: AnimationSpec;\n}\n\n// ---------------------------------------------------------------------------\n// Sankey spec (encoding-centric flow diagram)\n// ---------------------------------------------------------------------------\n\n/** Node alignment strategy for sankey layout. */\nexport type SankeyNodeAlign = 'left' | 'right' | 'center' | 'justify';\n\n/** Link coloring strategy for sankey diagrams. */\nexport type SankeyLinkColor = 'gradient' | 'source' | 'target' | 'neutral';\n\n/** Encoding channels specific to sankey diagrams. */\nexport interface SankeyEncoding {\n /** Source node field (required, nominal). */\n source: EncodingChannel;\n /** Target node field (required, nominal). */\n target: EncodingChannel;\n /** Flow value field (required, quantitative). */\n value: EncodingChannel;\n /** Color encoding for nodes/links (optional, nominal). */\n color?: EncodingChannel;\n /** Tooltip encoding (optional). */\n tooltip?: EncodingChannel | EncodingChannel[];\n}\n\nexport interface SankeySpec {\n /** Discriminant: always \"sankey\". */\n type: 'sankey';\n /** Tabular flow data. Each row is a source-target-value link. */\n data: DataRow[];\n /** Encoding channels mapping data fields to visual properties. */\n encoding: SankeyEncoding;\n /** Width of node rectangles in pixels. Defaults to 12. */\n nodeWidth?: number;\n /** Vertical padding between nodes in pixels. Defaults to 16. */\n nodePadding?: number;\n /** Node alignment algorithm. Defaults to 'justify'. */\n nodeAlign?: SankeyNodeAlign;\n /** Number of layout relaxation iterations. Defaults to 6. */\n iterations?: number;\n /** Link coloring strategy. Defaults to 'gradient'. */\n linkStyle?: SankeyLinkColor;\n /** Link fill opacity (0-1). Defaults to 0.5 in light mode, 0.6 in dark mode. */\n linkOpacity?: number;\n /**\n * Bucket sub-threshold nodes into a single \"Other\" node per column. Opt-in:\n * omitted, every node in the data is drawn.\n *\n * A number is the threshold as a share of the node's own column total\n * (`0.05` = under 5% of that column). The object form adds a custom `label`.\n * A column with only one sub-threshold node is left alone, since renaming one\n * node \"Other\" hides its identity without buying any space. Bucketing never\n * changes the diagram's total flow.\n */\n other?: number | { threshold: number; label?: string };\n /**\n * Which side of each node to place labels. 'auto' uses the default heuristic\n * (left-column right, right-column left, middle right). 'right' forces all\n * labels to the right of their nodes. 'left' forces all labels to the left.\n * Defaults to 'auto'.\n */\n nodeLabelAlign?: 'auto' | 'left' | 'right';\n /**\n * Explicit vertical ordering of nodes. An array of node IDs listed from\n * top to bottom. Nodes sharing the same column are sorted by their position\n * in this array. Nodes not in the array are placed after those that are.\n */\n nodeSort?: string[];\n /** Editorial chrome (title, subtitle, source, byline, footer). */\n chrome?: Chrome;\n /** Legend display configuration. */\n legend?: LegendConfig;\n /** Theme configuration overrides. */\n theme?: ThemeConfig;\n /** Dark mode behavior. Defaults to \"off\". */\n darkMode?: DarkMode;\n /** Whether to show the OpenData watermark. Defaults to true. */\n watermark?: boolean;\n /** Animation configuration for entrance animations. */\n animation?: AnimationSpec;\n /**\n * d3-format string applied to flow values in tooltips and ARIA labels.\n * Uses the literal suffix extension: \".0f%\" appends \"%\" to the formatted\n * number (data value 28 renders as \"28%\"). For currency: \"$,.0f\" or \"$~s\".\n * For SI suffixes: \"~s\" (renders 1000 as \"1k\"). When not set, values use\n * the default number formatter.\n * @deprecated Use the encoding channel's `format` field instead. This\n * property is expanded into the encoding during compilation.\n */\n valueFormat?: string;\n}\n\n// ---------------------------------------------------------------------------\n// TileMap spec (US state tile grid map)\n// ---------------------------------------------------------------------------\n\n/** Encoding channels specific to tile map visualizations. */\nexport interface TileMapEncoding {\n /** State code field (required, nominal). Maps to US state abbreviations. */\n state: EncodingChannel;\n /**\n * Value field. For quantitative mode, maps to a sequential color scale.\n * For categorical mode (when color channel is present or values are strings),\n * maps to category labels shown in tooltips.\n */\n value: EncodingChannel;\n /** Color encoding channel (optional, nominal). When present, enables categorical coloring. */\n color?: EncodingChannel;\n /** Tooltip encoding (optional). */\n tooltip?: EncodingChannel | EncodingChannel[];\n}\n\n/** Sequential color palette names available for tile maps. */\nexport type TileMapPalette = 'blue' | 'green' | 'orange' | 'purple' | 'teal';\n\nexport interface TileMapSpec {\n /** Discriminant: always \"tilemap\". */\n type: 'tilemap';\n /**\n * Data for the tile map. Accepts either:\n * - A record mapping state codes to numeric values: `{ \"CA\": 12000, \"TX\": 8500 }`\n * - A record mapping state codes to string categories: `{ \"CA\": \"high\", \"TX\": \"low\" }`\n * - Tabular data rows with state and value fields (requires encoding)\n */\n data: Record<string, number | string | null> | DataRow[];\n /**\n * Encoding channels mapping data fields to visual properties.\n * Required when data is DataRow[]. Auto-generated when data is a record map.\n */\n encoding?: TileMapEncoding;\n /** Sequential color palette. Defaults to 'blue'. Only used in quantitative mode. */\n palette?: TileMapPalette;\n /**\n * Custom category-to-color mapping for categorical tilemaps.\n * Keys are category values, values are CSS color strings.\n * When provided, forces categorical mode regardless of encoding.\n * Example: `{ \"medical_only\": \"#ee4a73\", \"religious\": \"#e07d00\", \"philosophical\": \"#06b6d4\" }`\n */\n colors?: Record<string, string>;\n /** Editorial chrome (title, subtitle, source, byline, footer). */\n chrome?: Chrome;\n /** Legend display configuration. */\n legend?: LegendConfig;\n /** Theme configuration overrides. */\n theme?: ThemeConfig;\n /** Dark mode behavior. Defaults to \"off\". */\n darkMode?: DarkMode;\n /** Whether to show the OpenData watermark. Defaults to true. */\n watermark?: boolean;\n /** Animation configuration for entrance animations. */\n animation?: AnimationSpec;\n /**\n * d3-format string applied to tile values, legend labels, and tooltips.\n * Examples: \".1f\" for one decimal, \"$,.0f\" for currency, \"~s\" for SI.\n * @deprecated Use the encoding channel's `format` field instead. This\n * property is expanded into the encoding during compilation.\n */\n valueFormat?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Map spec (choropleth / symbol maps)\n// ---------------------------------------------------------------------------\n\n/** Supported map projections. */\nexport type GeoMapProjection = 'albersUsa' | 'mercator' | 'equalEarth' | 'identity';\n\n/** Focus target for map camera: feature id(s) to zoom/pan to. */\n/**\n * Fit the point layer's cluster. `true` fits every point; `{ field, value }`\n * fits only the points whose data row has `row[field] === value` (e.g. only the\n * campuses rated \"F\"), which lets a story pan between sub-clusters.\n */\nexport type GeoMapPointsFocus = true | { field: string; value: string | number };\n\nexport type GeoMapFocus =\n | string\n | number\n | Array<string | number>\n | { features: string | number | Array<string | number>; padding?: number }\n | { points: GeoMapPointsFocus; padding?: number };\n\n/** Geo configuration for map specs. */\nexport interface GeoMapGeo {\n /** TopoJSON topology object. User imports this from us-atlas, world-atlas, or their own source. */\n features: unknown;\n /** Field in the TopoJSON feature properties to use as the join key. Defaults to 'id'. */\n idField?: string;\n /**\n * Map projection. Inferred from the topology when omitted: pre-projected\n * coordinates get 'identity', a near-global longitude span gets the\n * equal-area 'equalEarth', anything else gets 'albersUsa'.\n */\n projection?: GeoMapProjection;\n /**\n * Focus the camera. A feature id (or array) fits those features; `{ features, padding }`\n * adds breathing room; `{ points: true, padding }` fits the points layer's cluster\n * instead of any feature (use when points occupy a small part of a large feature, so\n * fitting the feature would leave the cluster small and off-center);\n * `{ points: { field, value }, padding }` fits only the matching point subset, so a\n * story can pan between sub-clusters. null clears focus from a prior story step.\n */\n focus?: GeoMapFocus | null;\n}\n\n/** Encoding channels specific to map visualizations. */\nexport interface GeoMapEncoding {\n /** Join key field in the data. Required for choropleth; optional in basemap-only mode (points layer with empty data). */\n key?: EncodingChannel;\n /** Color encoding (quantitative or nominal). Drives fill color of features. Optional when a points layer is present (basemap-only mode). */\n color?: EncodingChannel;\n /** Tooltip encoding (optional). */\n tooltip?: EncodingChannel | EncodingChannel[];\n}\n\n/** Point/symbol layer rendered above choropleth features. */\nexport interface GeoMapPointsLayer {\n /** Tabular data for the points. Independent of the choropleth data join. */\n data: DataRow[];\n /** Field holding longitude (x coordinate). */\n longitude: EncodingChannel;\n /** Field holding latitude (y coordinate). */\n latitude: EncodingChannel;\n /** Optional quantitative size encoding. Area-proportional: r proportional to sqrt(value). No size legend is rendered in v1; explain the scale in chrome.subtitle if needed. */\n size?: EncodingChannel;\n /** Optional color encoding (nominal or quantitative). Independent scale from choropleth. */\n color?: EncodingChannel;\n /** Tooltip channel(s), same shape as GeoMapEncoding.tooltip. */\n tooltip?: EncodingChannel | EncodingChannel[];\n /** Stable id for event callbacks, and the key used to match points across\n * renders so fill, radius, and position tween on data update. */\n key?: EncodingChannel;\n /** Fill opacity for point circles. Default 0.65. Range 0-1. */\n opacity?: number;\n}\n\nexport interface GeoMapSpec {\n /** Discriminant: always \"map\". */\n type: 'map';\n /** Geo configuration: TopoJSON features, join key, and projection. */\n geo: GeoMapGeo;\n /** Tabular data to join to geo features. */\n data: DataRow[];\n /** Encoding channels mapping data fields to visual properties. */\n encoding: GeoMapEncoding;\n /** Editorial chrome (title, subtitle, source, byline, footer). */\n chrome?: Chrome;\n /** Legend display configuration. */\n legend?: LegendConfig;\n /** Theme configuration overrides. */\n theme?: ThemeConfig;\n /** Dark mode behavior. Defaults to \"off\". */\n darkMode?: DarkMode;\n /** Whether to show the OpenData watermark. Defaults to true. */\n watermark?: boolean;\n /** Optional point/symbol layer rendered above choropleth features. */\n points?: GeoMapPointsLayer;\n /** Animation configuration for entrance animations. */\n animation?: AnimationSpec;\n /**\n * d3-format string applied to color values in tooltips and legend.\n * @deprecated Use the encoding channel's `format` field instead.\n */\n valueFormat?: string;\n}\n\n// ---------------------------------------------------------------------------\n// BarList spec (ranked horizontal bar list)\n// ---------------------------------------------------------------------------\n\nexport interface BarListSpec {\n /** Discriminant: always \"barlist\". */\n type: 'barlist';\n /**\n * Data rows. Each row must have at least a label field and a value field.\n * Rendered as a ranked list of horizontal bars.\n */\n data: DataRow[];\n /** Encoding channels mapping data fields to visual properties. */\n encoding: BarListEncoding;\n /** Bar height in pixels. Defaults to 6. */\n barHeight?: number;\n /** Corner radius: number in px or \"pill\" for fully rounded ends. Defaults to \"pill\". */\n cornerRadius?: number | 'pill';\n /** Maximum number of rows to show. Defaults to 20. */\n maxItems?: number;\n /** Editorial chrome (title, subtitle, source, byline, footer). */\n chrome?: Chrome;\n /** Theme configuration overrides. */\n theme?: ThemeConfig;\n /** Dark mode behavior. Defaults to \"off\". */\n darkMode?: DarkMode;\n /** Whether to show the OpenData watermark. Defaults to true. */\n watermark?: boolean;\n /** Animation configuration for entrance animations. */\n animation?: AnimationSpec;\n /**\n * d3-format string applied to bar values and tooltips.\n * Examples: \".1f\" for one decimal, \"$,.0f\" for currency, \"~s\" for SI.\n * @deprecated Use the encoding channel's `format` field instead. This\n * property is expanded into the encoding during compilation.\n */\n valueFormat?: string;\n}\n\n/** Encoding channels for a barlist visualization. */\nexport interface BarListEncoding {\n /** Label field (required, nominal). The category name for each row. */\n label: EncodingChannel;\n /** Value field (required, quantitative). The numeric value that drives bar width. */\n value: EncodingChannel;\n /** Subtitle field (optional, nominal). Secondary text shown beside the label in lighter weight. */\n subtitle?: EncodingChannel;\n /** Color field (optional, nominal). Maps categories to colors from the palette. When omitted, colors cycle by row index. */\n color?: EncodingChannel;\n /** Tooltip encoding (optional). */\n tooltip?: EncodingChannel | EncodingChannel[];\n}\n\n/**\n * Top-level visualization spec: union discriminated by structural shape.\n *\n * - ChartSpec: has `mark` field (no `type`, no `layer`)\n * - LayerSpec: has `layer` field\n * - TableSpec: has `type: 'table'`\n * - GraphSpec: has `type: 'graph'`\n * - SankeySpec: has `type: 'sankey'`\n * - TileMapSpec: has `type: 'tilemap'`\n * - GeoMapSpec: has `type: 'map'`\n * - BarListSpec: has `type: 'barlist'`\n *\n * Election parliament (hemicycle) charts are `ChartSpec` with `mark:\n * 'parliament'`, not a separate top-level type: seat data (party + seats)\n * maps naturally onto encoding.color/y like arc and waffle, and riding the\n * existing chart pipeline gets scales, legend, tooltips, a11y, and dark mode\n * for free instead of a second bespoke compile path (see\n * `packages/engine/src/charts/parliament/`).\n */\nexport type VizSpec =\n | ChartSpec\n | LayerSpec\n | TableSpec\n | GraphSpec\n | SankeySpec\n | TileMapSpec\n | GeoMapSpec\n | BarListSpec;\n\n/**\n * Chart spec without runtime data, for persistence/storage.\n * Generic: `ChartSpecWithoutData<MyRow>` constrains encoding field names.\n */\nexport type ChartSpecWithoutData<TData extends DataRow = DataRow> = Omit<ChartSpec<TData>, 'data'>;\n/** Table spec without runtime data and columns, for persistence/storage. Columns can be auto-generated via dataTable(). */\nexport type TableSpecWithoutData = Omit<TableSpec, 'data' | 'columns'>;\n/** Graph spec without runtime data, for persistence/storage. */\nexport type GraphSpecWithoutData = Omit<GraphSpec, 'nodes' | 'edges'>;\n/** Sankey spec without runtime data, for persistence/storage. */\nexport type SankeySpecWithoutData = Omit<SankeySpec, 'data'>;\n/** TileMap spec without runtime data, for persistence/storage. */\nexport type TileMapSpecWithoutData = Omit<TileMapSpec, 'data'>;\n/** Map spec without runtime data, for persistence/storage. */\nexport type GeoMapSpecWithoutData = Omit<GeoMapSpec, 'data' | 'points'> & {\n points?: Omit<GeoMapPointsLayer, 'data'>;\n};\n/** BarList spec without runtime data, for persistence/storage. */\nexport type BarListSpecWithoutData = Omit<BarListSpec, 'data'>;\n/** Union of data-stripped spec types for persistence/storage. */\nexport type StoredVizSpec =\n | ChartSpecWithoutData\n | TableSpecWithoutData\n | GraphSpecWithoutData\n | SankeySpecWithoutData\n | TileMapSpecWithoutData\n | GeoMapSpecWithoutData\n | BarListSpecWithoutData;\n\n// ---------------------------------------------------------------------------\n// Transforms (Vega-Lite aligned)\n// ---------------------------------------------------------------------------\n\n/** Logical AND combinator for filter predicates. */\nexport interface LogicalAnd<T> {\n and: T[];\n}\n\n/** Logical OR combinator for filter predicates. */\nexport interface LogicalOr<T> {\n or: T[];\n}\n\n/** Logical NOT combinator for filter predicates. */\nexport interface LogicalNot<T> {\n not: T;\n}\n\n/** A relative-time reference that resolves against the data extent. */\nexport interface RelativeTimeRef {\n anchor: 'max' | 'min';\n offset: number;\n unit: 'day' | 'week' | 'month' | 'quarter' | 'year';\n}\n\n/** A predicate that tests a field value against a condition. */\nexport interface FieldPredicate {\n /**\n * Data field to test.\n * Note: FieldPredicate is used in transforms (FilterTransform) which operate\n * on the raw DataRow type — field is typed as string here since transforms\n * can reference computed/derived fields not present in the original TData.\n */\n field: string;\n /** Equals comparison. */\n equal?: unknown;\n /** Less than. */\n lt?: number | RelativeTimeRef;\n /** Less than or equal. */\n lte?: number | RelativeTimeRef;\n /** Greater than. */\n gt?: number | RelativeTimeRef;\n /** Greater than or equal. */\n gte?: number | RelativeTimeRef;\n /** Inclusive range [min, max]. */\n range?: [number | RelativeTimeRef, number | RelativeTimeRef];\n /** Value is one of these. */\n oneOf?: unknown[];\n /** Whether the value is valid (non-null, non-undefined, non-NaN). */\n valid?: boolean;\n}\n\n/**\n * A filter predicate: a field predicate or logical combination of predicates.\n */\nexport type FilterPredicate =\n | FieldPredicate\n | LogicalAnd<FilterPredicate>\n | LogicalOr<FilterPredicate>\n | LogicalNot<FilterPredicate>;\n\n/** Parameters for binning a field. */\nexport interface BinParams {\n /** Maximum number of bins. */\n maxbins?: number;\n /** Exact step size between bins. */\n step?: number;\n /** Whether to nice-ify bin boundaries. */\n nice?: boolean;\n /** Explicit extent [min, max] for binning. */\n extent?: [number, number];\n}\n\n/** Expression for calculate transforms. */\nexport interface CalculateExpression {\n /** Arithmetic operation to apply. */\n op: '+' | '-' | '*' | '/' | 'abs' | 'round' | 'floor' | 'ceil' | 'log' | 'sqrt';\n /** Primary field to operate on. */\n field: string;\n /** Secondary field for binary operations (+, -, *, /). */\n field2?: string;\n /** Constant value for binary operations when field2 is not provided. */\n value?: number;\n}\n\n/**\n * Time unit granularity for temporal field transformations.\n * Follows Vega-Lite conventions.\n */\nexport type TimeUnit =\n | 'year'\n | 'quarter'\n | 'month'\n | 'week'\n | 'day'\n | 'dayofyear'\n | 'date'\n | 'hours'\n | 'minutes'\n | 'seconds'\n | 'milliseconds'\n | 'yearmonth'\n | 'yearmonthdate'\n | 'monthdate'\n | 'hoursminutes';\n\n/** Filter transform: removes rows that don't match the predicate. */\nexport interface FilterTransform {\n filter: FilterPredicate;\n}\n\n/** Bin transform: adds binned field(s) to each row. */\nexport interface BinTransform {\n bin: true | BinParams;\n field: string;\n as: string | [string, string];\n}\n\n/** Calculate transform: adds a computed field to each row. */\nexport interface CalculateTransform {\n calculate: CalculateExpression;\n as: string;\n}\n\n/** Time unit transform: extracts a time unit from a temporal field. */\nexport interface TimeUnitTransform {\n timeUnit: TimeUnit;\n field: string;\n as: string;\n}\n\n/**\n * Aggregate transform: group rows and compute summary statistics (VL aligned).\n * Produces one row per group with the groupby fields and computed aggregates.\n */\nexport interface AggregateTransform {\n aggregate: Array<{ op: AggregateOp; field: string; as: string }>;\n groupby: string[];\n}\n\n/**\n * Fold transform: unpivot wide-format columns into key/value rows (VL aligned).\n * For each input row, produces N output rows (one per fold field) with all\n * non-fold fields copied plus a key column (field name) and value column (field value).\n */\nexport interface FoldTransform {\n fold: string[];\n /** Output field names for [key, value]. Defaults to ['key', 'value']. */\n as?: [string, string];\n}\n\n/** Window operation types for computing values relative to other rows. */\nexport type WindowOp = 'lag' | 'lead' | 'diff' | 'pct_change' | 'cumsum' | 'rank' | 'first_value';\n\n/** Sort field definition for window transforms. */\nexport interface WindowSortField {\n field: string;\n order?: 'ascending' | 'descending';\n}\n\n/** Window field definition specifying which operation to compute. */\nexport interface WindowFieldDef {\n op: WindowOp;\n field: string;\n /** Row offset for lag/lead/diff/pct_change. Defaults to 1. */\n offset?: number;\n as: string;\n}\n\n/** Window transform: computes values relative to other rows in sort order within a partition. */\nexport interface WindowTransform {\n window: WindowFieldDef[];\n /** Fields to sort by within each partition. */\n sort: WindowSortField[];\n /** Fields to partition (group) by. Each group is windowed independently. */\n groupby?: string[];\n}\n\n/** Discriminated union of all transform types. */\nexport type Transform =\n | FilterTransform\n | BinTransform\n | CalculateTransform\n | TimeUnitTransform\n | AggregateTransform\n | FoldTransform\n | WindowTransform;\n\n// ---------------------------------------------------------------------------\n// Conditional encoding (Vega-Lite aligned)\n// ---------------------------------------------------------------------------\n\n/**\n * A single condition with a test predicate and resulting value/field.\n * When the test passes for a datum, the condition's value/field is used.\n *\n * @template TData - Propagated from ChartSpec<TData>. Constrains `field` to\n * `keyof TData & string` when a typed data row is provided.\n */\nexport interface Condition<TData extends DataRow = DataRow> {\n /** Predicate to test against each datum. */\n test: FilterPredicate;\n /**\n * Static value to use when the condition is true.\n * Accepted values: CSS color string, opacity (0-1), size number, boolean flag,\n * or a gradient definition for color channels.\n */\n value?: string | number | boolean | null | GradientDef;\n /**\n * Data field to use when the condition is true.\n * Constrained to column names of `TData` when using `ChartSpec<TData>`.\n */\n field?: keyof TData & string;\n /** Field type for the conditional field. */\n type?: FieldType;\n}\n\n/**\n * A conditional value definition for an encoding channel.\n * Evaluates conditions in order, falling back to the default value.\n *\n * @template TData - Propagated from ChartSpec<TData>.\n */\nexport interface ConditionalValueDef<TData extends DataRow = DataRow> {\n /** One or more conditions to evaluate. */\n condition: Condition<TData> | Condition<TData>[];\n /**\n * Default value when no condition matches.\n * Accepted values: CSS color string, opacity (0-1), size number, boolean flag,\n * or a gradient definition for color channels.\n */\n value?: string | number | boolean | null | GradientDef;\n}\n\n/**\n * Check if a channel definition is a regular EncodingChannel (has 'field' at top level).\n * Use this to narrow `EncodingChannel | ConditionalValueDef | ValueDef` in encoding\n * channels that support conditional encoding (color, size, opacity).\n */\nexport function isEncodingChannel<TData extends DataRow = DataRow>(\n def: EncodingChannel<TData> | ConditionalValueDef<TData> | ValueDef | undefined,\n): def is EncodingChannel<TData> {\n if (!def) return false;\n return 'field' in def && !('condition' in def);\n}\n\n/**\n * Check if a channel definition is a ConditionalValueDef.\n */\nexport function isConditionalDef<TData extends DataRow = DataRow>(\n def: EncodingChannel<TData> | ConditionalValueDef<TData> | ValueDef | undefined,\n): def is ConditionalValueDef<TData> {\n if (!def) return false;\n return 'condition' in def;\n}\n\n/**\n * Check if a channel definition is a bare ValueDef (constant value, no field\n * or condition). These are expanded to mark-level properties during the\n * pre-validation sugar expansion.\n */\nexport function isValueDef<TData extends DataRow = DataRow>(\n def: EncodingChannel<TData> | ConditionalValueDef<TData> | ValueDef | undefined,\n): def is ValueDef {\n if (!def) return false;\n return 'value' in def && !('condition' in def) && !('field' in def);\n}\n\n// ---------------------------------------------------------------------------\n// Mark type helpers\n// ---------------------------------------------------------------------------\n\n/** All valid mark type strings for runtime checking. */\nexport const MARK_TYPES: ReadonlySet<string> = new Set<MarkType>([\n 'bar',\n 'line',\n 'area',\n 'point',\n 'circle',\n 'arc',\n 'text',\n 'rule',\n 'tick',\n 'rect',\n 'lollipop',\n 'beeswarm',\n 'range',\n 'waffle',\n 'calendar',\n 'parliament',\n]);\n\n/**\n * Mark types that lay out inside the chart area without positional axes\n * (no axis or gridline computation; margins shrink to padding).\n */\nexport function isAxislessMark(markType: MarkType): boolean {\n return (\n markType === 'arc' ||\n markType === 'waffle' ||\n markType === 'calendar' ||\n markType === 'parliament'\n );\n}\n\n/**\n * Extract the mark type string from a mark field (string or MarkDef).\n */\nexport function resolveMarkType(mark: MarkType | MarkDef): MarkType {\n return typeof mark === 'string' ? mark : mark.type;\n}\n\n/**\n * Extract the full MarkDef from a mark field, filling in defaults for string shorthand.\n */\nexport function resolveMarkDef(mark: MarkType | MarkDef): MarkDef {\n return typeof mark === 'string' ? { type: mark } : mark;\n}\n\n// ---------------------------------------------------------------------------\n// Type guards\n// ---------------------------------------------------------------------------\n\n/** Check if a spec is a ChartSpec (has `mark` field, not a layer/table/graph). */\nexport function isChartSpec(spec: VizSpec | Record<string, unknown>): spec is ChartSpec {\n return 'mark' in spec && !('layer' in spec);\n}\n\n/** Check if a spec is a LayerSpec (has `layer` array). */\nexport function isLayerSpec(spec: VizSpec | Record<string, unknown>): spec is LayerSpec {\n return 'layer' in spec && Array.isArray((spec as Record<string, unknown>).layer);\n}\n\n/** Check if a spec is a TableSpec. */\nexport function isTableSpec(spec: VizSpec | Record<string, unknown>): spec is TableSpec {\n return 'type' in spec && (spec as Record<string, unknown>).type === 'table';\n}\n\n/** Check if a spec is a GraphSpec. */\nexport function isGraphSpec(spec: VizSpec | Record<string, unknown>): spec is GraphSpec {\n return 'type' in spec && (spec as Record<string, unknown>).type === 'graph';\n}\n\n/** Check if a spec is a SankeySpec. */\nexport function isSankeySpec(spec: VizSpec | Record<string, unknown>): spec is SankeySpec {\n return 'type' in spec && (spec as Record<string, unknown>).type === 'sankey';\n}\n\n/** Check if a spec is a TileMapSpec. */\nexport function isTileMapSpec(spec: VizSpec | Record<string, unknown>): spec is TileMapSpec {\n return 'type' in spec && (spec as Record<string, unknown>).type === 'tilemap';\n}\n\n/** Check if a spec is a BarListSpec. */\nexport function isBarListSpec(spec: VizSpec | Record<string, unknown>): spec is BarListSpec {\n return 'type' in spec && (spec as Record<string, unknown>).type === 'barlist';\n}\n\n/** Check if a spec is a GeoMapSpec. */\nexport function isGeoMapSpec(spec: VizSpec | Record<string, unknown>): spec is GeoMapSpec {\n return 'type' in spec && (spec as Record<string, unknown>).type === 'map';\n}\n\n// ---------------------------------------------------------------------------\n// Annotation type guards\n// ---------------------------------------------------------------------------\n\n/** Check if an annotation is a TextAnnotation. */\nexport function isTextAnnotation(annotation: Annotation): annotation is TextAnnotation {\n return annotation.type === 'text';\n}\n\n/** Check if an annotation is a RangeAnnotation. */\nexport function isRangeAnnotation(annotation: Annotation): annotation is RangeAnnotation {\n return annotation.type === 'range';\n}\n\n/**\n * Check if an annotation is a RefLineAnnotation. Also accepts the deprecated\n * `'rule'` type at runtime (pre-sugar-expansion input), since `annotation.type`\n * may not yet be narrowed to the canonical `'refline'` form.\n */\nexport function isRefLineAnnotation(annotation: Annotation): annotation is RefLineAnnotation {\n const type: string = annotation.type;\n return type === 'refline' || type === 'rule';\n}\n\n// ---------------------------------------------------------------------------\n// Display name mapping for accessibility\n// ---------------------------------------------------------------------------\n\n/** Human-readable display names for mark types (used in alt text, error messages). */\nexport const MARK_DISPLAY_NAMES: Record<MarkType, string> = {\n bar: 'Bar chart',\n line: 'Line chart',\n area: 'Area chart',\n point: 'Scatter plot',\n circle: 'Dot plot',\n arc: 'Pie chart', // overridden to \"Donut chart\" when innerRadius > 0\n text: 'Text chart',\n rule: 'Rule chart',\n tick: 'Tick plot',\n rect: 'Heatmap',\n lollipop: 'Lollipop chart',\n beeswarm: 'Beeswarm chart',\n range: 'Range plot',\n waffle: 'Waffle chart',\n calendar: 'Calendar heatmap',\n parliament: 'Parliament chart',\n};\n","/**\n * Color blindness simulation and palette distinguishability checks.\n *\n * Uses Brettel, Vienot, and Mollon (1997) simulation matrices for\n * protanopia, deuteranopia, and tritanopia.\n *\n * These are approximations suitable for checking palette accessibility,\n * not medical-grade simulations.\n */\n\nimport { rgb } from 'd3-color';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** The four common types of color vision deficiency. */\nexport type ColorBlindnessType = 'protanopia' | 'deuteranopia' | 'tritanopia' | 'achromatopsia';\n\n// ---------------------------------------------------------------------------\n// Simulation matrices\n// ---------------------------------------------------------------------------\n\n// 3x3 color transformation matrices for each deficiency type.\n// Applied in linear RGB space to simulate how colors appear.\n// Source: Brettel, Vienot & Mollon (1997), simplified.\n\ntype Matrix3x3 = readonly [\n readonly [number, number, number],\n readonly [number, number, number],\n readonly [number, number, number],\n];\n\nconst PROTAN_MATRIX: Matrix3x3 = [\n [0.567, 0.433, 0.0],\n [0.558, 0.442, 0.0],\n [0.0, 0.242, 0.758],\n];\n\nconst DEUTAN_MATRIX: Matrix3x3 = [\n [0.625, 0.375, 0.0],\n [0.7, 0.3, 0.0],\n [0.0, 0.3, 0.7],\n];\n\nconst TRITAN_MATRIX: Matrix3x3 = [\n [0.95, 0.05, 0.0],\n [0.0, 0.433, 0.567],\n [0.0, 0.475, 0.525],\n];\n\n// Total color blindness: every channel collapses to Rec. 709 relative\n// luminance, computed in linear RGB like the other matrices.\nconst ACHROMAT_MATRIX: Matrix3x3 = [\n [0.2126, 0.7152, 0.0722],\n [0.2126, 0.7152, 0.0722],\n [0.2126, 0.7152, 0.0722],\n];\n\nconst MATRICES: Record<ColorBlindnessType, Matrix3x3> = {\n protanopia: PROTAN_MATRIX,\n deuteranopia: DEUTAN_MATRIX,\n tritanopia: TRITAN_MATRIX,\n achromatopsia: ACHROMAT_MATRIX,\n};\n\n// ---------------------------------------------------------------------------\n// sRGB linearization helpers\n// ---------------------------------------------------------------------------\n\nfunction linearize(v: number): number {\n const s = v / 255;\n return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;\n}\n\nfunction delinearize(v: number): number {\n const s = v <= 0.0031308 ? v * 12.92 : 1.055 * v ** (1 / 2.4) - 0.055;\n return Math.round(Math.max(0, Math.min(255, s * 255)));\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Simulate how a color appears under a given color blindness type.\n * Returns a hex color string.\n */\nexport function simulateColorBlindness(color: string, type: ColorBlindnessType): string {\n const c = rgb(color);\n if (c == null) return color;\n\n const lin = [linearize(c.r), linearize(c.g), linearize(c.b)];\n const m = MATRICES[type];\n\n const r = m[0][0] * lin[0] + m[0][1] * lin[1] + m[0][2] * lin[2];\n const g = m[1][0] * lin[0] + m[1][1] * lin[1] + m[1][2] * lin[2];\n const b = m[2][0] * lin[0] + m[2][1] * lin[1] + m[2][2] * lin[2];\n\n return rgb(delinearize(r), delinearize(g), delinearize(b)).formatHex();\n}\n\n/**\n * Check if colors in a palette are distinguishable under a given\n * color blindness type.\n *\n * Uses a minimum perceptual distance threshold in simulated space.\n * Returns true if all pairs of colors are sufficiently different.\n */\nexport function checkPaletteDistinguishability(\n colors: string[],\n type: ColorBlindnessType,\n minDistance = 30,\n): boolean {\n const simulated = colors.map((c) => {\n const s = rgb(simulateColorBlindness(c, type));\n return s ? [s.r, s.g, s.b] : [0, 0, 0];\n });\n\n for (let i = 0; i < simulated.length; i++) {\n for (let j = i + 1; j < simulated.length; j++) {\n const dr = simulated[i][0] - simulated[j][0];\n const dg = simulated[i][1] - simulated[j][1];\n const db = simulated[i][2] - simulated[j][2];\n const dist = Math.sqrt(dr * dr + dg * dg + db * db);\n if (dist < minDistance) return false;\n }\n }\n\n return true;\n}\n","/**\n * WCAG contrast ratio utilities.\n *\n * Uses d3-color for color space parsing and manipulation.\n * All functions accept CSS color strings (hex, rgb, hsl, named colors).\n */\n\nimport { rgb } from 'd3-color';\n\n// ---------------------------------------------------------------------------\n// Relative luminance (WCAG 2.1)\n// ---------------------------------------------------------------------------\n\n/**\n * Compute the relative luminance of a color per WCAG 2.1 definition.\n * https://www.w3.org/TR/WCAG21/#dfn-relative-luminance\n */\nexport function relativeLuminance(color: string): number {\n const c = rgb(color);\n if (c == null) return 0;\n\n const srgb = [c.r / 255, c.g / 255, c.b / 255];\n const linear = srgb.map((v) => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4));\n return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Check whether a color string parses to a fully opaque color.\n * Contrast ratios are only meaningful against opaque backgrounds; the\n * default chart background ('transparent') defers to the host page and\n * can't be checked at compile time.\n */\nexport function isOpaqueColor(color: string): boolean {\n const c = rgb(color);\n return c != null && !Number.isNaN(c.r) && c.opacity === 1;\n}\n\n/**\n * Compute the WCAG contrast ratio between two colors.\n * Returns a value between 1 (identical) and 21 (black on white).\n */\nexport function contrastRatio(fg: string, bg: string): number {\n const l1 = relativeLuminance(fg);\n const l2 = relativeLuminance(bg);\n const lighter = Math.max(l1, l2);\n const darker = Math.min(l1, l2);\n return (lighter + 0.05) / (darker + 0.05);\n}\n\n/**\n * Pick a legible label color (white or near-black) for text placed on top of `bg`.\n *\n * Uses a perceptual luminance threshold rather than a strict 4.5:1 contrast\n * gate. WCAG's 4.5:1 ratio is calibrated for body text on a page; bold value\n * labels sitting on a saturated filled bar read fine at lower ratios. A pure\n * contrast-ratio gate sends mid-tone fills (e.g. slate `#94a3b8`, L≈0.36) to\n * dark text even though white reads cleaner on them.\n *\n * The threshold is mode-dependent because of simultaneous contrast: the same\n * bar fill looks lighter against a dark canvas than against a white page, so\n * dark text reads more grounded on it. Dark mode therefore uses a lower\n * threshold (L < 0.30) to pivot mid-tone fills to dark text sooner.\n *\n * - Light mode (L < 0.42 → white): white on saturated and mid-tone fills;\n * dark text only on genuinely light fills (`#b0b0b0` and lighter).\n * - Dark mode (L < 0.30 → white): saturated fills keep white; mid-tone fills\n * (slate `#94a3b8`, cyan `#06b6d4`, mid-grey) pivot to dark text.\n */\nexport function pickLabelColor(bg: string, darkMode = false): string {\n const WHITE = '#ffffff';\n const DARK = '#111111';\n const threshold = darkMode ? 0.3 : 0.42;\n return relativeLuminance(bg) < threshold ? WHITE : DARK;\n}\n\n/**\n * Check if two colors meet WCAG AA contrast requirements.\n * Normal text: 4.5:1, large text (18px+ bold or 24px+): 3:1.\n */\nexport function meetsAA(fg: string, bg: string, largeText = false): boolean {\n const ratio = contrastRatio(fg, bg);\n return largeText ? ratio >= 3 : ratio >= 4.5;\n}\n\n/**\n * Find an accessible variant of `baseColor` against `bg`.\n *\n * Preserves the hue and saturation of baseColor but adjusts lightness\n * until the target contrast ratio is met. Returns the original color\n * if it already meets the target.\n */\nexport function findAccessibleColor(baseColor: string, bg: string, targetRatio = 4.5): string {\n if (contrastRatio(baseColor, bg) >= targetRatio) {\n return baseColor;\n }\n\n const c = rgb(baseColor);\n if (c == null) return baseColor;\n\n const bgLum = relativeLuminance(bg);\n const baseLum = relativeLuminance(baseColor);\n\n // Try both directions: prefer the one matching bg luminance, but fall back\n // to the other if the base color is already at the extreme (e.g. white on\n // a medium-luminance background can't be lightened, so darken instead).\n const preferDarken = bgLum > 0.5;\n const directions = preferDarken ? [true, false] : [false, true];\n\n for (const darken of directions) {\n // Skip impossible directions: can't lighten white or darken black.\n if (!darken && baseLum > 0.95) continue;\n if (darken && baseLum < 0.05) continue;\n\n let lo = 0;\n let hi = 1;\n let best: string | null = null;\n\n for (let i = 0; i < 20; i++) {\n const mid = (lo + hi) / 2;\n const adjusted = darken\n ? rgb(c.r * (1 - mid), c.g * (1 - mid), c.b * (1 - mid))\n : rgb(c.r + (255 - c.r) * mid, c.g + (255 - c.g) * mid, c.b + (255 - c.b) * mid);\n\n const hex = adjusted.formatHex();\n const ratio = contrastRatio(hex, bg);\n\n if (ratio >= targetRatio) {\n best = hex;\n hi = mid;\n } else {\n lo = mid;\n }\n }\n\n if (best) return best;\n }\n\n return baseColor;\n}\n","export interface CssTokenDef {\n name: string;\n light: string;\n dark?: string;\n comment?: string;\n consumers?: string;\n section?: string;\n}\n\nexport const CSS_TOKEN_ROOT_SELECTORS: readonly string[] = [\n '.oc-root',\n '.oc-chart-root',\n '.oc-table-wrapper',\n '.oc-table-root',\n '.oc-graph-wrapper',\n '.oc-graph-root',\n '.oc-sankey-root',\n '.oc-tilemap-root',\n '.oc-barlist-root',\n '.oc-map-root',\n];\n\n/**\n * Text levels. Names invert across modes: in light mode \"muted\" sits at\n * a lighter zinc step than \"subtle\" because both are picked relative to\n * the active background, not from a fixed lightness ladder. The intent\n * is \"muted = first step away from primary text\"; \"subtle = next step\n * down\"; etc.\n *\n * Light mode: text=zinc-950, secondary=zinc-700, muted=zinc-500,\n * subtle=zinc-400, faint=zinc-300\n * Dark mode (dark.css): inverts the surface tokens but keeps the same\n * muted -> subtle -> faint progression away from primary.\n */\n\nexport const CSS_TOKENS: readonly CssTokenDef[] = [\n {\n name: '--oc-font-family',\n light:\n '\\n \"Inter Variable\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif',\n },\n {\n name: '--oc-font-mono',\n light: '\"JetBrains Mono\", \"Fira Code\", \"Cascadia Code\", monospace',\n },\n\n {\n section: 'Animation easing presets via CSS linear()',\n name: '--oc-ease-smooth',\n light:\n 'linear(\\n 0,\\n 0.157,\\n 0.438,\\n 0.64,\\n 0.766,\\n 0.85,\\n 0.906,\\n 0.941,\\n 0.964,\\n 0.978,\\n 0.988,\\n 0.994,\\n 0.998,\\n 1\\n )',\n },\n {\n name: '--oc-ease-snappy',\n light:\n 'linear(\\n 0,\\n 0.012,\\n 0.048,\\n 0.108,\\n 0.194,\\n 0.302,\\n 0.426,\\n 0.559,\\n 0.69,\\n 0.808,\\n 0.905,\\n 0.973,\\n 1.013,\\n 1.028,\\n 1.023,\\n 1.006,\\n 0.984,\\n 0.966,\\n 0.957,\\n 0.957,\\n 0.964,\\n 0.975,\\n 0.986,\\n 0.995,\\n 1,\\n 1.003,\\n 1.002,\\n 1,\\n 0.998,\\n 0.998,\\n 0.999,\\n 1\\n )',\n consumers: 'EASE_VAR_MAP in vanilla renderers',\n },\n\n { section: 'Animation timing defaults', name: '--oc-animation-duration', light: '450ms' },\n { name: '--oc-animation-stagger', light: '30ms' },\n { name: '--oc-annotation-delay', light: '150ms' },\n\n { section: 'Typography scale (editorial design system)', name: '--oc-title-size', light: '26px' },\n {\n name: '--oc-title-weight',\n light: '600',\n comment: 'one ladder: 400 regular / 500 medium / 600 semibold / 700 bold',\n },\n { name: '--oc-title-tracking', light: '-0.022em' },\n { name: '--oc-subtitle-size', light: '14px' },\n { name: '--oc-subtitle-weight', light: '400' },\n { name: '--oc-source-size', light: '11px' },\n { name: '--oc-source-weight', light: '400' },\n { name: '--oc-body-size', light: '13px' },\n { name: '--oc-eyebrow-size', light: '11px' },\n { name: '--oc-eyebrow-weight', light: '500' },\n { name: '--oc-eyebrow-tracking', light: '0.08em' },\n\n { section: 'Surfaces (light mode defaults)', name: '--oc-bg', light: '#ffffff', dark: '#09090b' },\n { name: '--oc-card', light: '#ffffff', dark: '#111113' },\n {\n name: '--oc-secondary',\n light: '#f4f4f5',\n dark: '#27272a',\n comment: 'zinc-100, raised surface',\n },\n\n {\n section: `Text levels. Names invert across modes: in light mode \"muted\" sits at\n * a lighter zinc step than \"subtle\" because both are picked relative to\n * the active background, not from a fixed lightness ladder. The intent\n * is \"muted = first step away from primary text\"; \"subtle = next step\n * down\"; etc.\n *\n * Light mode: text=zinc-950, secondary=zinc-700, muted=zinc-500,\n * subtle=zinc-400, faint=zinc-300\n * Dark mode (dark.css): inverts the surface tokens but keeps the same\n * muted -> subtle -> faint progression away from primary.`,\n name: '--oc-text',\n light: '#09090b',\n dark: '#f7f8f8',\n },\n { name: '--oc-text-secondary', light: '#3f3f46', dark: '#d0d6e0' },\n { name: '--oc-text-muted', light: '#71717a', dark: '#a1a1aa' },\n { name: '--oc-text-faint', light: '#d4d4d8', dark: '#52525b' },\n\n {\n section: `Derived neutral ramp. Named by the share of *text* in a text-toward-\n * background mix: 100 is nearly the background, 800 is nearly the text.\n * These static values are the mix against the default surfaces; mounts\n * restamp them from ResolvedTheme.colors.neutral, which a custom warm or\n * cool theme derives from its own text/background pair.`,\n name: '--oc-gray-100',\n light: '#f0f0f0',\n dark: '#171719',\n },\n { name: '--oc-gray-200', light: '#e1e1e2', dark: '#262627' },\n { name: '--oc-gray-300', light: '#c9c9c9', dark: '#3d3e3f' },\n { name: '--oc-gray-400', light: '#9d9d9d', dark: '#68696a' },\n { name: '--oc-gray-600', light: '#787879', dark: '#8c8c8d' },\n { name: '--oc-gray-800', light: '#474748', dark: '#bcbcbd' },\n\n {\n section: 'Lines',\n name: '--oc-gridline',\n light: 'rgba(0, 0, 0, 0.08)',\n dark: 'rgba(255, 255, 255, 0.06)',\n },\n {\n name: '--oc-axis',\n light: 'rgba(0, 0, 0, 0.14)',\n dark: 'rgba(255, 255, 255, 0.14)',\n comment: 'hairline = theme.colors.hairline, not the tick-label ink',\n },\n {\n name: '--oc-border',\n light: 'rgba(0, 0, 0, 0.08)',\n dark: 'rgba(255, 255, 255, 0.1)',\n },\n {\n name: '--oc-border-radius',\n light: '8px',\n comment: 'containers and tooltips; marks use --oc-radius-sm',\n },\n\n { section: 'Radius ladder', name: '--oc-radius-sm', light: '2px' },\n { name: '--oc-radius-md', light: '6px' },\n { name: '--oc-radius-lg', light: 'var(--oc-border-radius)' },\n { name: '--oc-radius-full', light: '999px' },\n\n {\n section: 'Brand and semantic',\n name: '--oc-accent',\n light: '#06b6d4',\n dark: '#06b6d4',\n },\n {\n name: '--oc-accent-strong',\n light: '#0891b2',\n dark: '#06b6d4',\n comment: 'darker cyan for line strokes on light bg',\n consumers: 'stamped via adaptForLightLineStroke',\n },\n {\n name: '--oc-positive',\n light: '#15803d',\n dark: '#4ade80',\n comment: 'kept equal to DEFAULT_THEME.colors.positive (token-theme-parity test)',\n },\n { name: '--oc-negative', light: '#dc2626', dark: '#f87171' },\n {\n name: '--oc-positive-tint',\n light: 'color-mix(in srgb, var(--oc-positive) 10%, var(--oc-bg))',\n comment: 'delta chip background',\n },\n {\n name: '--oc-negative-tint',\n light: 'color-mix(in srgb, var(--oc-negative) 10%, var(--oc-bg))',\n },\n { name: '--oc-focus', light: '#3b82f6', dark: '#60a5fa' },\n {\n name: '--oc-focus-ring',\n light: 'rgba(59, 130, 246, 0.1)',\n comment: 'static, intentionally NOT derived from --oc-focus and NOT flipped in dark mode',\n },\n {\n name: '--oc-focus-ring-strong',\n light: 'rgba(59, 130, 246, 0.25)',\n },\n {\n name: '--oc-editable-hover',\n light: 'rgba(79, 70, 229, 0.35)',\n comment: 'edit-mode hover outline, indigo one-off',\n },\n\n {\n section: 'Spacing scale (4px base) — only --oc-space-2/4 are consumed (JS-stamped)',\n name: '--oc-space-2',\n light: '8px',\n consumers:\n 'stamped at mount from theme.spacing (theme-tokens.ts); component CSS must NOT consume these',\n },\n {\n name: '--oc-space-4',\n light: '16px',\n consumers:\n 'stamped at mount from theme.spacing (theme-tokens.ts); component CSS must NOT consume these',\n },\n\n {\n section: `Hover language. The hovered element goes to full ink; the rest of its\n * group drops to --oc-hover-dim over --oc-hover-duration. Maps override the\n * dim locally (3000 county paths at 0.3 reads as a blackout, not a hint).`,\n name: '--oc-hover-dim',\n light: '0.3',\n },\n { name: '--oc-hover-duration', light: '140ms' },\n { name: '--oc-map-hover-dim', light: '0.75' },\n\n {\n section: 'Interactive states',\n name: '--oc-hover-bg',\n light: 'rgba(0, 0, 0, 0.025)',\n dark: 'rgba(255, 255, 255, 0.05)',\n },\n {\n name: '--oc-tooltip-bg',\n light: 'rgba(255, 255, 255, 0.88)',\n dark: 'rgba(17, 17, 19, 0.92)',\n },\n {\n name: '--oc-tooltip-border',\n light: 'rgba(0, 0, 0, 0.08)',\n dark: 'rgba(255, 255, 255, 0.08)',\n },\n {\n name: '--oc-tooltip-shadow',\n light: '0 2px 8px rgba(0, 0, 0, 0.08), 0 0 1px rgba(0, 0, 0, 0.12)',\n dark: '0 2px 8px rgba(0, 0, 0, 0.3), 0 0 1px rgba(0, 0, 0, 0.4)',\n },\n { name: '--oc-tooltip-text', light: '#09090b', dark: '#f7f8f8' },\n { name: '--oc-legend-text', light: '#3f3f46', dark: '#d0d6e0' },\n];\n\nconst TOKEN_MAP = new Map(CSS_TOKENS.map((t) => [t.name, t]));\n\nexport function cssTokenDefault(name: string, mode: 'light' | 'dark'): string {\n const token = TOKEN_MAP.get(name);\n if (!token) throw new Error(`Unknown CSS token: ${name}`);\n if (mode === 'dark' && token.dark !== undefined) return token.dark;\n return token.light;\n}\n","/**\n * Derived neutral ramp.\n *\n * Every secondary gray in the system is the theme's text color mixed toward\n * its background, rather than a fixed zinc ladder. A custom warm or cool\n * theme therefore gets warm or cool grays for free instead of zinc showing\n * up uninvited beside its palette.\n *\n * The ramp is computed once in `resolveTheme`/`adaptTheme` and carried on\n * `ResolvedTheme.colors.neutral`. Mounts stamp the `--oc-gray-*`,\n * `--oc-text-secondary`, `--oc-text-faint` and `--oc-border` CSS tokens from\n * it. The direction is one-way: theme -> CSS. Engine code reads\n * `theme.colors.neutral.*` and never `cssTokenDefault`.\n */\n\nimport { cssTokenDefault } from '../styles/token-definitions';\nimport { isOpaqueColor } from './contrast';\n\n/**\n * Neutral steps, named by the fraction of *text* in the text/background mix:\n * `100` is nearly the background, `800` is nearly the text.\n */\nexport interface NeutralRamp {\n 100: string;\n 200: string;\n 300: string;\n 400: string;\n 600: string;\n 800: string;\n /** Body-level secondary text. Same value as `800`. */\n secondary: string;\n /** Faintest legible ink (disabled states, empty cells). Same value as `300`. */\n faint: string;\n /** Container border / hairline on non-chart surfaces. Same value as `100`. */\n border: string;\n /**\n * The opaque canvas the theme paints on. Equal to the theme background when\n * that background has a color of its own; a transparent theme resolves to\n * the static `--oc-bg` token for the mode. Knockout rings, stacked-segment\n * seams, table cell fills and graph node rings all read this so they are cut\n * in the same color the surface actually renders as.\n */\n surface: string;\n}\n\n/** Mix weight (share of `text`) for each numbered step. */\nconst STEPS = [\n [100, 0.06],\n [200, 0.12],\n [300, 0.22],\n [400, 0.4],\n [600, 0.55],\n [800, 0.75],\n] as const;\n\n/** Parse `#rgb`/`#rrggbb` to an [r, g, b] triple, or null when unparseable. */\nfunction parseHex(color: string): [number, number, number] | null {\n const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color.trim());\n if (!m) return null;\n const hex =\n m[1].length === 3\n ? m[1]\n .split('')\n .map((c) => c + c)\n .join('')\n : m[1];\n return [\n parseInt(hex.slice(0, 2), 16),\n parseInt(hex.slice(2, 4), 16),\n parseInt(hex.slice(4, 6), 16),\n ];\n}\n\n/** Mix `text` toward `bg` in sRGB; `t` is the share of `text` in the result. */\nfunction mix(text: [number, number, number], bg: [number, number, number], t: number): string {\n const c = (i: number) => Math.round(text[i] * t + bg[i] * (1 - t));\n const hex = (n: number) => Math.max(0, Math.min(255, n)).toString(16).padStart(2, '0');\n return `#${hex(c(0))}${hex(c(1))}${hex(c(2))}`;\n}\n\n/** The ramp the static CSS token defaults carry, for a given mode. */\nfunction staticRamp(mode: 'light' | 'dark'): NeutralRamp {\n return {\n 100: cssTokenDefault('--oc-gray-100', mode),\n 200: cssTokenDefault('--oc-gray-200', mode),\n 300: cssTokenDefault('--oc-gray-300', mode),\n 400: cssTokenDefault('--oc-gray-400', mode),\n 600: cssTokenDefault('--oc-gray-600', mode),\n 800: cssTokenDefault('--oc-gray-800', mode),\n secondary: cssTokenDefault('--oc-text-secondary', mode),\n faint: cssTokenDefault('--oc-text-faint', mode),\n border: cssTokenDefault('--oc-border', mode),\n surface: cssTokenDefault('--oc-bg', mode),\n };\n}\n\n/**\n * Derive the neutral ramp for a text/background pair.\n *\n * Falls back to the static CSS token defaults when the background has no\n * intrinsic color (`transparent`, `none`, alpha-zero) or either color is not\n * a plain hex — the default theme's background is transparent, so this\n * fallback is the common path and is what keeps the theme and the generated\n * `tokens.css` in agreement.\n */\nexport function deriveNeutralRamp(text: string, bg: string, isDark = false): NeutralRamp {\n const mode = isDark ? 'dark' : 'light';\n // The surface is the background itself whenever that background paints\n // something; only a transparent/none background falls back to the token.\n const surface = isOpaqueColor(bg) ? bg : cssTokenDefault('--oc-bg', mode);\n if (!isOpaqueColor(bg)) return staticRamp(mode);\n const t = parseHex(text);\n const b = parseHex(bg);\n if (!t || !b) return { ...staticRamp(mode), surface };\n\n const ramp = Object.fromEntries(STEPS.map(([step, w]) => [step, mix(t, b, w)])) as Pick<\n NeutralRamp,\n 100 | 200 | 300 | 400 | 600 | 800\n >;\n return {\n ...ramp,\n secondary: ramp[800],\n faint: ramp[300],\n border: ramp[100],\n surface,\n };\n}\n","/**\n * Color palettes for @opendata-ai.\n *\n * Categorical palette leads with cyan and uses an OKLCH-balanced multi-hue\n * ramp tuned to L≈0.70, C≈0.13–0.15. Hex values are precomputed sRGB\n * (rather than raw `oklch()` strings) because raw OKLCH strings parse\n * unreliably through d3-color and the dark-mode adapter's hsl-based\n * binary search. Source OKLCH values are documented inline so the ramp\n * can be regenerated if needed.\n *\n * Convention for low-cardinality charts (1 / 2-4 / 5+ series) is available\n * as an opt-in via `theme.seriesStrategy`. The default strategy is 'palette'\n * (full categorical ramp always). The accent-neutral convention is:\n * - 1 series: cyan only\n * - 2-4 series: cyan + zinc-400/500/600 (achromatic)\n * - 5+ series: full multi-hue ramp below\n *\n * Sequential palettes: 5-7 stops from light to dark.\n * Diverging palettes: 7 stops with a neutral midpoint.\n */\n\n// ---------------------------------------------------------------------------\n// Achromatic ramp (zinc-based, dark-mode canonical)\n// ---------------------------------------------------------------------------\n\n/**\n * Achromatic surface and supporting-series ramp. Values are the canonical\n * dark-mode tokens; light-mode equivalents are derived per token in\n * `theme/dark-mode.ts` and the CSS partials, not mirrored here.\n */\nexport const ACHROMATIC_RAMP = {\n fg: '#f7f8f8', // primary text\n fg2: '#d0d6e0', // body text\n fgMuted: '#a1a1aa', // secondary series (zinc-400)\n fgSubtle: '#71717a', // tertiary series (zinc-500)\n fgFaint: '#52525b', // quaternary series (zinc-600)\n secondary: '#27272a', // hover / raised surface\n card: '#111113', // card surface\n bg: '#09090b', // canvas\n} as const;\n\n// ---------------------------------------------------------------------------\n// Categorical\n// ---------------------------------------------------------------------------\n\n/**\n * Quiet ordered editorial categorical palette: six hues, cyan in slot 1.\n *\n * Construction rules (the contract; the hexes below are output, not input):\n * - Hue order cyan 208 -> ochre 70 -> blue 262 -> rose 350 -> green 155 ->\n * violet 300, so every adjacent pair including the wrap is >= 80° apart.\n * - Lightness varies rather than matching: colour-vision deficiency\n * collapses hue and only lightness survives, so a matched-lightness ramp\n * fails the repo's own Brettel simulator on ochre/rose and blue/violet.\n * - Strokes carry more chroma than fills; a fill is the same hue one step\n * lighter and quieter, so a bar or slice never shouts as loud as a line.\n * - Dark variants raise L and trim chroma instead of reusing the light hexes.\n *\n * Hex values are precomputed sRGB via the Ottosson OKLab -> linear sRGB ->\n * sRGB pipeline (raw `oklch()` strings parse unreliably through d3-color and\n * the dark-mode adapter's hsl-based binary search). Each entry carries its\n * *measured* OKLCH so the ramp can be regenerated; do not edit hexes directly.\n *\n * Every array here clears `checkPaletteDistinguishability` at the default\n * minDistance 30 for deuteranopia and protanopia — see\n * `colors/__tests__/palette-a11y.test.ts`, which is the guard on any change.\n */\nexport const CATEGORICAL_PALETTE = [\n '#06b6d4', // cyan — oklch(0.715 0.126 215) — sRGB literal, primary accent\n '#e29e47', // ochre — oklch(0.750 0.130 70)\n '#4170cb', // blue — oklch(0.559 0.150 262)\n '#b2417f', // rose — oklch(0.550 0.161 350)\n '#3ca368', // green — oklch(0.640 0.130 155)\n '#a584dc', // violet — oklch(0.679 0.130 300)\n] as const;\n\n/**\n * Fill counterparts of {@link CATEGORICAL_PALETTE}: same hues, ~0.03 lighter\n * and ~0.03 lower chroma. Used for area-filling marks (bar, area, arc,\n * waffle, calendar, rect) so large blocks of colour read quieter than the\n * strokes of a line chart drawn beside them.\n */\nexport const CATEGORICAL_FILL_PALETTE = [\n '#51bccc', // cyan — oklch(0.739 0.100 209)\n '#dda96b', // ochre — oklch(0.770 0.100 70)\n '#587fc8', // blue — oklch(0.601 0.120 262)\n '#bf6291', // rose — oklch(0.619 0.131 350)\n '#62ab7d', // green — oklch(0.681 0.101 155)\n '#ae96da', // violet — oklch(0.719 0.100 300)\n] as const;\n\n/** Dark-mode stroke palette: L raised ~0.10, chroma trimmed. */\nexport const CATEGORICAL_PALETTE_DARK = [\n '#22d3ee', // cyan — oklch(0.797 0.134 212) — sRGB literal\n '#f9be78', // ochre — oklch(0.840 0.111 70)\n '#6591e1', // blue — oklch(0.660 0.129 262)\n '#d771a4', // rose — oklch(0.680 0.140 350)\n '#6ebf8c', // green — oklch(0.739 0.109 155)\n '#bca1ed', // violet — oklch(0.760 0.110 300)\n] as const;\n\n/** Dark-mode fill palette: the dark strokes one step quieter. */\nexport const CATEGORICAL_FILL_PALETTE_DARK = [\n '#72cedc', // cyan — oklch(0.800 0.090 208)\n '#f3c898', // ochre — oklch(0.859 0.079 70)\n '#7698d6', // blue — oklch(0.679 0.100 262)\n '#d281a8', // rose — oklch(0.700 0.111 350)\n '#82ba95', // green — oklch(0.740 0.079 155)\n '#c2afe7', // violet — oklch(0.789 0.081 300)\n] as const;\n\n/**\n * Slots 7-12. Only reached past slot 6, and reaching it emits a warning:\n * a chart with more than six categorical series is a chart that should be\n * bucketed, faceted, or direct-labelled instead.\n *\n * Built by interleaving: each base hue + 40° (which lands each entry near the\n * midpoint of a gap in the base ramp) with lightness alternating ±0.12 from\n * its base slot. The array is rotated so slot 7 sits 105° from cyan and never\n * neighbours it on a seven-slice pie wrap; the two entries that do land near\n * cyan in hue are pushed to slots 10 and 12.\n */\nexport const CATEGORICAL_EXTENDED_PALETTE = [\n '#8e8f19', // olive — oklch(0.629 0.130 110)\n '#b086eb', // periwinkle— oklch(0.701 0.149 302)\n '#ac3225', // brick — oklch(0.501 0.161 30)\n '#00bfc0', // teal — oklch(0.729 0.124 195)\n '#a5538c', // mulberry — oklch(0.560 0.129 340)\n '#65b1f6', // sky — oklch(0.740 0.126 248)\n] as const;\n\n/**\n * Measured OKLCH hue (deg) of each {@link CATEGORICAL_PALETTE} slot, in slot\n * order. Exported so the adjacency test can assert the ordering rule without\n * re-deriving OKLCH from the hexes.\n */\nexport const CATEGORICAL_HUES = [215.2, 70.0, 262.1, 349.7, 154.8, 300.2] as const;\n\n/** Measured OKLCH lightness of each {@link CATEGORICAL_PALETTE} slot. */\nexport const CATEGORICAL_LIGHTNESS = [0.715, 0.75, 0.559, 0.55, 0.64, 0.679] as const;\n\nexport type CategoricalPalette = typeof CATEGORICAL_PALETTE;\n\n// ---------------------------------------------------------------------------\n// Sequential\n// ---------------------------------------------------------------------------\n\n/** Sequential palette definition: an array of color stops from light to dark. */\nexport interface SequentialPalette {\n readonly name: string;\n readonly stops: readonly string[];\n}\n\nexport const SEQUENTIAL_BLUE: SequentialPalette = {\n name: 'blue',\n stops: ['#deebf7', '#c6dbef', '#9ecae1', '#6baed6', '#3182bd', '#08519c'],\n} as const;\n\nexport const SEQUENTIAL_GREEN: SequentialPalette = {\n name: 'green',\n stops: ['#e5f5e0', '#c7e9c0', '#a1d99b', '#74c476', '#31a354', '#006d2c'],\n} as const;\n\nexport const SEQUENTIAL_ORANGE: SequentialPalette = {\n name: 'orange',\n stops: ['#fee6ce', '#fdd0a2', '#fdae6b', '#fd8d3c', '#e6550d', '#a63603'],\n} as const;\n\nexport const SEQUENTIAL_PURPLE: SequentialPalette = {\n name: 'purple',\n stops: ['#efedf5', '#dadaeb', '#bcbddc', '#9e9ac8', '#756bb1', '#54278f'],\n} as const;\n\nexport const SEQUENTIAL_TEAL: SequentialPalette = {\n name: 'teal',\n stops: ['#06b6d4', '#05a3be', '#0490a8', '#037d92', '#026a7c', '#015766', '#004450'],\n} as const;\n\n/** All sequential palettes keyed by name. */\nexport const SEQUENTIAL_PALETTES: Record<string, string[]> = {\n blue: [...SEQUENTIAL_BLUE.stops],\n green: [...SEQUENTIAL_GREEN.stops],\n orange: [...SEQUENTIAL_ORANGE.stops],\n purple: [...SEQUENTIAL_PURPLE.stops],\n teal: [...SEQUENTIAL_TEAL.stops],\n};\n\n// ---------------------------------------------------------------------------\n// Diverging\n// ---------------------------------------------------------------------------\n\n/** Diverging palette definition: an array of color stops with a neutral midpoint. */\nexport interface DivergingPalette {\n readonly name: string;\n readonly stops: readonly string[];\n}\n\nexport const DIVERGING_RED_BLUE: DivergingPalette = {\n name: 'redBlue',\n stops: [\n '#b2182b', // strong red\n '#d6604d', // medium red\n '#f4a582', // light red\n '#f7f7f7', // neutral\n '#92c5de', // light blue\n '#4393c3', // medium blue\n '#2166ac', // strong blue\n ],\n} as const;\n\nexport const DIVERGING_BROWN_TEAL: DivergingPalette = {\n name: 'brownTeal',\n stops: [\n '#8c510a', // strong brown\n '#bf812d', // medium brown\n '#dfc27d', // light brown\n '#f6e8c3', // neutral\n '#80cdc1', // light teal\n '#35978f', // medium teal\n '#01665e', // strong teal\n ],\n} as const;\n\n/** All diverging palettes keyed by name. */\nexport const DIVERGING_PALETTES: Record<string, string[]> = {\n redBlue: [...DIVERGING_RED_BLUE.stops],\n brownTeal: [...DIVERGING_BROWN_TEAL.stops],\n};\n\n// ---------------------------------------------------------------------------\n// Scheme name resolution (spec-level `scale: { scheme }` support)\n// ---------------------------------------------------------------------------\n\n/**\n * Vega-Lite scheme-name aliases mapped onto the openchart palettes.\n * Keys are lowercase. Only schemes with a faithful local equivalent are\n * aliased; unknown names fail validation with the supported list rather\n * than silently substituting an unrelated ramp.\n */\nconst VL_SCHEME_ALIASES: Record<string, string> = {\n blues: 'blue',\n greens: 'green',\n oranges: 'orange',\n purples: 'purple',\n teals: 'teal',\n redblue: 'redBlue',\n brownteal: 'brownTeal',\n // ColorBrewer BrBG, which brownTeal mirrors\n brownbluegreen: 'brownTeal',\n category10: 'categorical',\n tableau10: 'categorical',\n};\n\n/**\n * Resolve a scheme name to its palette stops. Accepts openchart palette names\n * (blue, green, orange, purple, teal, redBlue, brownTeal, categorical) and\n * common Vega-Lite aliases (blues, greens, ..., redblue, category10).\n * Case-insensitive. Returns undefined for unknown names.\n */\nexport function resolveSchemeName(name: string): string[] | undefined {\n const canonical =\n SEQUENTIAL_PALETTES[name] || DIVERGING_PALETTES[name]\n ? name\n : (VL_SCHEME_ALIASES[name.toLowerCase()] ?? name.toLowerCase());\n if (canonical === 'categorical') return [...CATEGORICAL_PALETTE];\n return SEQUENTIAL_PALETTES[canonical] ?? DIVERGING_PALETTES[canonical];\n}\n\n/** Scheme names accepted by `scale: { scheme }`, for validation messages. */\nexport const SUPPORTED_SCHEME_NAMES: readonly string[] = [\n ...Object.keys(SEQUENTIAL_PALETTES),\n ...Object.keys(DIVERGING_PALETTES),\n 'categorical',\n ...Object.keys(VL_SCHEME_ALIASES),\n];\n","/**\n * Dark mode theme adaptation.\n *\n * Preserves hue of colors while adjusting lightness and saturation\n * to maintain the same relative contrast ratios on a dark background.\n */\n\nimport { hsl, rgb } from 'd3-color';\nimport { contrastRatio, isOpaqueColor } from '../colors/contrast';\nimport { deriveNeutralRamp } from '../colors/neutral';\nimport {\n ACHROMATIC_RAMP,\n CATEGORICAL_FILL_PALETTE_DARK,\n CATEGORICAL_PALETTE_DARK,\n} from '../colors/palettes';\nimport type { ResolvedTheme } from '../types/theme';\nimport { DEFAULT_THEME } from './defaults';\n\n// ---------------------------------------------------------------------------\n// Dark mode background\n// ---------------------------------------------------------------------------\n\n/** Default dark mode background color (zinc-based canvas). */\nconst DARK_BG = ACHROMATIC_RAMP.bg;\n/** Default dark mode text color. */\nconst DARK_TEXT = ACHROMATIC_RAMP.fg;\n\n// ---------------------------------------------------------------------------\n// Color adaptation\n// ---------------------------------------------------------------------------\n\n/**\n * Adapt a single color for dark mode.\n *\n * Preserves the hue and adjusts lightness/saturation so the adapted\n * color has the same contrast ratio against darkBg as the original\n * had against lightBg.\n */\nexport function adaptColorForDarkMode(color: string, lightBg: string, darkBg: string): string {\n // Adapter only handles hex/rgb-style inputs. Raw oklch() and other\n // CSS Color 4 strings parse unreliably through d3-color in happy-dom,\n // so guard early instead of silently falling back to a default.\n if (rgb(color) == null) {\n if (typeof console !== 'undefined' && console.warn) {\n console.warn(\n `[openchart] adaptColorForDarkMode: unparseable color \"${color}\", returning unchanged. Use precomputed sRGB hex.`,\n );\n }\n return color;\n }\n const originalRatio = contrastRatio(color, lightBg);\n const c = hsl(color);\n if (c == null || Number.isNaN(c.h)) {\n // Achromatic or invalid color: just adjust lightness\n const r = rgb(color);\n if (r == null) return color;\n const darkBgLum = _luminanceFromHex(darkBg);\n const isLight = darkBgLum < 0.5;\n if (isLight) return color;\n // Invert the lightness\n const inverted = hsl(color);\n if (inverted == null) return color;\n inverted.l = 1 - inverted.l;\n return inverted.formatHex();\n }\n\n // Binary search for lightness that gives equivalent contrast on dark bg\n let lo = 0.0;\n let hi = 1.0;\n let bestColor = color;\n let bestDiff = Infinity;\n\n for (let i = 0; i < 20; i++) {\n const mid = (lo + hi) / 2;\n const candidate = hsl(c.h, c.s, mid);\n const hex = candidate.formatHex();\n const ratio = contrastRatio(hex, darkBg);\n const diff = Math.abs(ratio - originalRatio);\n\n if (diff < bestDiff) {\n bestDiff = diff;\n bestColor = hex;\n }\n\n if (ratio < originalRatio) {\n // Need more contrast = more lightness on dark bg\n lo = mid;\n } else {\n hi = mid;\n }\n }\n\n return bestColor;\n}\n\n/** Quick luminance estimation from a hex color. */\nfunction _luminanceFromHex(color: string): number {\n const c = rgb(color);\n if (c == null) return 0;\n return (0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b) / 255;\n}\n\n// ---------------------------------------------------------------------------\n// Full theme adaptation\n// ---------------------------------------------------------------------------\n\n/**\n * Adapt an entire resolved theme for dark mode.\n *\n * Swaps background/text, adapts categorical and annotation colors,\n * adjusts gridline and axis colors for the dark background.\n */\nexport function adaptTheme(theme: ResolvedTheme): ResolvedTheme {\n const pairs = theme._tokenPairs;\n const inputBg = theme.colors.background;\n // A non-opaque background ('transparent', 'none', alpha-zero rgba) preserves\n // the background token but must still adopt dark text/gridline/axis colors —\n // the container is dark, not the chart canvas.\n const isTransparent = !isOpaqueColor(inputBg);\n const alreadyDark = isTransparent || _luminanceFromHex(inputBg) < 0.2;\n\n // Preserve user-supplied background (including transparent) but always\n // apply dark-mode text/axis/gridline colors so labels are readable on the\n // dark host surface.\n const darkBg = pairs?.['colors.background']\n ? pairs['colors.background'].dark\n : alreadyDark\n ? inputBg\n : DARK_BG;\n const darkMuted = ACHROMATIC_RAMP.fgMuted;\n\n // Per the \"sensible defaults, full override\" convention (see spec-grammar),\n // dark-mode adaptation only swaps in dark defaults for colors the spec\n // left at their light-mode default. An explicit override is preserved.\n // When a TokenValue pair has an explicit dark value, use it directly.\n const light = DEFAULT_THEME.colors;\n const lightChrome = DEFAULT_THEME.chrome;\n const overridden = <T>(current: T, lightDefault: T, darkDefault: T): T =>\n current === lightDefault ? darkDefault : current;\n\n const darkText = pairs?.['colors.text']\n ? pairs['colors.text'].dark\n : overridden(theme.colors.text, light.text, DARK_TEXT);\n const darkGridline = pairs?.['colors.gridline']\n ? pairs['colors.gridline'].dark\n : overridden(theme.colors.gridline, light.gridline, 'rgba(255,255,255,0.06)');\n const darkHairline = pairs?.['colors.hairline']\n ? pairs['colors.hairline'].dark\n : overridden(theme.colors.hairline, light.hairline, 'rgba(255,255,255,0.14)');\n // axis is also tick-label fill — needs WCAG AA contrast on dark bg.\n // Zinc-400 (`#a1a1aa`) hits ~6:1 against #09090b.\n const darkAxis = pairs?.['colors.axis']\n ? pairs['colors.axis'].dark\n : overridden(theme.colors.axis, light.axis, '#a1a1aa');\n\n // The categorical palette has purpose-built dark variants (L raised ~0.10,\n // chroma trimmed) rather than reusing the light hexes or running them\n // through contrast-equivalence, which dulls cyan into teal. A palette the\n // spec overrode passes through untouched, same rule as every other color.\n const categorical = overridden(theme.colors.categorical, light.categorical, [\n ...CATEGORICAL_PALETTE_DARK,\n ]);\n const categoricalFill = overridden(theme.colors.categoricalFill, light.categoricalFill, [\n ...CATEGORICAL_FILL_PALETTE_DARK,\n ]);\n\n return {\n ...theme,\n isDark: true,\n colors: {\n ...theme.colors,\n background: darkBg,\n text: darkText,\n gridline: darkGridline,\n axis: darkAxis,\n annotationFill: pairs?.['colors.annotationFill']\n ? pairs['colors.annotationFill'].dark\n : overridden(theme.colors.annotationFill, light.annotationFill, 'rgba(255,255,255,0.06)'),\n annotationText: pairs?.['colors.annotationText']\n ? pairs['colors.annotationText'].dark\n : overridden(theme.colors.annotationText, light.annotationText, darkMuted),\n categorical,\n categoricalFill,\n hairline: darkHairline,\n positive: pairs?.['colors.positive']\n ? pairs['colors.positive'].dark\n : overridden(theme.colors.positive, light.positive, '#4ade80'),\n negative: pairs?.['colors.negative']\n ? pairs['colors.negative'].dark\n : overridden(theme.colors.negative, light.negative, '#f87171'),\n neutral: deriveNeutralRamp(darkText, darkBg, true),\n },\n chrome: {\n // Eyebrow keeps its accent tint (cyan in both modes) unless the user\n // gave an explicit dark value; the other chrome elements desaturate\n // to a muted gray on the dark canvas. Each color only adapts if the\n // spec left it at the light default, so explicit chrome color\n // overrides survive dark-mode adaptation. When a TokenValue pair has\n // an explicit dark value, use it directly.\n eyebrow: pairs?.['chrome.eyebrow.color']\n ? { ...theme.chrome.eyebrow, color: pairs['chrome.eyebrow.color'].dark }\n : theme.chrome.eyebrow,\n title: {\n ...theme.chrome.title,\n color: pairs?.['chrome.title.color']\n ? pairs['chrome.title.color'].dark\n : overridden(theme.chrome.title.color, lightChrome.title.color, darkText),\n },\n subtitle: {\n ...theme.chrome.subtitle,\n color: pairs?.['chrome.subtitle.color']\n ? pairs['chrome.subtitle.color'].dark\n : overridden(theme.chrome.subtitle.color, lightChrome.subtitle.color, darkMuted),\n },\n source: {\n ...theme.chrome.source,\n color: pairs?.['chrome.source.color']\n ? pairs['chrome.source.color'].dark\n : overridden(theme.chrome.source.color, lightChrome.source.color, darkMuted),\n },\n byline: {\n ...theme.chrome.byline,\n color: pairs?.['chrome.byline.color']\n ? pairs['chrome.byline.color'].dark\n : overridden(theme.chrome.byline.color, lightChrome.byline.color, darkMuted),\n },\n footer: {\n ...theme.chrome.footer,\n color: pairs?.['chrome.footer.color']\n ? pairs['chrome.footer.color'].dark\n : overridden(theme.chrome.footer.color, lightChrome.footer.color, darkMuted),\n },\n },\n // The editorial rule is a brand device: it keeps its authored color unless\n // the theme gave an explicit dark half of the pair.\n rule:\n theme.rule && pairs?.['rule.color']\n ? { ...theme.rule, color: pairs['rule.color'].dark }\n : theme.rule,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Light mode line stroke darkening\n// ---------------------------------------------------------------------------\n\n/**\n * Returns a darker variant of a color for use as a foreground stroke on\n * light backgrounds, where mid-lightness palette colors (e.g. cyan-500\n * `#06b6d4`) lack contrast against white. Drops HSL lightness by ~12%\n * absolute (clamped to >= 0) while preserving hue and saturation, which\n * reproduces the cyan-500 → cyan-600 step the cyan accent originally\n * needed and works for any other palette accent the user picks.\n *\n * Returns the input unchanged when:\n * - the color is already dark enough (L <= ~0.40) — further darkening\n * just muddies the stroke without adding contrast\n * - the color isn't parseable\n * - the color is achromatic (NaN hue) — HSL lightness on grays drifts\n * toward black instead of staying neutral\n *\n * Exposed via the `--oc-accent-strong` CSS token.\n */\nexport function adaptForLightLineStroke(color: string): string {\n if (rgb(color) == null) return color;\n const c = hsl(color);\n if (c == null) return color;\n // Achromatic check: NaN hue OR low saturation. d3-color reports a valid\n // hue for grays whose RGB channels aren't perfectly equal (e.g. zinc\n // `#a1a1aa` has s≈0.05), so saturation is the more reliable signal.\n // Threshold 0.10 catches near-grays without tripping on real palette\n // colors (palette saturation is ≥ ~0.5).\n if (Number.isNaN(c.h) || c.s < 0.1) return color;\n if (c.l <= 0.4) return color;\n c.l = Math.max(0, c.l - 0.12);\n return c.formatHex();\n}\n","/**\n * Text measurement for environments without a DOM.\n *\n * Uses per-character advance-width tables measured from Inter (the default\n * chart font) for ~2-3% accuracy on Latin text at weights 400/700.\n * Other fonts set via theme.fonts.family still get a better-than-flat-ratio\n * approximation but no accuracy guarantee.\n *\n * Tables generated by scripts/generate-char-widths.mjs — regenerate, don't\n * hand-edit.\n */\n\nimport type { MeasureTextFn } from '../types/layout';\n\n// ---------------------------------------------------------------------------\n// Per-character advance widths for Inter, normalized to fontSize = 1.\n// Generated by scripts/generate-char-widths.mjs from Chromium canvas metrics.\n// ---------------------------------------------------------------------------\n\nconst CHAR_WIDTHS_400: Record<string, number> = {\n ' ': 0.278,\n '!': 0.278,\n '\"': 0.355,\n '#': 0.556,\n $: 0.556,\n '%': 0.889,\n '&': 0.667,\n \"'\": 0.191,\n '(': 0.333,\n ')': 0.333,\n '*': 0.389,\n '+': 0.584,\n ',': 0.278,\n '-': 0.333,\n '.': 0.278,\n '/': 0.278,\n '0': 0.556,\n '1': 0.556,\n '2': 0.556,\n '3': 0.556,\n '4': 0.556,\n '5': 0.556,\n '6': 0.556,\n '7': 0.556,\n '8': 0.556,\n '9': 0.556,\n ':': 0.278,\n ';': 0.278,\n '<': 0.584,\n '=': 0.584,\n '>': 0.584,\n '?': 0.556,\n '@': 1.015,\n A: 0.667,\n B: 0.667,\n C: 0.722,\n D: 0.722,\n E: 0.667,\n F: 0.611,\n G: 0.778,\n H: 0.722,\n I: 0.278,\n J: 0.5,\n K: 0.667,\n L: 0.556,\n M: 0.833,\n N: 0.722,\n O: 0.778,\n P: 0.667,\n Q: 0.778,\n R: 0.722,\n S: 0.667,\n T: 0.611,\n U: 0.722,\n V: 0.667,\n W: 0.944,\n X: 0.667,\n Y: 0.667,\n Z: 0.611,\n '[': 0.278,\n '\\\\': 0.278,\n ']': 0.278,\n '^': 0.469,\n _: 0.556,\n '`': 0.333,\n a: 0.556,\n b: 0.556,\n c: 0.5,\n d: 0.556,\n e: 0.556,\n f: 0.278,\n g: 0.556,\n h: 0.556,\n i: 0.222,\n j: 0.222,\n k: 0.5,\n l: 0.222,\n m: 0.833,\n n: 0.556,\n o: 0.556,\n p: 0.556,\n q: 0.556,\n r: 0.333,\n s: 0.5,\n t: 0.278,\n u: 0.556,\n v: 0.5,\n w: 0.722,\n x: 0.5,\n y: 0.5,\n z: 0.5,\n '{': 0.334,\n '|': 0.26,\n '}': 0.334,\n '~': 0.584,\n '₹': 0.52,\n '₩': 1.022,\n '¥': 0.556,\n '¥': 1,\n '€': 0.744,\n '£': 0.556,\n '¢': 0.556,\n '§': 0.556,\n '©': 0.737,\n '®': 0.737,\n '™': 1,\n '°': 0.4,\n '–': 0.556,\n '—': 1,\n '‘': 0.222,\n '’': 0.222,\n '“': 0.333,\n '”': 0.333,\n '…': 1,\n '±': 0.549,\n '×': 0.584,\n '÷': 0.549,\n};\n\nconst AVG_CHAR_WIDTH_400 = 0.53;\n\nconst CHAR_WIDTHS_700: Record<string, number> = {\n ' ': 0.278,\n '!': 0.333,\n '\"': 0.474,\n '#': 0.556,\n $: 0.556,\n '%': 0.889,\n '&': 0.722,\n \"'\": 0.238,\n '(': 0.333,\n ')': 0.333,\n '*': 0.389,\n '+': 0.584,\n ',': 0.278,\n '-': 0.333,\n '.': 0.278,\n '/': 0.278,\n '0': 0.556,\n '1': 0.556,\n '2': 0.556,\n '3': 0.556,\n '4': 0.556,\n '5': 0.556,\n '6': 0.556,\n '7': 0.556,\n '8': 0.556,\n '9': 0.556,\n ':': 0.333,\n ';': 0.333,\n '<': 0.584,\n '=': 0.584,\n '>': 0.584,\n '?': 0.611,\n '@': 0.975,\n A: 0.722,\n B: 0.722,\n C: 0.722,\n D: 0.722,\n E: 0.667,\n F: 0.611,\n G: 0.778,\n H: 0.722,\n I: 0.278,\n J: 0.556,\n K: 0.722,\n L: 0.611,\n M: 0.833,\n N: 0.722,\n O: 0.778,\n P: 0.667,\n Q: 0.778,\n R: 0.722,\n S: 0.667,\n T: 0.611,\n U: 0.722,\n V: 0.667,\n W: 0.944,\n X: 0.667,\n Y: 0.667,\n Z: 0.611,\n '[': 0.333,\n '\\\\': 0.278,\n ']': 0.333,\n '^': 0.584,\n _: 0.556,\n '`': 0.333,\n a: 0.556,\n b: 0.611,\n c: 0.556,\n d: 0.611,\n e: 0.556,\n f: 0.333,\n g: 0.611,\n h: 0.611,\n i: 0.278,\n j: 0.278,\n k: 0.556,\n l: 0.278,\n m: 0.889,\n n: 0.611,\n o: 0.611,\n p: 0.611,\n q: 0.611,\n r: 0.389,\n s: 0.556,\n t: 0.333,\n u: 0.611,\n v: 0.556,\n w: 0.778,\n x: 0.556,\n y: 0.556,\n z: 0.5,\n '{': 0.389,\n '|': 0.28,\n '}': 0.389,\n '~': 0.584,\n '₹': 0.553,\n '₩': 1.024,\n '¥': 0.556,\n '¥': 1,\n '€': 0.744,\n '£': 0.556,\n '¢': 0.556,\n '§': 0.556,\n '©': 0.737,\n '®': 0.737,\n '™': 1,\n '°': 0.4,\n '–': 0.556,\n '—': 1,\n '‘': 0.278,\n '’': 0.278,\n '“': 0.5,\n '”': 0.5,\n '…': 1,\n '±': 0.549,\n '×': 0.584,\n '÷': 0.549,\n};\n\nconst AVG_CHAR_WIDTH_700 = 0.554;\n\n// Weights < 650 scale the CHAR_WIDTHS_400 table; weights >= 650 scale\n// CHAR_WIDTHS_700. The factor is 1.0 at each table's native weight.\nconst WEIGHT_ADJUSTMENT: Record<number, number> = {\n 100: 0.9,\n 200: 0.92,\n 300: 0.95,\n 400: 1.0,\n 500: 1.02,\n 600: 1.05,\n 700: 1.0, // uses CHAR_WIDTHS_700 directly\n 800: 1.1,\n 900: 1.12,\n};\n\ninterface ResolvedTable {\n table: Record<string, number>;\n avg: number;\n scale: number;\n}\n\n// Precomputed per-weight results so resolveTable never allocates. Covers the\n// explicit WEIGHT_ADJUSTMENT entries plus a default (scale 1.0) fallback for\n// any other weight, bucketed the same way resolveTable's branch always was.\nconst RESOLVED_TABLE_400_DEFAULT: ResolvedTable = {\n table: CHAR_WIDTHS_400,\n avg: AVG_CHAR_WIDTH_400,\n scale: 1.0,\n};\nconst RESOLVED_TABLE_700_DEFAULT: ResolvedTable = {\n table: CHAR_WIDTHS_700,\n avg: AVG_CHAR_WIDTH_700,\n scale: 1.0,\n};\n\nconst RESOLVED_TABLE_CACHE = new Map<number, ResolvedTable>();\nfor (const [weightStr, scale] of Object.entries(WEIGHT_ADJUSTMENT)) {\n const weight = Number(weightStr);\n RESOLVED_TABLE_CACHE.set(\n weight,\n weight >= 650\n ? { table: CHAR_WIDTHS_700, avg: AVG_CHAR_WIDTH_700, scale }\n : { table: CHAR_WIDTHS_400, avg: AVG_CHAR_WIDTH_400, scale },\n );\n}\n\nfunction resolveTable(fontWeight: number): ResolvedTable {\n const cached = RESOLVED_TABLE_CACHE.get(fontWeight);\n if (cached) return cached;\n return fontWeight >= 650 ? RESOLVED_TABLE_700_DEFAULT : RESOLVED_TABLE_400_DEFAULT;\n}\n\nfunction charWidthRatio(ch: string, table: Record<string, number>, avg: number): number {\n const direct = table[ch];\n if (direct !== undefined) return direct;\n const code = ch.codePointAt(0) ?? 0;\n // Combining marks (diacritics) have zero advance width\n if ((code >= 0x0300 && code <= 0x036f) || (code >= 0x1ab0 && code <= 0x1aff)) return 0;\n // CJK ideographs, kana, hangul, and fullwidth forms are full-em\n if (\n (code >= 0x1100 && code <= 0x11ff) ||\n (code >= 0x2e80 && code <= 0x9fff) ||\n (code >= 0xac00 && code <= 0xd7af) ||\n (code >= 0xf900 && code <= 0xfaff) ||\n (code >= 0xff00 && code <= 0xff60)\n )\n return 1.0;\n return avg;\n}\n\n/**\n * Estimate the average character width for a given font size and weight.\n * Used by both text width estimation and word-wrap line counting.\n */\nexport function estimateCharWidth(fontSize: number, fontWeight = 400): number {\n const { avg, scale } = resolveTable(fontWeight);\n return fontSize * avg * scale;\n}\n\nfunction measureLine(\n line: string,\n table: Record<string, number>,\n avg: number,\n scale: number,\n fontSize: number,\n): number {\n let sum = 0;\n for (const ch of line) {\n sum += charWidthRatio(ch, table, avg);\n }\n return sum * fontSize * scale;\n}\n\n/**\n * Estimate the rendered width of a text string.\n *\n * Uses per-character advance widths measured from Inter (the default chart\n * font). Accurate to ~2-3% for Latin text at weights 400/700; other weights\n * are interpolated from the nearest table.\n */\nexport function estimateTextWidth(text: string, fontSize: number, fontWeight = 400): number {\n const { table, avg, scale } = resolveTable(fontWeight);\n\n if (text.includes('\\n')) {\n let maxWidth = 0;\n for (const line of text.split('\\n')) {\n const w = measureLine(line, table, avg, scale, fontSize);\n if (w > maxWidth) maxWidth = w;\n }\n return maxWidth;\n }\n return measureLine(text, table, avg, scale, fontSize);\n}\n\n/**\n * Width reserved for the \"OpenData\" brand watermark in the bottom-right corner.\n */\nexport const BRAND_RESERVE_WIDTH = 130;\n\n/** Font size of the brand watermark (px). Shared between layout and renderer. */\nexport const BRAND_FONT_SIZE = 12;\n\n/** Minimum chart width to render the brand watermark (px). */\nexport const BRAND_MIN_WIDTH = 120;\n\n/**\n * Width threshold below which layout tightens spacing (legend gaps, chrome\n * padding, label gaps) to maximize data area on small screens.\n */\nexport const COMPACT_WIDTH = 420;\n\n/**\n * Estimate the rendered height of a text block.\n */\nexport function estimateTextHeight(fontSize: number, lineCount = 1, lineHeight = 1.3): number {\n return fontSize * lineHeight * lineCount;\n}\n\n/**\n * Distance from the top of a text box to its alphabetic baseline.\n *\n * Layout stores text positions as top-edge coordinates; renderers convert to\n * baseline coordinates with this ratio instead of `dominant-baseline: hanging`.\n * WebKit computes the hanging baseline from different font metrics than Blink\n * and never inherits dominant-baseline into tspans (WebKit bugs 139258 /\n * 297455), which shifted whole titles above the container on iOS Safari.\n * The 0.8 ratio matches Blink's rendered hanging position for typical UI\n * fonts (hanging sits at ~0.8 x ascent, Inter ascent ~= 0.97em) and the\n * Highcharts fontMetrics convention.\n */\nexport function textAscent(fontSize: number): number {\n return fontSize * 0.8;\n}\n\n// ---------------------------------------------------------------------------\n// Measurer resolver — bridges heuristic ↔ injected canvas measurer\n// ---------------------------------------------------------------------------\n\nexport const heuristicMeasure: MeasureTextFn = (text, fontSize, fontWeight) => ({\n width: estimateTextWidth(text, fontSize, fontWeight),\n height: estimateTextHeight(fontSize),\n});\n\nexport function resolveMeasurer(measureText?: MeasureTextFn): MeasureTextFn {\n return measureText ?? heuristicMeasure;\n}\n","/**\n * Responsive breakpoints and layout strategies.\n *\n * Width breakpoints:\n * - compact: < 400px (mobile, small embeds)\n * - medium: 400-700px (tablet, sidebars)\n * - full: > 700px (desktop, full-width)\n *\n * Height classes:\n * - minimal: < 100px (sparklines, chrome-less thumbnails)\n * - cramped: 100-199px (dashboard widgets with a compact title)\n * - short: 200-350px (embedded panels, short containers)\n * - normal: > 350px (standard containers)\n */\n\n// ---------------------------------------------------------------------------\n// Width breakpoint type and detection\n// ---------------------------------------------------------------------------\n\n/** Responsive breakpoint based on container width. */\nexport type Breakpoint = 'compact' | 'medium' | 'full';\n\n/** Breakpoint thresholds in pixels. */\nexport const BREAKPOINT_COMPACT_MAX = 400;\nexport const BREAKPOINT_MEDIUM_MAX = 700;\n\n/**\n * Determine the breakpoint for a given container width.\n */\nexport function getBreakpoint(width: number): Breakpoint {\n if (width < BREAKPOINT_COMPACT_MAX) return 'compact';\n if (width <= BREAKPOINT_MEDIUM_MAX) return 'medium';\n return 'full';\n}\n\n// ---------------------------------------------------------------------------\n// Height class type and detection\n// ---------------------------------------------------------------------------\n\n/** Height classification based on container height. */\nexport type HeightClass = 'minimal' | 'cramped' | 'short' | 'normal';\n\n/** Height class thresholds in pixels. */\nexport const HEIGHT_MINIMAL_MAX = 100;\nexport const HEIGHT_CRAMPED_MAX = 200;\nexport const HEIGHT_SHORT_MAX = 350;\n\n/**\n * Determine the height class for a given container height.\n */\nexport function getHeightClass(height: number): HeightClass {\n if (height < HEIGHT_MINIMAL_MAX) return 'minimal';\n if (height < HEIGHT_CRAMPED_MAX) return 'cramped';\n if (height <= HEIGHT_SHORT_MAX) return 'short';\n return 'normal';\n}\n\n// ---------------------------------------------------------------------------\n// Layout strategy\n// ---------------------------------------------------------------------------\n\n/** Label display mode at a given breakpoint. */\nexport type LabelMode = 'all' | 'important' | 'none';\n\n/**\n * Legend position at a given breakpoint. `'top-left'` is a map-only overlay\n * position: the legend floats inside the map area's top-left corner (with its\n * own backdrop) instead of reserving a block of figure height. Charts treat\n * it as the default position.\n */\nexport type LegendPosition = 'top' | 'top-left' | 'right' | 'bottom' | 'bottom-right' | 'inline';\n\n/** Annotation position strategy. */\nexport type AnnotationPosition = 'inline' | 'tooltip-only';\n\n/** Axis label density (controls tick count reduction). */\nexport type AxisLabelDensity = 'full' | 'reduced' | 'minimal';\n\n/** Chrome display mode based on available height. */\nexport type ChromeMode = 'full' | 'compact' | 'hidden';\n\n/**\n * Layout strategy defining how the visualization adapts to available space.\n * Returned by getLayoutStrategy() based on width breakpoint and height class.\n */\nexport interface LayoutStrategy {\n /** How data labels are displayed. */\n labelMode: LabelMode;\n /** Where the legend is positioned. */\n legendPosition: LegendPosition;\n /** How annotations are displayed. */\n annotationPosition: AnnotationPosition;\n /** Axis tick density. */\n axisLabelDensity: AxisLabelDensity;\n /** Chrome display mode: full, compact (title only), or hidden. */\n chromeMode: ChromeMode;\n /** Max fraction of container height for legend (0-1). -1 means unlimited. */\n legendMaxHeight: number;\n}\n\n/**\n * Get the base layout strategy for a width breakpoint.\n */\nfunction getWidthStrategy(breakpoint: Breakpoint): LayoutStrategy {\n switch (breakpoint) {\n case 'compact':\n return {\n labelMode: 'none',\n legendPosition: 'top',\n annotationPosition: 'tooltip-only',\n axisLabelDensity: 'minimal',\n chromeMode: 'full',\n legendMaxHeight: -1,\n };\n case 'medium':\n return {\n labelMode: 'important',\n legendPosition: 'top',\n annotationPosition: 'inline',\n axisLabelDensity: 'reduced',\n chromeMode: 'full',\n legendMaxHeight: -1,\n };\n case 'full':\n return {\n labelMode: 'all',\n legendPosition: 'top',\n annotationPosition: 'inline',\n axisLabelDensity: 'full',\n chromeMode: 'full',\n legendMaxHeight: -1,\n };\n }\n}\n\n/**\n * Apply height constraints to a width-based strategy.\n * Short containers compress chrome and cap legend height.\n * Minimal containers hide chrome and labels entirely. Cramped containers\n * (100-199px) keep the same reduced layout but show a compact title: a\n * titled dashboard tile in that range is unusable without its label.\n */\nfunction applyHeightConstraints(\n strategy: LayoutStrategy,\n heightClass: HeightClass,\n): LayoutStrategy {\n if (heightClass === 'normal') return strategy;\n\n if (heightClass === 'minimal' || heightClass === 'cramped') {\n return {\n ...strategy,\n chromeMode: heightClass === 'minimal' ? 'hidden' : 'compact',\n legendMaxHeight: 0,\n labelMode: 'none',\n annotationPosition: 'tooltip-only',\n };\n }\n\n // short\n return {\n ...strategy,\n chromeMode: 'compact',\n legendMaxHeight: 0.15,\n };\n}\n\n/**\n * Get the layout strategy for a given breakpoint and height class.\n *\n * Compact: minimal chrome, no inline labels, legend on top, reduced axes.\n * Medium: moderate labels, legend on top, reduced axes.\n * Full: all labels, legend on top, full axes.\n *\n * Legend position is 'top' at every breakpoint: a top legend reads as a key to\n * the chart before the eye reaches the plot, and it keeps the plotting area\n * full-width instead of surrendering a right-hand column. `legend.position`\n * still overrides it per spec.\n *\n * Height constraints further reduce chrome and legend when container is short.\n */\nexport function getLayoutStrategy(\n breakpoint: Breakpoint,\n heightClass: HeightClass = 'normal',\n): LayoutStrategy {\n const base = getWidthStrategy(breakpoint);\n return applyHeightConstraints(base, heightClass);\n}\n\n// ---------------------------------------------------------------------------\n// Chrome economy\n// ---------------------------------------------------------------------------\n\n/**\n * Height below which gridlines are dropped. A 140px dashboard tile has room\n * for the shape of the series and nothing else; five hairlines behind it are\n * decoration, not structure.\n */\nexport const HEIGHT_NO_GRID_MAX = 150;\n\n/** Tick-label budget for the x-axis at the compact breakpoint. */\nexport const COMPACT_MAX_X_TICKS = 3;\n\n/**\n * How much chart chrome a container can afford.\n *\n * Small embeds get the same treatment an editorial designer would give them:\n * drop the grid first, then the axes, and keep only the tick labels that\n * anchor the reader (first, last, and one in between). Every drop is a\n * default — an explicit `axis.grid` / `encoding.*.axis` on the spec wins.\n */\nexport interface ChromeEconomy {\n /** Draw gridlines by default? False under `HEIGHT_NO_GRID_MAX`. */\n gridlines: boolean;\n /** Draw axes by default? False under `HEIGHT_CRAMPED_MAX`. */\n axes: boolean;\n /** Cap on drawn x-axis tick labels, or undefined for no cap. */\n maxXTicks?: number;\n}\n\n/**\n * Resolve the chrome economy for a container size.\n *\n * @param width - Container width in pixels.\n * @param height - Container height in pixels.\n */\nexport function resolveChromeEconomy(width: number, height: number): ChromeEconomy {\n const economy: ChromeEconomy = {\n gridlines: height >= HEIGHT_NO_GRID_MAX,\n axes: height >= HEIGHT_CRAMPED_MAX,\n };\n if (getBreakpoint(width) === 'compact') economy.maxXTicks = COMPACT_MAX_X_TICKS;\n return economy;\n}\n","/**\n * Pixel-level responsive metrics for chart layout.\n *\n * These are the numeric tuning values that correspond to the semantic decisions\n * in LayoutStrategy. Centralizing them here ensures the engine and renderer\n * always agree on the same thresholds and offsets without duplicating raw numbers.\n *\n * LayoutStrategy is semantic (what to show), this module is metric (how much space).\n */\n\nimport { estimateTextWidth } from '../layout/text-measure';\nimport { BREAKPOINT_COMPACT_MAX } from './breakpoints';\n\n// ---------------------------------------------------------------------------\n// Y-axis title offsets\n// ---------------------------------------------------------------------------\n\n/** Distance from the chart edge to the rotated y-axis title center (compact viewports). */\nexport const AXIS_TITLE_OFFSET_COMPACT = 35;\n\n/** Distance from the chart edge to the rotated y-axis title center (standard viewports). */\nexport const AXIS_TITLE_OFFSET_DEFAULT = 45;\n\n/** Returns the y-axis title offset appropriate for the given container width. */\nexport function getAxisTitleOffset(width: number): number {\n return width < BREAKPOINT_COMPACT_MAX ? AXIS_TITLE_OFFSET_COMPACT : AXIS_TITLE_OFFSET_DEFAULT;\n}\n\n// ---------------------------------------------------------------------------\n// Horizontal padding\n// ---------------------------------------------------------------------------\n\n/**\n * On compact viewports axis titles and tick labels tolerate closer container edges\n * than chrome text (title, subtitle). Reduce horizontal padding to reclaim space.\n */\nexport const HPAD_COMPACT_FRACTION = 0.5;\n\n/** Minimum horizontal padding regardless of scaling. */\nexport const HPAD_COMPACT_MIN = 4;\n\n// ---------------------------------------------------------------------------\n// Tick label offset\n// ---------------------------------------------------------------------------\n\n/**\n * Horizontal gap between the chart edge and the near edge of y-axis tick labels.\n * The engine uses this to reserve right-axis margin; the renderer uses it to position labels.\n * Both must agree on this value — do not change one without the other.\n */\nexport const TICK_LABEL_OFFSET = 6;\n\n// ---------------------------------------------------------------------------\n// Axis title trailing padding\n// ---------------------------------------------------------------------------\n\n/**\n * Extra padding beyond half-glyph height added to the rotated axis title margin on\n * standard (non-compact) viewports. Omitted on compact viewports to save space.\n */\nexport const AXIS_TITLE_TRAILING_PAD = 4;\n\n// ---------------------------------------------------------------------------\n// Axis title gap\n// ---------------------------------------------------------------------------\n\n/**\n * Visible breathing room between the widest tick label's far edge and the\n * rotated y-axis title's near edge.\n *\n * The title is rotated -90deg and anchored at its center, so its glyph box\n * extends half the title font size toward the tick labels. To keep the gap\n * constant regardless of title font size, callers add that half-glyph on top\n * of this value (see axisTitleOffset() below). At the old fixed gap of 14 the\n * clearance silently shrank as the font grew (14 - 11 = 3px at body size 21),\n * which let large-font titles collide with the tick labels.\n *\n * Used in both the engine (margin reservation) and the renderer\n * (axes.ts title placement) via axisTitleOffset(). Both must agree on this value.\n */\nexport const AXIS_TITLE_GAP = 3;\n\n/**\n * Computes the distance from the chart edge to the rotated y-axis title's center.\n *\n * The title center must sit far enough left that, after accounting for the\n * widest tick label and the title's own half-glyph height, AXIS_TITLE_GAP of\n * visible clearance remains. Falls back to the viewport-minimum offset when the\n * dynamic value would be smaller (short tick labels on wide containers).\n *\n * When `inline` is true the y-tick labels render inside the chart area (above\n * their gridlines) rather than in a left gutter, so the title only needs to\n * clear the chart edge — no tick-label width and no viewport-minimum floor,\n * both of which would otherwise leave a dead gap between the title and the plot.\n *\n * Shared by the engine (margin reservation) and the renderer (title placement)\n * so the reserved space always matches where the title is drawn.\n */\nexport function axisTitleOffset(\n maxTickLabelWidth: number,\n titleFontSize: number,\n width: number,\n inline = false,\n): number {\n const halfTitleGlyph = Math.ceil(titleFontSize / 2);\n if (inline) {\n // No gutter tick labels to clear: title edge + gap + half-glyph only.\n return TICK_LABEL_OFFSET + AXIS_TITLE_GAP + halfTitleGlyph;\n }\n const dynamic = TICK_LABEL_OFFSET + maxTickLabelWidth + AXIS_TITLE_GAP + halfTitleGlyph;\n return Math.max(dynamic, getAxisTitleOffset(width));\n}\n\n// ---------------------------------------------------------------------------\n// Narrow viewport threshold (between compact and medium)\n// ---------------------------------------------------------------------------\n\n/**\n * Width below which \"narrow\" adjustments apply: extra iOS Safari top padding and\n * tighter category label gaps. Sits between compact (< 400) and medium (400–700).\n * Not a semantic breakpoint — a layout heuristic for narrow-but-not-compact containers.\n */\nexport const NARROW_VIEWPORT_MAX = 500;\n\n// ---------------------------------------------------------------------------\n// Top padding (iOS Safari address bar / notch clearance)\n// ---------------------------------------------------------------------------\n\n/** Extra top padding added below NARROW_VIEWPORT_MAX to clear iOS Safari browser chrome. */\nexport const TOP_PAD_EXTRA_NARROW = 10;\n\n/** @deprecated Use NARROW_VIEWPORT_MAX instead. */\nexport const TOP_PAD_NARROW_MAX = NARROW_VIEWPORT_MAX;\n\n// ---------------------------------------------------------------------------\n// Category label gaps (left-axis bar/dot charts)\n// ---------------------------------------------------------------------------\n\n/** Gap between category label text and chart edge on narrow viewports (< NARROW_VIEWPORT_MAX). */\nexport const LABEL_GAP_COMPACT = 8;\n\n/** Gap between category label text and chart edge on standard viewports. */\nexport const LABEL_GAP_DEFAULT = 12;\n\n/** @deprecated Use NARROW_VIEWPORT_MAX instead. */\nexport const LABEL_GAP_NARROW_MAX = NARROW_VIEWPORT_MAX;\n\n// ---------------------------------------------------------------------------\n// Max left-axis label fraction\n// ---------------------------------------------------------------------------\n\n/**\n * Width threshold for the medium left-label fraction cap.\n * Sits between medium (400-700) and full (> 700) to prevent wide category labels\n * from consuming too much horizontal space at mid-range widths.\n */\nexport const MAX_LEFT_LABEL_FRACTION_MEDIUM_MAX = 600;\n\n/** Max fraction of container width reservable for left category labels (compact). */\nexport const MAX_LEFT_LABEL_FRACTION_COMPACT = 0.45;\n\n/** Max fraction of container width reservable for left category labels (mid-range). */\nexport const MAX_LEFT_LABEL_FRACTION_MEDIUM = 0.55;\n\n/** Max fraction of container width reservable for left category labels (standard). */\nexport const MAX_LEFT_LABEL_FRACTION_DEFAULT = 1;\n\n// ---------------------------------------------------------------------------\n// X-axis extent (height below chart area)\n// ---------------------------------------------------------------------------\n\nexport const X_AXIS_BAND_HEIGHT = 26;\nexport const X_AXIS_TITLE_BAND = 22;\nexport const X_AXIS_TITLE_BAND_ROTATED = 20;\n\n/**\n * Ceiling on the vertical extent reserved for rotated x tick labels. The angle\n * ladder (engine layout/axes/rotation) avoids -90° for labels longer than this.\n *\n * The cap bounds the RESERVATION, so a label whose rotated projection exceeds it\n * must be truncated to fit — see `truncateRotatedLabel`. Reserving a capped band\n * while drawing the full string is what let long ticks spill past the axis and\n * collide with the source line.\n */\nexport const X_AXIS_ROTATED_EXTENT_CAP = 120;\n\n/**\n * Line-height multiplier for a tick label's text ribbon. Shared by the\n * rotated-extent reservation below and the engine's rotation collision test\n * (layout/axes/rotation) so the reserved space and the fit threshold can't\n * drift apart.\n */\nexport const TICK_LINE_HEIGHT_FACTOR = 1.2;\n\n/**\n * Gap between the axis line and the top of the rotated label band. Mirrors the\n * tick-mark-to-label offset used for flat labels; keeps the reservation matched\n * to the drawn footprint across Blink and WebKit, whose rotated-glyph metrics\n * differ by a couple of pixels. Shared by the extent reservation and the\n * truncation budget so they stay in lockstep.\n */\nconst AXIS_TO_LABEL_GAP = 3;\n\nexport interface XAxisExtentInput {\n labels: string[];\n tickAngle?: number;\n hasTitle: boolean;\n tickFontSize: number;\n tickFontWeight: number;\n xAxisHeight?: number;\n measure?: (text: string, fontSize: number, fontWeight: number) => number;\n}\n\nexport function computeXAxisExtentFromLabels(input: XAxisExtentInput): number {\n const measure = input.measure ?? estimateTextWidth;\n const baseHeight = input.xAxisHeight ?? X_AXIS_BAND_HEIGHT;\n\n if (input.tickAngle && Math.abs(input.tickAngle) > 10) {\n const angleRad = Math.abs(input.tickAngle) * (Math.PI / 180);\n let maxLabelWidth = 40;\n for (const label of input.labels) {\n const w = measure(label, input.tickFontSize, input.tickFontWeight);\n if (w > maxLabelWidth) maxLabelWidth = w;\n }\n // Vertical extent of a rotated label = the projection of its bounding box\n // onto the vertical axis: textWidth*|sin θ| (the tilted length) plus\n // lineHeight*|cos θ| (the label's own height, which still contributes\n // unless the text is fully vertical). Measuring only the sin term\n // under-reserved space, letting rotated ticks spill into the source line.\n const lineHeight = input.tickFontSize * TICK_LINE_HEIGHT_FACTOR;\n const rotatedHeight = Math.min(\n maxLabelWidth * Math.sin(angleRad) + lineHeight * Math.cos(angleRad) + AXIS_TO_LABEL_GAP,\n X_AXIS_ROTATED_EXTENT_CAP,\n );\n return input.hasTitle ? rotatedHeight + X_AXIS_TITLE_BAND_ROTATED : rotatedHeight;\n }\n\n const tickExtent = 4 + input.tickFontSize + Math.max(11, Math.ceil(input.tickFontSize * 0.7));\n const flatHeight = Math.max(baseHeight, tickExtent);\n return input.hasTitle ? flatHeight + X_AXIS_TITLE_BAND : flatHeight;\n}\n\n/**\n * The widest a rotated tick label may be before its vertical projection exceeds\n * `X_AXIS_ROTATED_EXTENT_CAP`.\n *\n * Inverts the rotated-extent formula in `computeXAxisExtentFromLabels`:\n *\n * extent = width*sin(θ) + lineHeight*cos(θ) + gap\n * width = (cap - lineHeight*cos(θ) - gap) / sin(θ)\n *\n * Deriving it from the same terms (rather than hardcoding a second number) keeps\n * the truncation budget and the space reservation locked together — if one\n * changes, the other follows.\n */\nexport function maxRotatedLabelWidth(tickAngle: number, tickFontSize: number): number {\n const angleRad = Math.abs(tickAngle) * (Math.PI / 180);\n const sin = Math.sin(angleRad);\n // A flat (or near-flat) label has no vertical projection to bound.\n if (sin <= 0.001) return Number.POSITIVE_INFINITY;\n const lineHeight = tickFontSize * TICK_LINE_HEIGHT_FACTOR;\n const usable = X_AXIS_ROTATED_EXTENT_CAP - lineHeight * Math.cos(angleRad) - AXIS_TO_LABEL_GAP;\n return Math.max(0, usable / sin);\n}\n\n/**\n * Truncate a label with an ellipsis so it fits inside an explicit pixel budget.\n * Returns the label unchanged when it already fits.\n *\n * Binary-search on character count against the real measure function, so it\n * respects proportional fonts rather than assuming a fixed character width.\n */\nexport function truncateToWidth(\n label: string,\n budget: number,\n fontSize: number,\n fontWeight: number,\n measure: (text: string, fontSize: number, fontWeight?: number) => number = estimateTextWidth,\n): string {\n if (!Number.isFinite(budget)) return label;\n if (measure(label, fontSize, fontWeight) <= budget) return label;\n\n const ELLIPSIS = '…';\n // Nothing sensible fits: keep a single character + ellipsis rather than\n // returning an empty label.\n let lo = 0;\n let hi = label.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n const candidate = label.slice(0, mid) + ELLIPSIS;\n if (measure(candidate, fontSize, fontWeight) <= budget) {\n lo = mid;\n } else {\n hi = mid - 1;\n }\n }\n return lo > 0 ? label.slice(0, lo).trimEnd() + ELLIPSIS : ELLIPSIS;\n}\n\n/**\n * Truncate a rotated tick label with an ellipsis so it fits inside the capped\n * reservation. Returns the label unchanged when it already fits.\n *\n * Delegates to `truncateToWidth` with the budget derived from the rotation\n * geometry, so the binary-search stays in one place.\n */\nexport function truncateRotatedLabel(\n label: string,\n tickAngle: number,\n tickFontSize: number,\n tickFontWeight: number,\n measure: (text: string, fontSize: number, fontWeight?: number) => number = estimateTextWidth,\n): string {\n const budget = maxRotatedLabelWidth(tickAngle, tickFontSize);\n return truncateToWidth(label, budget, tickFontSize, tickFontWeight, measure);\n}\n","/**\n * Default theme definition.\n *\n * Editorial design system with a cyan-led six-hue categorical palette, Inter\n * Variable typography (weights 400/500/600/700), and zinc-based achromatic\n * surfaces. Light is the default surface; dark adaptation lives in\n * dark-mode.ts.\n *\n * resolveTheme() deep-merges user overrides onto this base.\n */\n\nimport {\n CATEGORICAL_FILL_PALETTE,\n CATEGORICAL_PALETTE,\n DIVERGING_PALETTES,\n SEQUENTIAL_PALETTES,\n} from '../colors/palettes';\nimport { X_AXIS_BAND_HEIGHT } from '../responsive/metrics';\nimport type { Theme } from '../types/theme';\n\n/**\n * The default theme. All fields are required and fully specified.\n */\nexport const DEFAULT_THEME: Theme = {\n colors: {\n categorical: [...CATEGORICAL_PALETTE],\n categoricalFill: [...CATEGORICAL_FILL_PALETTE],\n sequential: SEQUENTIAL_PALETTES,\n diverging: DIVERGING_PALETTES,\n background: 'transparent',\n text: '#09090b',\n gridline: 'rgba(0,0,0,0.08)',\n // Structural hairline: axis lines, separators. Deliberately heavier than\n // the gridline and lighter than any ink, so structure reads as structure.\n hairline: 'rgba(0,0,0,0.14)',\n // Used for axis lines/ticks AND axis tick label fill. Must clear WCAG AA\n // contrast (4.5:1) on white because tick labels are rendered with this\n // color. Zinc-500 hits ~5.7:1.\n axis: '#71717a',\n annotationFill: 'rgba(0,0,0,0.04)',\n annotationText: '#71717a',\n positive: '#15803d',\n negative: '#dc2626',\n },\n fonts: {\n family:\n '\"Inter Variable\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif',\n mono: '\"JetBrains Mono\", \"Fira Code\", \"Cascadia Code\", monospace',\n sizes: {\n title: 26,\n subtitle: 14,\n body: 13,\n small: 11,\n axisTick: 11,\n metricLabel: 11,\n metricValue: 22,\n },\n // One ladder everywhere: 400 regular / 500 medium (eyebrow, labels, UI,\n // values) / 600 semibold (title, series names, annotation lede) / 700 bold.\n weights: {\n normal: 400,\n medium: 500,\n semibold: 600,\n bold: 700,\n },\n },\n spacing: {\n padding: 20,\n chromeGap: 6,\n chromeToChart: 8,\n chartToFooter: 8,\n axisMargin: 6,\n xAxisHeight: X_AXIS_BAND_HEIGHT,\n // Gap (px) between the x-axis line and the TOP of the tick labels. The\n // renderer treats this as a literal top gap and then shifts the label down\n // by textAscent() to land it on the alphabetic baseline (rather than\n // dominant-baseline:hanging, which WebKit positions from different metrics),\n // so the gap holds regardless of font size.\n xAxisLabelPadding: 4,\n },\n /** Containers and tooltips. Marks carry their own 2px radius constant. */\n borderRadius: 8,\n chrome: {\n eyebrow: {\n fontSize: 11,\n fontWeight: 500,\n color: '#06b6d4',\n lineHeight: 1.4,\n },\n title: {\n fontSize: 26,\n fontWeight: 600,\n color: '#09090b',\n lineHeight: 1.15,\n },\n subtitle: {\n fontSize: 14,\n fontWeight: 400,\n color: '#71717a',\n lineHeight: 1.45,\n },\n source: {\n fontSize: 11,\n fontWeight: 400,\n color: '#71717a',\n lineHeight: 1.4,\n },\n byline: {\n fontSize: 11,\n fontWeight: 400,\n color: '#71717a',\n lineHeight: 1.4,\n },\n footer: {\n fontSize: 11,\n fontWeight: 400,\n color: '#71717a',\n lineHeight: 1.4,\n },\n },\n /** No editorial rule by default; the `broadsheet` preset sets one. */\n rule: null,\n seriesStrategy: 'palette',\n};\n","/**\n * Named theme presets built on the design token system.\n *\n * Each preset is a ThemeConfig object that can be passed to createChart()\n * or the React/Svelte/Vue Chart component. The engine deep-merges them\n * onto DEFAULT_THEME, so only the deltas are specified here.\n */\n\nimport { CATEGORICAL_FILL_PALETTE_DARK, CATEGORICAL_PALETTE_DARK } from '../colors/palettes';\nimport type { ThemeConfig } from '../types/spec';\n\n/** Editorial preset: the current default look. Exported for explicit selection. */\nexport const editorial: ThemeConfig = {};\n\n/** Essay preset: serif display titles, warmer background, generous spacing. */\nexport const essay: ThemeConfig = {\n colors: {\n background: { light: '#faf8f5', dark: '#1a1816' },\n text: { light: '#1c1917', dark: '#e7e5e4' },\n gridline: { light: 'rgba(0,0,0,0.08)', dark: 'rgba(255,255,255,0.06)' },\n axis: { light: '#78716c', dark: '#a8a29e' },\n },\n fonts: {\n family: '\"Source Serif 4\", \"Source Serif Pro\", Georgia, \"Times New Roman\", serif',\n sizes: { title: 28, subtitle: 15, body: 14, small: 12 },\n weights: { normal: 400, medium: 500, semibold: 600, bold: 700 },\n },\n spacing: {\n padding: 24,\n chromeGap: 6,\n chromeToChart: 12,\n chartToFooter: 12,\n },\n chrome: {\n title: { fontWeight: 600, lineHeight: 1.2 },\n subtitle: { fontWeight: 400, lineHeight: 1.5 },\n },\n borderRadius: 0,\n};\n\n/** Wire preset: dense, mono numerals, tighter chrome. Dashboard/agency feel. */\nexport const wire: ThemeConfig = {\n colors: {\n background: { light: '#ffffff', dark: '#0a0a0a' },\n text: { light: '#171717', dark: '#ededed' },\n gridline: { light: 'rgba(0,0,0,0.06)', dark: 'rgba(255,255,255,0.06)' },\n axis: { light: '#737373', dark: '#a3a3a3' },\n },\n fonts: {\n family: '\"JetBrains Mono\", \"Fira Code\", \"Cascadia Code\", monospace',\n sizes: { title: 16, subtitle: 12, body: 11, small: 10, axisTick: 10 },\n weights: { normal: 400, medium: 500, semibold: 600, bold: 700 },\n },\n spacing: {\n padding: 12,\n chromeGap: 2,\n chromeToChart: 4,\n chartToFooter: 4,\n },\n chrome: {\n title: { fontWeight: 600, lineHeight: 1.25 },\n subtitle: { fontWeight: 400, lineHeight: 1.3 },\n },\n borderRadius: 2,\n};\n\n/**\n * Broadsheet preset: newspaper editorial. Warm paper, a red masthead rule and\n * eyebrow, a bold 24px title, and a quiet palette built around one loud hue.\n *\n * Palette (measured OKLCH; checked with the repo's Brettel CVD simulator —\n * deuteranopia and protanopia both pass, nearest simulated pair 48.7 against\n * a threshold of 30):\n * 1 #1d5f8a oklch(0.466 0.095 242) ink blue\n * 2 #e3120b oklch(0.580 0.232 29) house red — the one loud hue\n * 3 #7fb0d3 oklch(0.735 0.073 240) pale blue\n * 4 #e0a63b oklch(0.762 0.137 79) ochre\n * 5 #3a8a6e oklch(0.576 0.090 167) green\n * 6 #8c8680 oklch(0.623 0.011 68) warm gray\n * Fills are the same hues one step lighter (L +0.06, chroma -12%).\n */\nexport const broadsheet: ThemeConfig = {\n colors: {\n categorical: ['#1d5f8a', '#e3120b', '#7fb0d3', '#e0a63b', '#3a8a6e', '#8c8680'],\n categoricalFill: ['#3b7097', '#ee493a', '#97c2e2', '#efbb64', '#579a81', '#9e9893'],\n background: { light: '#fffdf9', dark: '#171513' },\n text: { light: '#1a1714', dark: '#f4f1ea' },\n gridline: { light: 'rgba(26,23,20,0.08)', dark: 'rgba(244,241,234,0.07)' },\n // Heavier than the default 0.14: newsprint rules are drawn to be seen.\n hairline: { light: 'rgba(26,23,20,0.28)', dark: 'rgba(244,241,234,0.28)' },\n axis: { light: '#6b645c', dark: '#a8a099' },\n annotationFill: { light: 'rgba(26,23,20,0.05)', dark: 'rgba(244,241,234,0.07)' },\n annotationText: { light: '#6b645c', dark: '#a8a099' },\n },\n fonts: {\n sizes: { title: 24, subtitle: 14, body: 13, small: 11 },\n },\n spacing: {\n padding: 20,\n chromeGap: 8,\n chromeToChart: 16,\n chartToFooter: 12,\n },\n chrome: {\n eyebrow: { color: { light: '#e3120b', dark: '#f4463f' }, fontSize: 11, fontWeight: 700 },\n title: { fontSize: 24, fontWeight: 700, lineHeight: 1.18 },\n subtitle: { fontWeight: 400, lineHeight: 1.45 },\n },\n /** The masthead device: a short red bar above the eyebrow. */\n rule: { color: { light: '#e3120b', dark: '#f4463f' }, width: 40, thickness: 3 },\n borderRadius: 0,\n};\n\n/**\n * Terminal preset: a dense dark product surface. Dark in BOTH modes — the\n * resolver detects the dark background and adapts chrome text without\n * `darkMode` being set — so it carries the dark palette variants explicitly\n * rather than waiting for adaptTheme().\n *\n * `seriesStrategy: 'accent-neutral'` keeps one cyan accent against neutral\n * siblings, the register a monitoring UI wants.\n */\nexport const terminal: ThemeConfig = {\n colors: {\n categorical: [...CATEGORICAL_PALETTE_DARK],\n categoricalFill: [...CATEGORICAL_FILL_PALETTE_DARK],\n background: { light: '#0b0f14', dark: '#0b0f14' },\n text: { light: '#e6edf3', dark: '#e6edf3' },\n gridline: { light: 'rgba(230,237,243,0.06)', dark: 'rgba(230,237,243,0.06)' },\n hairline: { light: 'rgba(230,237,243,0.14)', dark: 'rgba(230,237,243,0.14)' },\n // 6.25:1 on #0b0f14 — tick labels are drawn in this color too.\n axis: { light: '#8b949e', dark: '#8b949e' },\n annotationFill: { light: 'rgba(230,237,243,0.06)', dark: 'rgba(230,237,243,0.06)' },\n annotationText: { light: '#8b949e', dark: '#8b949e' },\n positive: { light: '#4ade80', dark: '#4ade80' },\n negative: { light: '#f87171', dark: '#f87171' },\n },\n fonts: {\n family:\n '\"Inter Variable\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif',\n mono: '\"JetBrains Mono\", \"Fira Code\", \"Cascadia Code\", monospace',\n sizes: { title: 15, subtitle: 12, body: 12, small: 10, axisTick: 10 },\n },\n spacing: {\n padding: 12,\n chromeGap: 3,\n chromeToChart: 6,\n chartToFooter: 6,\n },\n chrome: {\n eyebrow: { color: { light: '#22d3ee', dark: '#22d3ee' }, fontSize: 10, fontWeight: 500 },\n title: { fontSize: 15, fontWeight: 600, lineHeight: 1.3 },\n subtitle: { fontWeight: 400, lineHeight: 1.4 },\n },\n borderRadius: 4,\n seriesStrategy: 'accent-neutral',\n};\n","/**\n * Theme resolver: deep-merges user overrides onto default theme.\n *\n * Produces a ResolvedTheme where every property is guaranteed to have\n * a value. The engine uses ResolvedTheme internally so it never needs\n * null checks on theme properties.\n *\n * Auto-detects dark backgrounds and adapts chrome text colors so\n * custom themes with dark canvases are readable without explicitly\n * enabling darkMode.\n */\n\nimport { isOpaqueColor } from '../colors/contrast';\nimport { deriveNeutralRamp } from '../colors/neutral';\nimport type { ChromeThemeOverride, ThemeConfig } from '../types/spec';\nimport type { ChromeDefaults, ResolvedTheme, Theme, TokenValue } from '../types/theme';\nimport { DEFAULT_THEME } from './defaults';\n\n/**\n * Deep merge source into target, creating a new object.\n * Only merges plain objects; arrays and primitives replace directly.\n */\n// biome-ignore lint/suspicious/noExplicitAny: recursive object merge requires dynamic typing\nfunction deepMerge(target: any, source: any): any {\n const result = { ...target };\n\n for (const key of Object.keys(source)) {\n const sourceVal = source[key];\n const targetVal = target[key];\n\n if (\n sourceVal !== undefined &&\n sourceVal !== null &&\n typeof sourceVal === 'object' &&\n !Array.isArray(sourceVal) &&\n typeof targetVal === 'object' &&\n targetVal !== null &&\n !Array.isArray(targetVal)\n ) {\n result[key] = deepMerge(targetVal, sourceVal);\n } else if (sourceVal !== undefined) {\n result[key] = sourceVal;\n }\n }\n\n return result;\n}\n\n/** Extract the light-mode string from a TokenValue. */\nfunction resolveTokenLight(v: TokenValue): string {\n return typeof v === 'string' ? v : v.light;\n}\n\n/**\n * Convert a user-facing ThemeConfig (partial) into the full Theme shape\n * that deepMerge can work with.\n */\nfunction themeConfigToPartial(config: ThemeConfig): Partial<Theme> {\n const partial: Partial<Theme> = {};\n\n if (config.colors) {\n const colors: Partial<Theme['colors']> = {};\n // Shorthand: flat string[] is treated as categorical palette\n if (Array.isArray(config.colors)) {\n colors.categorical = config.colors;\n } else {\n if (config.colors.categorical) colors.categorical = config.colors.categorical;\n if (config.colors.categoricalFill) colors.categoricalFill = config.colors.categoricalFill;\n if (config.colors.sequential) colors.sequential = config.colors.sequential;\n if (config.colors.diverging) colors.diverging = config.colors.diverging;\n if (config.colors.background) colors.background = resolveTokenLight(config.colors.background);\n if (config.colors.text) colors.text = resolveTokenLight(config.colors.text);\n if (config.colors.gridline) colors.gridline = resolveTokenLight(config.colors.gridline);\n if (config.colors.hairline) colors.hairline = resolveTokenLight(config.colors.hairline);\n if (config.colors.axis) colors.axis = resolveTokenLight(config.colors.axis);\n if (config.colors.annotationFill)\n colors.annotationFill = resolveTokenLight(config.colors.annotationFill);\n if (config.colors.annotationText)\n colors.annotationText = resolveTokenLight(config.colors.annotationText);\n if (config.colors.positive) colors.positive = resolveTokenLight(config.colors.positive);\n if (config.colors.negative) colors.negative = resolveTokenLight(config.colors.negative);\n }\n // A user who sets only `categorical` gets fills from the same array. The\n // two palettes must never drift apart behind the author's back: a custom\n // six-hue palette shouldn't leave bars on the built-in fills.\n if (colors.categorical && !colors.categoricalFill) {\n colors.categoricalFill = colors.categorical;\n }\n partial.colors = colors as Theme['colors'];\n }\n\n if (config.fonts) {\n const fonts: Partial<Theme['fonts']> = {};\n if (config.fonts.family) fonts.family = config.fonts.family;\n if (config.fonts.mono) fonts.mono = config.fonts.mono;\n if (config.fonts.sizes) fonts.sizes = config.fonts.sizes as Theme['fonts']['sizes'];\n if (config.fonts.weights) fonts.weights = config.fonts.weights as Theme['fonts']['weights'];\n partial.fonts = fonts as Theme['fonts'];\n }\n\n if (config.spacing) {\n const spacing: Partial<Theme['spacing']> = {};\n if (config.spacing.padding !== undefined) spacing.padding = config.spacing.padding;\n if (config.spacing.chromeGap !== undefined) spacing.chromeGap = config.spacing.chromeGap;\n if (config.spacing.chromeToChart !== undefined)\n spacing.chromeToChart = config.spacing.chromeToChart;\n if (config.spacing.chartToFooter !== undefined)\n spacing.chartToFooter = config.spacing.chartToFooter;\n if (config.spacing.axisMargin !== undefined) spacing.axisMargin = config.spacing.axisMargin;\n if (config.spacing.xAxisHeight !== undefined) spacing.xAxisHeight = config.spacing.xAxisHeight;\n if (config.spacing.xAxisLabelPadding !== undefined)\n spacing.xAxisLabelPadding = config.spacing.xAxisLabelPadding;\n partial.spacing = spacing as Theme['spacing'];\n }\n\n if (config.borderRadius !== undefined) {\n partial.borderRadius = config.borderRadius;\n }\n\n if (config.chrome) {\n const chrome: Record<string, Partial<ChromeDefaults>> = {};\n for (const [element, value] of Object.entries(config.chrome)) {\n if (value === undefined) continue;\n if (typeof value === 'string') {\n chrome[element] = { color: value };\n } else {\n const override: Partial<ChromeDefaults> = {};\n const obj = value as ChromeThemeOverride;\n if (obj.color !== undefined) override.color = resolveTokenLight(obj.color);\n if (obj.fontSize !== undefined) override.fontSize = obj.fontSize;\n if (obj.fontWeight !== undefined) override.fontWeight = obj.fontWeight;\n if (obj.lineHeight !== undefined) override.lineHeight = obj.lineHeight;\n chrome[element] = override;\n }\n }\n partial.chrome = chrome as unknown as Theme['chrome'];\n }\n\n if (config.rule) {\n partial.rule = {\n color: resolveTokenLight(config.rule.color),\n width: config.rule.width,\n thickness: config.rule.thickness,\n };\n }\n\n if (config.seriesStrategy) {\n partial.seriesStrategy = config.seriesStrategy;\n }\n\n return partial;\n}\n\n/**\n * Collect TokenValue pairs from the user's ThemeConfig so adaptTheme()\n * can use explicit dark values instead of algorithmic adaptation.\n */\nfunction collectTokenPairs(\n config: ThemeConfig,\n): Record<string, { light: string; dark: string }> | undefined {\n const pairs: Record<string, { light: string; dark: string }> = {};\n if (config.colors && !Array.isArray(config.colors)) {\n const colorFields = [\n 'background',\n 'text',\n 'gridline',\n 'hairline',\n 'axis',\n 'annotationFill',\n 'annotationText',\n 'positive',\n 'negative',\n ] as const;\n for (const field of colorFields) {\n const v = config.colors[field];\n if (v && typeof v === 'object' && 'light' in v) {\n pairs[`colors.${field}`] = v;\n }\n }\n }\n if (config.rule?.color && typeof config.rule.color === 'object' && 'light' in config.rule.color) {\n pairs['rule.color'] = config.rule.color;\n }\n if (config.chrome) {\n for (const [element, value] of Object.entries(config.chrome)) {\n if (\n value &&\n typeof value === 'object' &&\n 'color' in value &&\n value.color &&\n typeof value.color === 'object' &&\n 'light' in value.color\n ) {\n pairs[`chrome.${element}.color`] = value.color;\n }\n }\n }\n return Object.keys(pairs).length > 0 ? pairs : undefined;\n}\n\n/**\n * Parse a hex color to sRGB relative luminance (WCAG 2.1).\n * Returns 0 for invalid/unparseable colors.\n */\nfunction relativeLuminance(hex: string): number {\n const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());\n if (!m) return 0;\n const r = parseInt(m[1].slice(0, 2), 16) / 255;\n const g = parseInt(m[1].slice(2, 4), 16) / 255;\n const b = parseInt(m[1].slice(4, 6), 16) / 255;\n const toLinear = (c: number) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);\n return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);\n}\n\n/** Returns true if the hex color is perceptually dark (luminance < 0.2). */\nfunction isDarkBackground(hex: string): boolean {\n // A non-opaque background ('transparent', 'none', alpha-zero rgba) has no\n // intrinsic luminance — don't treat it as dark so light-mode chrome text\n // defaults don't get inverted.\n if (!isOpaqueColor(hex)) return false;\n return relativeLuminance(hex) < 0.2;\n}\n\n/**\n * Adapt chrome text colors for a dark background.\n * Only overrides values that still match the light-mode defaults,\n * so explicit user overrides are preserved.\n */\nfunction adaptChromeForDarkBg(theme: Theme, textColor: string): Theme {\n const light = DEFAULT_THEME.chrome;\n return {\n ...theme,\n chrome: {\n // Eyebrow keeps its accent tint regardless of surface mode.\n eyebrow: theme.chrome.eyebrow,\n title: {\n ...theme.chrome.title,\n color:\n theme.chrome.title.color === light.title.color ? textColor : theme.chrome.title.color,\n },\n subtitle: {\n ...theme.chrome.subtitle,\n color:\n theme.chrome.subtitle.color === light.subtitle.color\n ? adjustOpacity(textColor, 0.7)\n : theme.chrome.subtitle.color,\n },\n source: {\n ...theme.chrome.source,\n color:\n theme.chrome.source.color === light.source.color\n ? adjustOpacity(textColor, 0.5)\n : theme.chrome.source.color,\n },\n byline: {\n ...theme.chrome.byline,\n color:\n theme.chrome.byline.color === light.byline.color\n ? adjustOpacity(textColor, 0.5)\n : theme.chrome.byline.color,\n },\n footer: {\n ...theme.chrome.footer,\n color:\n theme.chrome.footer.color === light.footer.color\n ? adjustOpacity(textColor, 0.5)\n : theme.chrome.footer.color,\n },\n },\n };\n}\n\n/** Blend a hex color toward white/black by a factor to simulate opacity. */\nfunction adjustOpacity(hex: string, opacity: number): string {\n const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());\n if (!m) return hex;\n const r = parseInt(m[1].slice(0, 2), 16);\n const g = parseInt(m[1].slice(2, 4), 16);\n const b = parseInt(m[1].slice(4, 6), 16);\n // Blend toward mid-gray for muted secondary text\n const mix = (c: number) => Math.round(c * opacity + 128 * (1 - opacity));\n const toHex = (n: number) => n.toString(16).padStart(2, '0');\n return `#${toHex(mix(r))}${toHex(mix(g))}${toHex(mix(b))}`;\n}\n\n/**\n * Resolve a theme by deep-merging user overrides onto a base theme.\n *\n * Auto-detects dark backgrounds: if the resolved background color is\n * perceptually dark and chrome text colors are still the light-mode\n * defaults, they're adapted for readability.\n *\n * @param userTheme - Optional partial theme overrides from the spec.\n * @param base - Base theme to merge onto. Defaults to DEFAULT_THEME.\n * @returns A ResolvedTheme with all properties guaranteed.\n */\nexport function resolveTheme(userTheme?: ThemeConfig, base: Theme = DEFAULT_THEME): ResolvedTheme {\n let merged: Theme = userTheme ? deepMerge(base, themeConfigToPartial(userTheme)) : { ...base };\n\n // Propagate fonts.sizes overrides onto the chrome element fontSizes.\n // The chrome defaults hard-code fontSize independently from fonts.sizes,\n // so we sync them here after merging so a single sizes override applies everywhere.\n if (userTheme?.fonts?.sizes) {\n const s = userTheme.fonts.sizes;\n merged = {\n ...merged,\n chrome: {\n ...merged.chrome,\n ...(s.title !== undefined && {\n title: { ...merged.chrome.title, fontSize: s.title },\n }),\n ...(s.subtitle !== undefined && {\n subtitle: { ...merged.chrome.subtitle, fontSize: s.subtitle },\n }),\n ...(s.small !== undefined && {\n source: { ...merged.chrome.source, fontSize: s.small },\n byline: { ...merged.chrome.byline, fontSize: s.small },\n footer: { ...merged.chrome.footer, fontSize: s.small },\n }),\n },\n };\n }\n\n // Auto-adapt chrome for dark backgrounds\n const dark = isDarkBackground(merged.colors.background);\n if (dark) {\n merged = adaptChromeForDarkBg(merged, merged.colors.text);\n }\n\n return {\n ...merged,\n colors: {\n ...merged.colors,\n neutral: deriveNeutralRamp(merged.colors.text, merged.colors.background, dark),\n },\n isDark: dark,\n _tokenPairs: userTheme ? collectTokenPairs(userTheme) : undefined,\n } as ResolvedTheme;\n}\n","/**\n * Chrome layout computation.\n *\n * Takes a Chrome spec + resolved theme and produces a ResolvedChrome\n * with computed text positions, styles, and total chrome heights.\n *\n * Supports three chrome modes:\n * - full: all chrome elements rendered at normal size\n * - compact: title only, no subtitle/source/byline/footer\n * - hidden: no chrome at all (maximizes chart area)\n *\n * Font sizes scale down continuously at narrow widths to keep\n * chrome proportional to the container.\n */\n\nimport type { ChromeMode } from '../responsive/breakpoints';\nimport type {\n MeasureTextFn,\n ResolvedChrome,\n ResolvedChromeElement,\n ResolvedChromeRule,\n TextStyle,\n} from '../types/layout';\nimport type { Chrome, ChromeText } from '../types/spec';\nimport type { ChromeDefaults, ResolvedTheme } from '../types/theme';\nimport {\n BRAND_FONT_SIZE,\n BRAND_MIN_WIDTH,\n BRAND_RESERVE_WIDTH,\n COMPACT_WIDTH,\n estimateCharWidth,\n estimateTextHeight,\n} from './text-measure';\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Normalize a chrome field to text + optional style and offset overrides. */\nfunction normalizeChromeText(\n value: string | ChromeText | undefined,\n): { text: string; style?: ChromeText['style']; offset?: ChromeText['offset'] } | null {\n if (value === undefined) return null;\n if (typeof value === 'string') return { text: value };\n return { text: value.text, style: value.style, offset: value.offset };\n}\n\n/**\n * Scale a font size based on container width. Only applies to default sizes\n * (not user overrides). Scales from 100% at >= 500px down to 72% at <= 250px.\n */\nfunction scaleFontSize(baseFontSize: number, width: number): number {\n if (width >= 500) return baseFontSize;\n if (width <= 250) return Math.max(Math.round(baseFontSize * 0.72), 10);\n const t = (width - 250) / 250;\n return Math.max(Math.round(baseFontSize * (0.72 + t * 0.28)), 10);\n}\n\n/**\n * Extra scale applied to the default compact title font size in the 100-199px\n * cramped height range (see `computeChrome`'s `crampedTitle` param).\n */\nconst CRAMPED_TITLE_SCALE = 0.65;\n\n/** Build a TextStyle from chrome defaults + optional overrides, with width-based scaling. */\nfunction buildTextStyle(\n defaults: ChromeDefaults,\n fontFamily: string,\n textColor: string,\n width: number,\n overrides?: ChromeText['style'],\n extraScale = 1,\n): TextStyle {\n const hasExplicitSize = overrides?.fontSize !== undefined;\n const baseFontSize = overrides?.fontSize ?? defaults.fontSize;\n const fontSize = hasExplicitSize\n ? baseFontSize\n : Math.max(Math.round(scaleFontSize(baseFontSize, width) * extraScale), 10);\n\n // No dominantBaseline: chrome y positions are top-edge coordinates and\n // renderers convert to the alphabetic baseline via textAscent(). WebKit\n // renders `hanging` from different font metrics than Blink and never\n // inherits it into tspans (bugs 139258/297455), which shifted wrapped\n // titles above the container on iOS Safari.\n return {\n fontFamily: overrides?.fontFamily ?? fontFamily,\n fontSize,\n fontWeight: overrides?.fontWeight ?? defaults.fontWeight,\n fill: overrides?.color ?? textColor ?? defaults.color,\n lineHeight: defaults.lineHeight,\n textAnchor: 'start',\n };\n}\n\n/**\n * Estimate how many lines text will wrap to, given a max width.\n * Uses character-count word-wrapping that matches the SVG renderer's\n * wrapText behavior (word-boundary breaks, same charWidth heuristic).\n * Returns at least 1.\n */\nfunction estimateLineCount(\n text: string,\n style: TextStyle,\n maxWidth: number,\n measureText?: MeasureTextFn,\n maxLines?: number,\n): number {\n if (maxWidth <= 0) return 1;\n\n // Split on explicit newlines first, then estimate wrapping per segment\n const segments = text.split('\\n');\n if (segments.length > 1) {\n const count = segments.reduce((total, segment) => {\n return (\n total +\n (segment.length === 0 ? 1 : estimateLineCount(segment, style, maxWidth, measureText))\n );\n }, 0);\n return maxLines != null ? Math.min(count, maxLines) : count;\n }\n\n // Use real text measurement when available, fall back to heuristic\n if (measureText) {\n const textWidth = measureText(text, style.fontSize, style.fontWeight).width;\n if (textWidth <= maxWidth) return 1;\n\n const words = text.split(' ');\n let lines = 1;\n let current = '';\n\n for (const word of words) {\n const candidate = current ? `${current} ${word}` : word;\n const candidateWidth = measureText(candidate, style.fontSize, style.fontWeight).width;\n if (candidateWidth > maxWidth && current) {\n lines++;\n current = word;\n } else {\n current = candidate;\n }\n }\n\n return maxLines != null ? Math.min(lines, maxLines) : lines;\n }\n\n const charWidth = estimateCharWidth(style.fontSize, style.fontWeight);\n const maxChars = Math.floor(maxWidth / charWidth);\n\n if (text.length <= maxChars) return 1;\n\n const words = text.split(' ');\n let lines = 1;\n let current = '';\n\n for (const word of words) {\n const candidate = current ? `${current} ${word}` : word;\n if (candidate.length > maxChars && current) {\n lines++;\n current = word;\n } else {\n current = candidate;\n }\n }\n\n return maxLines != null ? Math.min(lines, maxLines) : lines;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Line-height multiplier for the auto-thinning footnote list.\n *\n * Exported because the chrome renderer steps each footnote line down by it\n * while the band height below is computed from it -- the two have to agree, or\n * the last line drifts outside the band that was reserved for it.\n */\nexport const FOOTNOTE_LINE_HEIGHT = 1.3;\n/** Gap below the footnote list, separating it from the source row. */\nconst FOOTNOTE_BAND_GAP = 4;\n\n/**\n * Vertical space a footnote list of `count` lines occupies below the plot.\n *\n * Single source of truth, deliberately: the engine reserves this band in the\n * bottom margin, the chrome renderer shifts the source row down by it, and the\n * brand watermark shifts by it too. When those three disagree the footnotes\n * land on top of the watermark, which is exactly the bug this centralizes away.\n */\nexport function footnoteBandHeight(count: number, theme: ResolvedTheme): number {\n if (count <= 0) return 0;\n return count * theme.fonts.sizes.small * FOOTNOTE_LINE_HEIGHT + FOOTNOTE_BAND_GAP;\n}\n\n/**\n * Compute resolved chrome layout from a Chrome spec and resolved theme.\n *\n * Produces positioned text elements and total chrome heights (top and bottom).\n * Top chrome: title, subtitle. Bottom chrome: source, byline, footer.\n *\n * @param chrome - The Chrome spec from the user's VizSpec.\n * @param theme - The fully resolved theme.\n * @param width - Total available width in pixels.\n * @param measureText - Optional real text measurement function from the adapter.\n * @param chromeMode - Chrome display mode: full, compact (title only), or hidden.\n * @param padding - Override padding (for scaled padding from dimensions).\n * @param watermark - Whether the brand watermark renders (affects bottom space).\n * @param bottomLegendHeight - Reserved height for a bottom-positioned legend\n * (legend bounds height + gap). When > 0, source/byline/footer y positions\n * are shifted down by this amount so the chrome stacks BELOW the legend\n * rather than colliding with it. The returned `bottomHeight` includes this\n * reservation, so callers should not double-reserve it in margin math.\n * @param crampedTitle - Shrinks the default (non-explicit) title font size by\n * `CRAMPED_TITLE_SCALE` on top of the width-based scaling. Set by callers for the\n * 100-199px cramped height range: at that height a full-size compact title\n * can push `chrome.topHeight` past the min-chart-height guardrail, which\n * would strip the title back out via the hidden-chrome fallback.\n */\nexport function computeChrome(\n chrome: Chrome | undefined,\n theme: ResolvedTheme,\n width: number,\n measureText?: MeasureTextFn,\n chromeMode: ChromeMode = 'full',\n padding?: number,\n watermark: boolean = true,\n bottomLegendHeight: number = 0,\n crampedTitle: boolean = false,\n): ResolvedChrome {\n if (!chrome || chromeMode === 'hidden') {\n // `watermark` is still true here in two cases: the user explicitly opted\n // in at a cramped height (the compiler suppresses the default there), or\n // the chromeEatsPlot/min-dims fallback hid chrome at a normal height where\n // the default watermark was never suppressed. Reserve the brand band for\n // both so the brand doesn't paint over the plot's bottom edge.\n if (watermark && width >= BRAND_MIN_WIDTH) {\n const hiddenPad = padding ?? theme.spacing.padding;\n const brandHeight = estimateTextHeight(BRAND_FONT_SIZE, 1);\n return {\n topHeight: 0,\n bottomHeight: theme.spacing.chartToFooter + brandHeight + hiddenPad + bottomLegendHeight,\n };\n }\n if (bottomLegendHeight > 0) {\n // No brand band, but a bottom legend was reserved upstream. Surface the\n // reservation through bottomHeight so margin math stays additive, same\n // as the compact branch below.\n return { topHeight: 0, bottomHeight: bottomLegendHeight };\n }\n return { topHeight: 0, bottomHeight: 0 };\n }\n\n const pad = padding ?? theme.spacing.padding;\n const chromeGap = theme.spacing.chromeGap;\n // Subtract a small buffer beyond padding so the heuristic char-width\n // estimate triggers word-wrap before the real font overflows the container.\n const WRAP_SAFETY_BUFFER = 5;\n const maxWidth = width - pad * 2 - WRAP_SAFETY_BUFFER;\n const fontFamily = theme.fonts.family;\n\n // Track vertical cursor for top elements\n let topY = pad;\n const topElements: Partial<Pick<ResolvedChrome, 'eyebrow' | 'title' | 'subtitle'>> = {};\n\n // Eyebrow (hidden in compact mode — same rule as subtitle, keeps the\n // title alone at narrow viewports).\n const eyebrowNorm = chromeMode === 'compact' ? null : normalizeChromeText(chrome.eyebrow);\n const titleNormPeek = normalizeChromeText(chrome.title);\n const subtitleNormPeek = chromeMode === 'compact' ? null : normalizeChromeText(chrome.subtitle);\n\n // Editorial rule (theme.rule): a short colored bar above the block. Only\n // drawn when there is top chrome for it to sit above; it reserves its own\n // thickness plus one chromeGap, exactly like a text element.\n let ruleElement: ResolvedChromeRule | undefined;\n if (theme.rule && (eyebrowNorm || titleNormPeek || subtitleNormPeek)) {\n ruleElement = {\n x: pad,\n y: topY,\n width: theme.rule.width,\n thickness: theme.rule.thickness,\n color: theme.rule.color,\n };\n topY += theme.rule.thickness + chromeGap;\n }\n\n if (eyebrowNorm) {\n const style = buildTextStyle(\n theme.chrome.eyebrow,\n fontFamily,\n theme.chrome.eyebrow.color,\n width,\n eyebrowNorm.style,\n );\n const maxLines = eyebrowNorm.style?.maxLines;\n const lineCount = estimateLineCount(eyebrowNorm.text, style, maxWidth, measureText, maxLines);\n const element: ResolvedChromeElement = {\n text: eyebrowNorm.text,\n x: pad + (eyebrowNorm.offset?.dx ?? 0),\n y: topY + (eyebrowNorm.offset?.dy ?? 0),\n maxWidth,\n ...(maxLines != null ? { maxLines } : {}),\n style,\n };\n topElements.eyebrow = element;\n topY += estimateTextHeight(style.fontSize, lineCount, style.lineHeight) + chromeGap;\n }\n\n // Title\n const titleNorm = titleNormPeek;\n if (titleNorm) {\n const style = buildTextStyle(\n theme.chrome.title,\n fontFamily,\n theme.chrome.title.color,\n width,\n titleNorm.style,\n crampedTitle ? CRAMPED_TITLE_SCALE : 1,\n );\n const maxLines = titleNorm.style?.maxLines;\n const lineCount = estimateLineCount(titleNorm.text, style, maxWidth, measureText, maxLines);\n const element: ResolvedChromeElement = {\n text: titleNorm.text,\n x: pad + (titleNorm.offset?.dx ?? 0),\n y: topY + (titleNorm.offset?.dy ?? 0),\n maxWidth,\n ...(maxLines != null ? { maxLines } : {}),\n style,\n };\n topElements.title = element;\n topY += estimateTextHeight(style.fontSize, lineCount, style.lineHeight) + chromeGap;\n }\n\n // Subtitle (hidden in compact mode)\n const subtitleNorm = subtitleNormPeek;\n if (subtitleNorm) {\n const style = buildTextStyle(\n theme.chrome.subtitle,\n fontFamily,\n theme.chrome.subtitle.color,\n width,\n subtitleNorm.style,\n );\n const maxLines = subtitleNorm.style?.maxLines;\n const lineCount = estimateLineCount(subtitleNorm.text, style, maxWidth, measureText, maxLines);\n const element: ResolvedChromeElement = {\n text: subtitleNorm.text,\n x: pad + (subtitleNorm.offset?.dx ?? 0),\n y: topY + (subtitleNorm.offset?.dy ?? 0),\n maxWidth,\n ...(maxLines != null ? { maxLines } : {}),\n style,\n };\n topElements.subtitle = element;\n topY += estimateTextHeight(style.fontSize, lineCount, style.lineHeight) + chromeGap;\n }\n\n // Add chromeToChart gap if there are any top elements. Tighten on narrow\n // viewports so the subtitle doesn't float far above a legend or chart area.\n const hasTopChrome = eyebrowNorm || titleNorm || subtitleNorm;\n const chromeToChart =\n width < COMPACT_WIDTH ? Math.min(theme.spacing.chromeToChart, 2) : theme.spacing.chromeToChart;\n const topHeight = hasTopChrome ? topY - pad + chromeToChart - chromeGap : 0;\n\n // Bottom chrome text hidden in compact mode, but brand watermark still\n // renders for wide-enough charts. Reserve space so it doesn't overflow.\n if (chromeMode === 'compact') {\n let compactBottom = 0;\n if (watermark && width >= BRAND_MIN_WIDTH) {\n const brandHeight = estimateTextHeight(BRAND_FONT_SIZE, 1);\n compactBottom = theme.spacing.chartToFooter + brandHeight + pad + bottomLegendHeight;\n } else if (bottomLegendHeight > 0) {\n // No bottom chrome content but a bottom legend was reserved upstream.\n // Surface the reservation through bottomHeight so margin math stays\n // additive and consistent with full mode.\n compactBottom = bottomLegendHeight;\n }\n return {\n topHeight,\n bottomHeight: compactBottom,\n ...(ruleElement ? { rule: ruleElement } : {}),\n ...topElements,\n };\n }\n\n // Bottom elements: source, byline, footer, and an optional custom brand\n // (`chrome.brand`). When a custom brand is supplied it suppresses the default\n // OpenData watermark and renders right-anchored on the same baseline.\n const brandNorm = normalizeChromeText(chrome.brand);\n const showWatermark = watermark && !brandNorm;\n // Reserve space on the right for the brand watermark or custom brand so the\n // left-anchored bottom chrome items don't overlap.\n const shouldReserveBrandWidth = (showWatermark || !!brandNorm) && width >= BRAND_MIN_WIDTH;\n const bottomMaxWidth = maxWidth - (shouldReserveBrandWidth ? BRAND_RESERVE_WIDTH : 0);\n const bottomElements: Partial<Pick<ResolvedChrome, 'source' | 'byline' | 'footer'>> = {};\n let bottomHeight = 0;\n\n const bottomItems: Array<{\n key: 'source' | 'byline' | 'footer';\n norm: { text: string; style?: ChromeText['style']; offset?: ChromeText['offset'] };\n defaults: ChromeDefaults;\n }> = [];\n\n const sourceNorm = normalizeChromeText(chrome.source);\n if (sourceNorm) {\n bottomItems.push({\n key: 'source',\n norm: sourceNorm,\n defaults: theme.chrome.source,\n });\n }\n\n const bylineNorm = normalizeChromeText(chrome.byline);\n if (bylineNorm) {\n bottomItems.push({\n key: 'byline',\n norm: bylineNorm,\n defaults: theme.chrome.byline,\n });\n }\n\n const footerNorm = normalizeChromeText(chrome.footer);\n if (footerNorm) {\n bottomItems.push({\n key: 'footer',\n norm: footerNorm,\n defaults: theme.chrome.footer,\n });\n }\n\n if (bottomItems.length > 0) {\n bottomHeight += theme.spacing.chartToFooter;\n // Push bottom chrome below the bottom legend (if reserved). Stored y values\n // include this offset so renderers don't need to know about the legend band.\n bottomHeight += bottomLegendHeight;\n\n for (const item of bottomItems) {\n const style = buildTextStyle(\n item.defaults,\n fontFamily,\n item.defaults.color,\n width,\n item.norm.style,\n );\n const maxLines = item.norm.style?.maxLines;\n const lineCount = estimateLineCount(\n item.norm.text,\n style,\n bottomMaxWidth,\n measureText,\n maxLines,\n );\n const height = estimateTextHeight(style.fontSize, lineCount, style.lineHeight);\n\n // y positions will be computed relative to the bottom of the\n // chart area by the engine. We store offsets from bottom start.\n bottomElements[item.key] = {\n text: item.norm.text,\n x: pad + (item.norm.offset?.dx ?? 0),\n y: bottomHeight + (item.norm.offset?.dy ?? 0), // offset from where bottom chrome starts\n maxWidth: bottomMaxWidth,\n ...(maxLines != null ? { maxLines } : {}),\n style,\n };\n\n bottomHeight += height + chromeGap;\n }\n\n // Remove trailing gap\n bottomHeight -= chromeGap;\n\n // Ensure bottom height accommodates the brand watermark, which renders\n // at the same Y as the first bottom chrome item but is taller (20px font\n // vs 12px source). Without this, the brand overflows the SVG viewBox.\n if (showWatermark && width >= BRAND_MIN_WIDTH) {\n const brandHeight = estimateTextHeight(BRAND_FONT_SIZE, 1);\n // firstItemY is chartToFooter (the Y offset of the first bottom item).\n // The brand needs brandHeight below that point; bottom chrome content\n // needs (bottomHeight - chartToFooter). Take the max.\n const contentBelowFirstItem = bottomHeight - theme.spacing.chartToFooter;\n if (brandHeight > contentBelowFirstItem) {\n bottomHeight += brandHeight - contentBelowFirstItem;\n }\n }\n\n // Add bottom padding\n bottomHeight += pad;\n } else if (showWatermark && width >= BRAND_MIN_WIDTH) {\n // No bottom chrome items, but brand watermark still renders.\n // Reserve space: chartToFooter gap + brand text height + padding,\n // plus the bottom-legend reservation so the watermark sits below it.\n const brandHeight = estimateTextHeight(BRAND_FONT_SIZE, 1);\n bottomHeight = theme.spacing.chartToFooter + brandHeight + pad + bottomLegendHeight;\n } else if (bottomLegendHeight > 0) {\n // No bottom chrome content and no watermark, but a bottom legend was\n // reserved upstream. Surface the reservation so callers don't need to\n // re-add it on top of bottomHeight.\n bottomHeight = bottomLegendHeight;\n }\n\n // Custom brand is right-anchored on the same row as the first bottom chrome\n // item. Compute its style/position once we know whether bottom chrome exists.\n let brandElement: ResolvedChromeElement | undefined;\n if (brandNorm && width >= BRAND_MIN_WIDTH) {\n const brandStyle = buildTextStyle(\n theme.chrome.footer,\n fontFamily,\n theme.chrome.footer.color,\n width,\n brandNorm.style,\n );\n brandStyle.textAnchor = 'end';\n // Y matches the first bottom item (chartToFooter offset). When there are\n // no bottom items, the brand becomes the sole footer element and we need\n // to allocate the same baseline.\n const brandY =\n bottomItems.length > 0 ? theme.spacing.chartToFooter : theme.spacing.chartToFooter;\n brandElement = {\n text: brandNorm.text,\n x: width - pad + (brandNorm.offset?.dx ?? 0),\n y: brandY + (brandNorm.offset?.dy ?? 0),\n maxWidth: BRAND_RESERVE_WIDTH,\n style: brandStyle,\n };\n // Ensure bottomHeight reserves space when brand is the only footer item.\n if (bottomItems.length === 0) {\n const brandHeight = estimateTextHeight(brandStyle.fontSize, 1, brandStyle.lineHeight);\n bottomHeight = theme.spacing.chartToFooter + brandHeight + pad;\n }\n }\n\n return {\n topHeight,\n bottomHeight,\n ...(ruleElement ? { rule: ruleElement } : {}),\n ...topElements,\n ...bottomElements,\n ...(brandElement ? { brand: brandElement } : {}),\n };\n}\n","/**\n * Word-wrap a text string into lines that fit within a max width.\n *\n * Shared by the SVG chart renderer and the sankey renderer. When a\n * `measureText` callback is provided, uses real DOM measurement for\n * accurate wrapping. Otherwise falls back to a character-width\n * heuristic driven by `estimateCharWidth` from text-measure.ts.\n *\n * Non-browser callers (SSR, tests) omit the measureText argument to\n * use the heuristic fallback. Do not change the signature without\n * re-verifying visual baselines for every caller.\n */\n\nimport type { MeasureTextFn } from '../types/layout';\nimport { estimateCharWidth } from './text-measure';\n\n/**\n * Break text into lines that fit within maxWidth using word wrapping.\n *\n * Splits on explicit newlines first, then word-wraps each segment.\n * Preserves empty segments so consecutive newlines produce blank lines.\n *\n * @param text - The text to wrap. May contain `\\n` for forced breaks.\n * @param fontSize - Font size in pixels.\n * @param fontWeight - Font weight (100-900).\n * @param maxWidth - Maximum line width in pixels. Non-positive values return `[text]` unchanged.\n * @param measureText - Optional real text measurer. When omitted, uses a character-width heuristic.\n */\nexport function wrapText(\n text: string,\n fontSize: number,\n fontWeight: number,\n maxWidth: number,\n measureText?: MeasureTextFn,\n): string[] {\n if (maxWidth <= 0) return [text];\n\n // Split on explicit newlines first\n const segments = text.split('\\n');\n if (segments.length > 1) {\n return segments.flatMap((segment) =>\n segment.length === 0 ? [''] : wrapText(segment, fontSize, fontWeight, maxWidth, measureText),\n );\n }\n\n // Use real text measurement when available\n if (measureText) {\n const textWidth = measureText(text, fontSize, fontWeight).width;\n if (textWidth <= maxWidth) return [text];\n\n const words = text.split(' ');\n const lines: string[] = [];\n let current = '';\n\n for (const word of words) {\n const candidate = current ? `${current} ${word}` : word;\n const candidateWidth = measureText(candidate, fontSize, fontWeight).width;\n if (candidateWidth > maxWidth && current) {\n lines.push(current);\n current = word;\n } else {\n current = candidate;\n }\n }\n if (current) lines.push(current);\n\n return lines;\n }\n\n // Heuristic: estimate character width from font size and weight.\n // Reuses the same ratio and weight adjustment constants as text-measure.\n const charWidth = estimateCharWidth(fontSize, fontWeight);\n const maxChars = Math.floor(maxWidth / charWidth);\n\n if (text.length <= maxChars) return [text];\n\n const words = text.split(' ');\n const lines: string[] = [];\n let current = '';\n\n for (const word of words) {\n const candidate = current ? `${current} ${word}` : word;\n if (candidate.length > maxChars && current) {\n lines.push(current);\n current = word;\n } else {\n current = candidate;\n }\n }\n if (current) lines.push(current);\n\n return lines;\n}\n","/**\n * Label collision detection and resolution.\n *\n * Greedy algorithm: sort by priority, place in order, try offset\n * positions for conflicts, demote to tooltip-only if no position works.\n */\n\nimport { estimateTextWidth } from '../layout/text-measure';\nimport type { Rect, ResolvedLabel, TextStyle } from '../types/layout';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Priority levels for label placement. Data labels win over annotations which win over axes. */\nexport type LabelPriority = 'data' | 'annotation' | 'axis';\n\n/** Priority sort order (lower = higher priority). */\nconst PRIORITY_ORDER: Record<LabelPriority, number> = {\n data: 0,\n annotation: 1,\n axis: 2,\n};\n\n/**\n * A label candidate for collision resolution.\n * The collision engine decides its final position and visibility.\n */\nexport interface LabelCandidate {\n /** The label text. */\n text: string;\n /** Preferred anchor position (before collision resolution). */\n anchorX: number;\n /** Preferred anchor position (before collision resolution). */\n anchorY: number;\n /** Estimated width of the label text. */\n width: number;\n /** Estimated height of the label text. */\n height: number;\n /** Label priority for collision resolution. */\n priority: LabelPriority;\n /** Text style to apply. */\n style: TextStyle;\n /**\n * Optional caller-assigned source index. resolveCollisions may reorder its\n * output (it sorts by priority), so callers that zip the result back against\n * a parallel array should carry an index here and read it off ResolvedLabel\n * rather than relying on positional order.\n */\n index?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Collision detection\n// ---------------------------------------------------------------------------\n\n/**\n * Detect AABB (axis-aligned bounding box) overlap between two rectangles.\n */\nexport function detectCollision(a: Rect, b: Rect): boolean {\n return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;\n}\n\n/** Compute the area of intersection between two axis-aligned rectangles. Returns 0 if disjoint. */\nexport function overlapArea(a: Rect, b: Rect): number {\n const w = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);\n const h = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);\n return w > 0 && h > 0 ? w * h : 0;\n}\n\n// ---------------------------------------------------------------------------\n// Offset strategies\n// ---------------------------------------------------------------------------\n\n/** An offset position to try when resolving a label collision. */\nexport interface OffsetStrategy {\n dx: number;\n dy: number;\n}\n\n/** Offsets to try when a label collides with an existing placement. */\nexport const OFFSET_STRATEGIES: readonly OffsetStrategy[] = [\n { dx: 0, dy: 0 }, // original position\n { dx: 0, dy: -1.2 }, // above (factor of height)\n { dx: 0, dy: 1.2 }, // below\n { dx: 1.1, dy: 0 }, // right\n { dx: -1.1, dy: 0 }, // left\n { dx: 1.1, dy: -1.2 }, // upper-right\n { dx: -1.1, dy: -1.2 }, // upper-left\n];\n\n/**\n * Extended offset strategies with additional vertical spread for dense\n * multi-series endpoints (e.g. 5+ line series converging at similar y-values).\n */\nexport const EXTENDED_OFFSET_STRATEGIES: readonly OffsetStrategy[] = [\n ...OFFSET_STRATEGIES,\n { dx: 0, dy: -2.4 }, // further above\n { dx: 0, dy: 2.4 }, // further below\n { dx: 0, dy: -3.6 }, // even further above\n { dx: 0, dy: 3.6 }, // even further below\n { dx: 1.1, dy: -2.4 }, // upper-right far\n { dx: 1.1, dy: 2.4 }, // lower-right far\n];\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve label collisions using a greedy placement algorithm.\n *\n * Sorts labels by priority (data > annotation > axis), then places\n * each label at its preferred position. If it collides with an already-placed\n * label, tries offset positions. If no position works, the label is\n * demoted to tooltip-only (visible: false).\n *\n * @param labels - Array of label candidates to position.\n * @param strategies - Optional offset strategies to use (defaults to OFFSET_STRATEGIES).\n * @returns Array of resolved labels with computed positions and visibility.\n */\nexport function resolveCollisions(\n labels: LabelCandidate[],\n strategies: readonly OffsetStrategy[] = OFFSET_STRATEGIES,\n): ResolvedLabel[] {\n // Sort by priority (highest first)\n const sorted = [...labels].sort(\n (a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority],\n );\n\n const placed: Rect[] = [];\n const results: ResolvedLabel[] = [];\n\n for (const label of sorted) {\n let bestRect: Rect | null = null;\n let bestX = label.anchorX;\n let bestY = label.anchorY;\n\n // Try each offset strategy\n for (const offset of strategies) {\n const candidateX = label.anchorX + offset.dx * label.width;\n const candidateY = label.anchorY + offset.dy * label.height;\n const candidateRect: Rect = {\n x: candidateX,\n y: candidateY,\n width: label.width,\n height: label.height,\n };\n\n const hasCollision = placed.some((p) => detectCollision(candidateRect, p));\n\n if (!hasCollision) {\n bestRect = candidateRect;\n bestX = candidateX;\n bestY = candidateY;\n break;\n }\n }\n\n if (bestRect) {\n placed.push(bestRect);\n const nudgeDx = bestX - label.anchorX;\n const nudgeDy = bestY - label.anchorY;\n const nudgeDist = Math.sqrt(nudgeDx * nudgeDx + nudgeDy * nudgeDy);\n // Only show a connector when the label is nudged far enough that the\n // reader can't tell which data point it belongs to. Short nudges between\n // adjacent series labels produce visual noise without aiding comprehension.\n const MIN_CONNECTOR_DISTANCE = 20;\n const needsConnector = nudgeDist >= MIN_CONNECTOR_DISTANCE;\n\n results.push({\n text: label.text,\n x: bestX,\n y: bestY,\n style: label.style,\n visible: true,\n // Only carry the index through when the caller set one, so labels that\n // don't need it keep an unchanged output shape.\n ...(label.index !== undefined ? { index: label.index } : {}),\n connector: needsConnector\n ? {\n from: { x: bestX, y: bestY },\n to: { x: label.anchorX, y: label.anchorY },\n stroke: label.style.fill,\n style: 'straight' as const,\n arrow: false,\n }\n : undefined,\n });\n } else {\n // No position works, demote to tooltip-only\n results.push({\n text: label.text,\n x: label.anchorX,\n y: label.anchorY,\n style: label.style,\n visible: false,\n ...(label.index !== undefined ? { index: label.index } : {}),\n });\n }\n }\n\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Label bounds estimation\n// ---------------------------------------------------------------------------\n\n/**\n * Compute the bounding rect of a resolved label from its position and text.\n * Uses heuristic text measurement so it works without DOM access.\n *\n * label.y is the SVG text anchor point, whose meaning depends on the label's\n * dominant-baseline. The rect's top edge is derived accordingly so the box\n * covers the actual glyphs (obstacles were sitting ~0.8*fontSize below the\n * text for alphabetic-baseline labels, e.g. column value labels):\n * - 'central': y is the vertical center, so top = y - height/2.\n * - 'hanging'/'text-after-edge': y is the top edge already.\n * - default (alphabetic): y is the baseline, so top = y - fontSize*0.8\n * (matches textAscent, the top-to-baseline distance renderers apply).\n */\nexport function computeLabelBounds(label: ResolvedLabel): Rect {\n const fontSize = label.style.fontSize;\n const fontWeight = label.style.fontWeight;\n const width = estimateTextWidth(label.text, fontSize, fontWeight);\n const height = fontSize * (label.style.lineHeight ?? 1.2);\n\n // Adjust x based on text anchor\n let x = label.x;\n if (label.style.textAnchor === 'middle') {\n x = label.x - width / 2;\n } else if (label.style.textAnchor === 'end') {\n x = label.x - width;\n }\n\n // Map the anchor y to the box top based on dominant-baseline.\n const baseline = label.style.dominantBaseline;\n let y: number;\n if (baseline === 'central') {\n y = label.y - height / 2;\n } else if (baseline === 'hanging' || baseline === 'text-after-edge') {\n y = label.y;\n } else {\n y = label.y - fontSize * 0.8;\n }\n\n return { x, y, width, height };\n}\n","/**\n * Locale-aware number and date formatting utilities.\n *\n * Uses d3-format and d3-time-format for formatting,\n * with convenience wrappers for common patterns.\n */\n\nimport { format as d3Format } from 'd3-format';\nimport { timeFormat, utcFormat } from 'd3-time-format';\n\n// ---------------------------------------------------------------------------\n// Cached d3-format instances\n// ---------------------------------------------------------------------------\n\n// Module-level caching assumes the d3-format/d3-time-format default locale;\n// consumer `formatDefaultLocale()` calls are unsupported (verified: no\n// callers of formatDefaultLocale exist in this codebase).\nconst FORMAT_INTEGER = d3Format(',');\nconst FORMAT_TINY = d3Format('.2~r');\nconst FORMAT_DECIMAL = d3Format(',.2f');\nconst FORMAT_ABBREV_MANTISSA = d3Format('.1~f');\nconst FORMAT_PERCENT_FRACTION = d3Format('.1~%');\nconst FORMAT_PERCENT_RAW = d3Format(',.1~f');\nconst FORMAT_CURRENCY_INTEGER = d3Format('$,');\nconst FORMAT_CURRENCY_DECIMAL = d3Format('$,.2f');\nconst FORMAT_STEP_FRACTION = [d3Format('.0~f'), d3Format('.1~f'), d3Format('.2~f')] as const;\n\n// ---------------------------------------------------------------------------\n// Number formatting\n// ---------------------------------------------------------------------------\n\n/**\n * Format a number with locale-appropriate separators.\n *\n * Uses d3-format under the hood. Default format: comma-separated\n * with auto-precision (e.g. 1500000 -> \"1,500,000\").\n *\n * @param value - The number to format.\n * @param locale - Locale string (currently unused, reserved for i18n).\n */\nexport function formatNumber(value: number, _locale?: string): string {\n if (!Number.isFinite(value)) return String(value);\n if (Number.isInteger(value)) {\n return FORMAT_INTEGER(value);\n }\n if (Math.abs(value) < 0.005) {\n return FORMAT_TINY(value);\n }\n return FORMAT_DECIMAL(value);\n}\n\n// ---------------------------------------------------------------------------\n// Number abbreviation\n// ---------------------------------------------------------------------------\n\nconst UNIT_TABLE: Array<{ threshold: number; suffix: string; divisor: number }> = [\n { threshold: 1_000_000_000_000, suffix: 'T', divisor: 1_000_000_000_000 },\n { threshold: 1_000_000_000, suffix: 'B', divisor: 1_000_000_000 },\n { threshold: 1_000_000, suffix: 'M', divisor: 1_000_000 },\n { threshold: 1_000, suffix: 'k', divisor: 1_000 },\n];\n\nfunction unitFor(abs: number): { suffix: string; divisor: number } {\n for (const entry of UNIT_TABLE) {\n if (abs >= entry.threshold) return entry;\n }\n return { suffix: '', divisor: 1 };\n}\n\nfunction fractionDigits(x: number): number {\n const a = Math.abs(x);\n for (let p = 0; p <= 3; p++) {\n const scaled = a * 10 ** p;\n if (Math.abs(scaled - Math.round(scaled)) < 1e-9) return p;\n }\n return 4;\n}\n\nexport function abbreviateNumber(value: number): string {\n if (!Number.isFinite(value)) return String(value);\n\n const absValue = Math.abs(value);\n const sign = value < 0 ? '-' : '';\n\n for (let i = 0; i < UNIT_TABLE.length; i++) {\n const { threshold, suffix, divisor } = UNIT_TABLE[i];\n if (absValue >= threshold) {\n const mantissa = FORMAT_ABBREV_MANTISSA(absValue / divisor);\n // Roll-up: if mantissa rounds to 1000, use the next unit up.\n // When i === 0 (T), there's no larger unit so we emit \"1000T\".\n if (mantissa === '1000' && i > 0) {\n return `${sign}1${UNIT_TABLE[i - 1].suffix}`;\n }\n return `${sign}${mantissa}${suffix}`;\n }\n }\n\n return formatNumber(value);\n}\n\n// ---------------------------------------------------------------------------\n// Ordinal formatting\n// ---------------------------------------------------------------------------\n\n/**\n * Format a number as an English ordinal: 1 -> \"1st\", 2 -> \"2nd\", 3 -> \"3rd\",\n * 11 -> \"11th\", 22 -> \"22nd\". Values are rounded to the nearest integer first;\n * intended for rank axes where the domain is integer positions.\n */\nexport function formatOrdinal(value: number): string {\n if (!Number.isFinite(value)) return String(value);\n const n = Math.round(value);\n const abs = Math.abs(n);\n const rem100 = abs % 100;\n const rem10 = abs % 10;\n let suffix: string;\n if (rem100 >= 11 && rem100 <= 13) suffix = 'th';\n else if (rem10 === 1) suffix = 'st';\n else if (rem10 === 2) suffix = 'nd';\n else if (rem10 === 3) suffix = 'rd';\n else suffix = 'th';\n return `${n}${suffix}`;\n}\n\n// ---------------------------------------------------------------------------\n// d3-format with suffix support\n// ---------------------------------------------------------------------------\n\n/**\n * Regex that matches a valid d3-format specifier (with optional ~ trim flag)\n * followed by a literal suffix. The first capture group is the d3 format part,\n * the second is the trailing literal suffix (e.g. \"T\", \"pp\", etc.).\n *\n * Examples:\n * - \"$,.2~fT\" -> [\"$,.2~f\", \"T\"]\n * - \".0f%\" -> [\".0f\", \"%\"] (% here is literal, not d3 percent type)\n * - \"$,.0f\" -> no match (valid d3 format, no suffix)\n */\nconst D3_FORMAT_SUFFIX_RE = /^(.*~?[efgsrdxXobcnp%])(.+)$/;\n\n/**\n * Build a number formatter from a d3-format string, with support for a\n * trailing literal suffix that d3-format itself would reject.\n *\n * Also accepts the OpenChart extension `'ordinal'`, which renders English\n * ordinals (\"1st\", \"2nd\", \"3rd\") for rank axes.\n *\n * Returns null if the format string is falsy or unparseable.\n */\nexport function buildD3Formatter(formatStr: string | undefined): ((v: number) => string) | null {\n if (!formatStr) return null;\n\n // OpenChart extension: rank-axis ordinals (\"1st\", \"2nd\", \"3rd\").\n if (formatStr === 'ordinal') return formatOrdinal;\n\n // OpenChart extension: 'compact' abbreviates (1.2k, 3.4M). Charts abbreviate\n // by default; on a table, where full precision is the default, this is how a\n // column opts in. Handled here so every resolution path picks it up.\n if (formatStr === 'compact') {\n return (v: number) => {\n if (!Number.isFinite(v)) return String(v);\n return Math.abs(v) >= 1000 ? abbreviateNumber(v) : formatNumber(v);\n };\n }\n\n try {\n const fmt = d3Format(formatStr);\n // Replace SI prefix \"G\" (giga) with \"B\" (billion) for financial readability\n if (formatStr.includes('s')) {\n return (v: number) => fmt(v).replace(/G$/, 'B');\n }\n return fmt;\n } catch {\n // If d3-format rejects it, try stripping a trailing literal suffix\n const m = formatStr.match(D3_FORMAT_SUFFIX_RE);\n if (m) {\n try {\n const fmt = d3Format(m[1]);\n const suffix = m[2];\n return (v: number) => fmt(v) + suffix;\n } catch {\n // Unparseable even after suffix stripping\n }\n }\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Number formatter type + semantic formatters\n// ---------------------------------------------------------------------------\n\nexport type NumberFormatter = (value: number) => string;\n\nexport function formatPercent(value: number, options?: { fraction?: boolean }): string {\n const fraction = options?.fraction ?? true;\n if (fraction) {\n return FORMAT_PERCENT_FRACTION(value);\n }\n return `${FORMAT_PERCENT_RAW(value)}%`;\n}\n\nexport function formatCurrency(value: number, options?: { compact?: boolean }): string {\n const compact = options?.compact ?? false;\n const abs = Math.abs(value);\n const sign = value < 0 ? '-' : '';\n\n if (compact) {\n if (abs >= 1000) {\n return `${sign}$${abbreviateNumber(abs)}`;\n }\n return `${sign}$${formatNumber(abs)}`;\n }\n if (Number.isInteger(value)) {\n return sign + FORMAT_CURRENCY_INTEGER(abs);\n }\n return sign + FORMAT_CURRENCY_DECIMAL(abs);\n}\n\n// ---------------------------------------------------------------------------\n// Years guard\n// ---------------------------------------------------------------------------\n\nexport function isYearLikeValues(values: readonly number[]): boolean {\n if (values.length === 0) return false;\n for (const v of values) {\n if (!Number.isFinite(v) || !Number.isInteger(v) || v < 1500 || v > 2500) return false;\n }\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// Field format context\n// ---------------------------------------------------------------------------\n\nexport interface FieldFormatContext {\n extent?: [number, number];\n allIntegers?: boolean;\n surface?: 'chart' | 'table';\n step?: number;\n stepReference?: number;\n}\n\nexport function computeFieldFormatContext(\n values: Iterable<unknown>,\n surface?: 'chart' | 'table',\n): FieldFormatContext {\n let min = Infinity;\n let max = -Infinity;\n let allIntegers = true;\n let hasFinite = false;\n\n for (const raw of values) {\n const v = typeof raw === 'number' ? raw : typeof raw === 'string' ? Number(raw) : NaN;\n if (!Number.isFinite(v)) continue;\n hasFinite = true;\n if (v < min) min = v;\n if (v > max) max = v;\n if (allIntegers && !Number.isInteger(v)) allIntegers = false;\n }\n\n const ctx: FieldFormatContext = {};\n if (hasFinite) {\n ctx.extent = [min, max];\n ctx.allIntegers = allIntegers;\n }\n if (surface) ctx.surface = surface;\n return ctx;\n}\n\nexport function isYearContext(ctx: FieldFormatContext | undefined): boolean {\n if (!ctx?.extent || !ctx.allIntegers) return false;\n const [min, max] = ctx.extent;\n return min >= 1500 && max <= 2500;\n}\n\n// ---------------------------------------------------------------------------\n// Keyword-aware formatter resolution\n// ---------------------------------------------------------------------------\n\nexport function resolveNumberFormatter(\n formatStr: string | undefined,\n ctx?: FieldFormatContext,\n): NumberFormatter | null {\n if (!formatStr) return null;\n\n if (formatStr === 'ordinal') return formatOrdinal;\n\n // OpenChart extension: 'compact' abbreviates (1.2k, 3.4M). Charts abbreviate\n // by default; on a table, where full precision is the default, this is how a\n // column opts in. Handled here so every resolution path picks it up.\n if (formatStr === 'compact') {\n return (v: number) => {\n if (!Number.isFinite(v)) return String(v);\n return Math.abs(v) >= 1000 ? abbreviateNumber(v) : formatNumber(v);\n };\n }\n\n if (formatStr === 'percent') {\n // Heuristic: if all values fit in [0,1] (maxAbs <= 1), treat them as\n // fractions (0.45 -> \"45%\"). Values > 1 are treated as pre-scaled\n // (45 -> \"45%\"). Datasets like [0, 0.5, 1.1] are ambiguous; the <= 1\n // threshold errs on the side of the most common convention (fractions).\n const fraction = ctx?.extent\n ? Math.max(Math.abs(ctx.extent[0]), Math.abs(ctx.extent[1])) <= 1\n : true;\n return (v: number) => formatPercent(v, { fraction });\n }\n\n if (formatStr === 'currency') {\n const isTable = ctx?.surface === 'table';\n if (!isTable && ctx?.step != null && (ctx.stepReference ?? 0) >= 1000) {\n const stepFmt = buildCompactStepFormatter(ctx.step);\n return (v: number) => {\n const sign = v < 0 ? '-' : '';\n return `${sign}$${stepFmt(Math.abs(v))}`;\n };\n }\n return (v: number) => formatCurrency(v, { compact: !isTable });\n }\n\n return buildD3Formatter(formatStr);\n}\n\n// ---------------------------------------------------------------------------\n// Default number formatter\n// ---------------------------------------------------------------------------\n\nexport function defaultNumberFormatter(ctx?: FieldFormatContext): NumberFormatter {\n if (isYearContext(ctx)) {\n return (v: number) => String(Math.round(v));\n }\n\n if (ctx?.surface === 'table') {\n return formatNumber;\n }\n\n if (ctx?.step != null && (ctx.stepReference ?? maxAbs(ctx.extent)) >= 1000) {\n return buildCompactStepFormatter(ctx.step);\n }\n\n return (v: number) => {\n if (!Number.isFinite(v)) return String(v);\n // When no context is provided (ARIA marks, standalone helpers) we\n // fall back to a per-value year heuristic: integers in [1500, 2500]\n // render bare. This trades false positives on non-year integers\n // (e.g. \"connections: 2024\") for avoiding \"2k\" on actual year axes.\n // Callers with field context should always pass it to get the\n // field-level isYearContext check instead.\n if (ctx?.extent === undefined && ctx?.allIntegers === undefined) {\n if (Number.isInteger(v) && v >= 1500 && v <= 2500) return String(v);\n }\n return Math.abs(v) >= 1000 ? abbreviateNumber(v) : formatNumber(v);\n };\n}\n\nfunction maxAbs(extent: [number, number] | undefined): number {\n if (!extent) return 0;\n return Math.max(Math.abs(extent[0]), Math.abs(extent[1]));\n}\n\n// ---------------------------------------------------------------------------\n// Step-aware compact tick formatter\n// ---------------------------------------------------------------------------\n\nexport function buildCompactStepFormatter(step: number): NumberFormatter {\n return (v: number) => {\n if (v === 0) return '0';\n if (!Number.isFinite(v)) return String(v);\n const abs = Math.abs(v);\n if (abs < 1000) return formatNumber(v);\n const { suffix, divisor } = unitFor(abs);\n const p = fractionDigits(step / divisor);\n if (p > 2) {\n return Number.isInteger(v) ? FORMAT_INTEGER(v) : FORMAT_DECIMAL(v);\n }\n const sign = v < 0 ? '-' : '';\n return sign + FORMAT_STEP_FRACTION[p](abs / divisor) + suffix;\n };\n}\n\n// ---------------------------------------------------------------------------\n// Date formatting\n// ---------------------------------------------------------------------------\n\n/** Granularity levels for date formatting. */\nexport type DateGranularity = 'year' | 'quarter' | 'month' | 'week' | 'day' | 'hour' | 'minute';\n\n/** Format strings for each granularity level. */\nconst GRANULARITY_FORMATS: Record<DateGranularity, string> = {\n year: '%Y',\n quarter: '', // Quarter is always special-cased in formatDate() below\n month: '%b %Y',\n week: '%b %d',\n day: '%b %d, %Y',\n hour: '%b %d %H:%M',\n minute: '%H:%M',\n};\n\n/**\n * Compact format strings used by axis ticks when full-format labels overlap.\n * Month is bare '%b': granularity inference runs per tick, so year-boundary\n * ticks already render as '2025' and the axis reads \"2025 · Feb · Mar\".\n * Year stays '%Y' — abbreviating to \"'25\" saves too little for the ambiguity.\n */\nconst GRANULARITY_FORMATS_COMPACT: Record<DateGranularity, string> = {\n year: '%Y',\n quarter: '', // special-cased below, like GRANULARITY_FORMATS\n month: '%b',\n week: '%b %d',\n day: '%b %d',\n hour: '%H:%M',\n minute: '%H:%M',\n};\n\n/**\n * Format a date value for display.\n *\n * @param value - Date object, ISO string, or timestamp number.\n * @param locale - Locale string (currently unused, reserved for i18n).\n * @param granularity - Time granularity for format selection.\n * @param useUtc - Whether to infer granularity and format using UTC methods.\n * Pass `false` when formatting ticks from a local-time scale (d3 scaleTime),\n * so that e.g. midnight local isn't misread as an intra-day UTC time.\n * Defaults to `true` for backward compatibility.\n * @param compact - Use the compact format table (axis ticks whose full-format\n * labels would overlap). Defaults to `false`.\n */\nexport function formatDate(\n value: Date | string | number,\n _locale?: string,\n granularity?: DateGranularity,\n useUtc: boolean = true,\n compact: boolean = false,\n): string {\n const date = value instanceof Date ? value : new Date(value);\n if (Number.isNaN(date.getTime())) return String(value);\n\n const gran = granularity ?? inferGranularity(date, useUtc);\n\n // Special handling for quarter (not a d3 format token)\n if (gran === 'quarter') {\n const q = useUtc\n ? Math.ceil((date.getUTCMonth() + 1) / 3)\n : Math.ceil((date.getMonth() + 1) / 3);\n const year = useUtc ? date.getUTCFullYear() : date.getFullYear();\n if (compact) {\n return `Q${q} '${String(year).slice(-2)}`;\n }\n return `Q${q} ${year}`;\n }\n\n const formatStr = (compact ? GRANULARITY_FORMATS_COMPACT : GRANULARITY_FORMATS)[gran];\n if (useUtc) {\n return getUtcFormatter(formatStr)(date);\n }\n return getTimeFormatter(formatStr)(date);\n}\n\n// Compiled d3-time-format formatters, keyed by format string. The key space\n// is bounded to the strings in GRANULARITY_FORMATS / GRANULARITY_FORMATS_COMPACT.\nconst TIME_FORMAT_CACHE = new Map<string, ReturnType<typeof timeFormat>>();\nconst UTC_FORMAT_CACHE = new Map<string, ReturnType<typeof utcFormat>>();\n\nfunction getTimeFormatter(formatStr: string): ReturnType<typeof timeFormat> {\n let fmt = TIME_FORMAT_CACHE.get(formatStr);\n if (!fmt) {\n fmt = timeFormat(formatStr);\n TIME_FORMAT_CACHE.set(formatStr, fmt);\n }\n return fmt;\n}\n\nfunction getUtcFormatter(formatStr: string): ReturnType<typeof utcFormat> {\n let fmt = UTC_FORMAT_CACHE.get(formatStr);\n if (!fmt) {\n fmt = utcFormat(formatStr);\n UTC_FORMAT_CACHE.set(formatStr, fmt);\n }\n return fmt;\n}\n\n/**\n * Build a formatter for temporal values using a d3-time-format string (e.g. \"%Y\", \"%b %Y\").\n * Returns a function that accepts a Date, string, or number and returns the formatted string.\n * Returns null if the format string is falsy.\n */\nexport function buildTemporalFormatter(\n formatStr: string | undefined,\n): ((value: Date | string | number) => string) | null {\n if (!formatStr) return null;\n const fmt = utcFormat(formatStr);\n return (value: Date | string | number) => {\n const date = value instanceof Date ? value : new Date(value);\n if (Number.isNaN(date.getTime())) return String(value);\n return fmt(date);\n };\n}\n\n/**\n * Infer the appropriate granularity from a date value.\n * If time components are all zero, assume day or higher.\n *\n * @param useUtc - When true, inspect UTC fields. When false, inspect local-time\n * fields. This must match the D3 scale type: scaleUtc -> true, scaleTime -> false.\n */\nfunction inferGranularity(date: Date, useUtc: boolean = true): DateGranularity {\n const hours = useUtc ? date.getUTCHours() : date.getHours();\n const minutes = useUtc ? date.getUTCMinutes() : date.getMinutes();\n const day = useUtc ? date.getUTCDate() : date.getDate();\n const month = useUtc ? date.getUTCMonth() : date.getMonth();\n\n if (hours !== 0 || minutes !== 0) {\n return minutes !== 0 ? 'minute' : 'hour';\n }\n if (day !== 1) return 'day';\n if (month !== 0) return 'month';\n return 'year';\n}\n","/**\n * Automatic alt text generation for chart accessibility.\n *\n * Produces human-readable descriptions of chart content for screen readers\n * and a tabular fallback for data access.\n */\n\nimport type { ChartSpec, DataRow } from '../types/spec';\nimport { MARK_DISPLAY_NAMES, resolveMarkDef, resolveMarkType } from '../types/spec';\n\n// ---------------------------------------------------------------------------\n// Alt text generation\n// ---------------------------------------------------------------------------\n\n/**\n * Generate alt text describing a chart's content.\n *\n * Produces a description like:\n * \"Line chart showing GDP Growth Rate from 2020 to 2024 with 2 series (US, UK)\"\n *\n * @param spec - The chart spec.\n * @param data - The data array.\n */\nexport function generateAltText(spec: ChartSpec, data: DataRow[]): string {\n const markType = resolveMarkType(spec.mark);\n const markDef = resolveMarkDef(spec.mark);\n let chartName = MARK_DISPLAY_NAMES[markType] ?? `${markType} chart`;\n\n // Special case: donut detection\n if (markType === 'arc' && markDef.innerRadius && markDef.innerRadius > 0) {\n chartName = 'Donut chart';\n }\n\n const parts: string[] = [chartName];\n\n // Add title context if present\n const title = spec.chrome?.title;\n if (title) {\n const titleText = typeof title === 'string' ? title : title.text;\n parts.push(`showing ${titleText}`);\n }\n\n // Waffle: name the unit framing. Per-category \"x of N units\" phrasing\n // lives on the mark aria labels; the chart-level text carries the total.\n if (markType === 'waffle') {\n const units = markDef.units ?? 100;\n parts.push(`dividing ${units} units`);\n }\n\n // Parliament: name the total seat count and the majority threshold, the two\n // numbers a reader most wants. Per-party seat counts live on the seat marks'\n // aria labels; this carries the overall framing.\n if (markType === 'parliament') {\n const valueField = spec.encoding.theta?.field ?? spec.encoding.y?.field;\n if (valueField) {\n const totalSeats = data.reduce((sum, d) => {\n const v = Number(d[valueField]);\n return sum + (Number.isFinite(v) && v > 0 ? v : 0);\n }, 0);\n if (totalSeats > 0) {\n const majority = Math.floor(totalSeats / 2) + 1;\n parts.push(`with ${totalSeats} seats, ${majority} needed for a majority`);\n }\n }\n }\n\n // Beeswarm: name the distribution fields (value axis + optional lane).\n // Skips the generic x-axis description below, which would either say\n // nothing (quantitative value axis) or repeat the lane count.\n if (markType === 'beeswarm') {\n const xIsValue = spec.encoding.x?.type === 'quantitative';\n const valueField = xIsValue ? spec.encoding.x?.field : spec.encoding.y?.field;\n const laneField = xIsValue ? spec.encoding.y?.field : spec.encoding.x?.field;\n if (valueField) {\n parts.push(\n `plotting the distribution of ${valueField}${laneField ? ` by ${laneField}` : ''}`,\n );\n }\n }\n\n // Describe the data range\n if (spec.encoding.x && markType !== 'beeswarm' && data.length > 0) {\n const field = spec.encoding.x.field;\n const values = data.map((d) => d[field]).filter((v) => v != null);\n\n if (values.length > 0) {\n if (spec.encoding.x.type === 'temporal') {\n const dates = values.map((v) => (v instanceof Date ? v : new Date(String(v))));\n const validDates = dates.filter((d) => !Number.isNaN(d.getTime()));\n if (validDates.length >= 2) {\n validDates.sort((a, b) => a.getTime() - b.getTime());\n const first = validDates[0].getUTCFullYear();\n const last = validDates[validDates.length - 1].getUTCFullYear();\n if (first !== last) {\n parts.push(`from ${first} to ${last}`);\n }\n }\n } else if (spec.encoding.x.type === 'nominal' || spec.encoding.x.type === 'ordinal') {\n const uniqueValues = [...new Set(values.map(String))];\n parts.push(`across ${uniqueValues.length} categories`);\n }\n }\n }\n\n // Describe series if color encoding is present\n if (spec.encoding.color && 'field' in spec.encoding.color && data.length > 0) {\n const colorField = spec.encoding.color.field;\n const uniqueSeries = [...new Set(data.map((d) => String(d[colorField])).filter(Boolean))];\n if (uniqueSeries.length > 0) {\n parts.push(`with ${uniqueSeries.length} series (${uniqueSeries.join(', ')})`);\n }\n }\n\n // Add data point count\n parts.push(`(${data.length} data points)`);\n\n return parts.join(' ');\n}\n\n// ---------------------------------------------------------------------------\n// Data table fallback\n// ---------------------------------------------------------------------------\n\n/**\n * Maximum number of data rows in the screen-reader table. A 50k-point scatter\n * would otherwise put 50k hidden rows in the DOM.\n */\nexport const MAX_SR_TABLE_ROWS = 1000;\n\n/**\n * Generate a tabular data fallback for screen readers.\n *\n * Returns a 2D array where the first row is headers and subsequent\n * rows are data values. Only includes fields referenced in the encoding.\n *\n * Data rows are capped at MAX_SR_TABLE_ROWS (the header row does not count\n * against the cap). SCOPE: this caps the chart compile path only. Graph,\n * sankey, tilemap and map build their `dataTableFallback` independently and\n * stay uncapped.\n *\n * @param spec - The chart spec.\n * @param data - The data array.\n */\nexport function generateDataTable(spec: ChartSpec, data: DataRow[]): unknown[][] {\n // Collect fields from encoding\n const fields: string[] = [];\n const encoding = spec.encoding;\n\n if (encoding.x) fields.push(encoding.x.field);\n if (encoding.y) fields.push(encoding.y.field);\n if (encoding.color && 'field' in encoding.color) fields.push(encoding.color.field);\n if (encoding.size && 'field' in encoding.size) fields.push(encoding.size.field);\n\n // Deduplicate\n const uniqueFields = [...new Set(fields)];\n\n if (uniqueFields.length === 0) return [];\n\n // Header row\n const headers = uniqueFields;\n\n // Data rows\n const rows = data\n .slice(0, MAX_SR_TABLE_ROWS)\n .map((row) => uniqueFields.map((field) => row[field] ?? ''));\n\n return [headers, ...rows];\n}\n","/**\n * ARIA label generation for individual marks.\n *\n * Produces per-mark labels for screen reader navigation, enabling\n * users to explore data points individually.\n */\n\nimport { defaultNumberFormatter } from '../locale/format';\nimport type { Mark } from '../types/layout';\n\n/**\n * Generate ARIA labels for a set of marks.\n *\n * Returns a Map keyed by a mark identifier (index-based) with\n * descriptive labels like \"Data point: US GDP 42 in 2020-01\".\n *\n * @param marks - Array of mark objects from the compiled chart layout.\n */\nexport function generateAriaLabels(marks: Mark[]): Map<string, string> {\n const labels = new Map<string, string>();\n\n for (let i = 0; i < marks.length; i++) {\n const mark = marks[i];\n const key = `mark-${i}`;\n\n switch (mark.type) {\n case 'line': {\n const series = mark.seriesKey ?? 'Series';\n const pointCount = mark.points.length;\n labels.set(key, `Line series: ${series} with ${pointCount} points`);\n break;\n }\n\n case 'area': {\n const series = mark.seriesKey ?? 'Area';\n labels.set(key, `Area series: ${series}`);\n break;\n }\n\n case 'rect': {\n const dataEntries = Object.entries(mark.data).filter(([k]) => !k.startsWith('_'));\n const description = dataEntries.map(([k, v]) => `${k}: ${formatValue(v)}`).join(', ');\n labels.set(key, `Data point: ${description}`);\n break;\n }\n\n case 'arc': {\n const dataEntries = Object.entries(mark.data).filter(([k]) => !k.startsWith('_'));\n const description = dataEntries.map(([k, v]) => `${k}: ${formatValue(v)}`).join(', ');\n labels.set(key, `Slice: ${description}`);\n break;\n }\n\n case 'point': {\n const dataEntries = Object.entries(mark.data).filter(([k]) => !k.startsWith('_'));\n const description = dataEntries.map(([k, v]) => `${k}: ${formatValue(v)}`).join(', ');\n labels.set(key, `Data point: ${description}`);\n break;\n }\n }\n }\n\n return labels;\n}\n\nconst ariaNumberFormatter = defaultNumberFormatter();\n\nfunction formatValue(value: unknown): string {\n if (value == null) return 'N/A';\n if (value instanceof Date) return value.toISOString().slice(0, 10);\n if (typeof value === 'number') {\n return ariaNumberFormatter(value);\n }\n return String(value);\n}\n","/**\n * Spec construction helpers: typed builder functions for common chart types.\n *\n * These builders reduce boilerplate when creating specs programmatically.\n * They accept field names as strings (simple case) or full EncodingChannel\n * objects (when you need to customize type, aggregate, axis, or scale).\n *\n * Type inference: when a string field name is provided, the builder samples\n * data values to infer the encoding type (quantitative, temporal, nominal).\n */\n\nimport type {\n AnimationSpec,\n Annotation,\n ChartSpec,\n Chrome,\n DarkMode,\n DataRow,\n Encoding,\n EncodingChannel,\n FieldType,\n LegendConfig,\n MarkDef,\n MarkType,\n TableSpec,\n ThemeConfig,\n TileMapEncoding,\n TileMapPalette,\n TileMapSpec,\n} from '../types/spec';\nimport type { ColumnConfig } from '../types/table';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** A field reference: either a plain string (field name) or a full EncodingChannel. */\nexport type FieldRef = string | EncodingChannel;\n\n/** Common options shared by all chart builder functions. */\nexport interface ChartBuilderOptions {\n /** Color encoding: field name or full channel config for series differentiation. */\n color?: FieldRef;\n /** Size encoding: field name or full channel config. */\n size?: FieldRef;\n /** Editorial chrome (title, subtitle, source, etc.). */\n chrome?: Chrome;\n /** Data annotations. */\n annotations?: Annotation[];\n /** Whether the chart adapts to container width. Defaults to true. */\n responsive?: boolean;\n /** Theme configuration overrides. */\n theme?: ThemeConfig;\n /** Dark mode behavior. */\n darkMode?: DarkMode;\n}\n\n/** Options for the dataTable builder. */\nexport interface TableBuilderOptions {\n /** Column definitions. Auto-generated from data keys if omitted. */\n columns?: ColumnConfig[];\n /** Field to use as a unique row identifier. */\n rowKey?: string;\n /** Editorial chrome. */\n chrome?: Chrome;\n /** Theme configuration overrides. */\n theme?: ThemeConfig;\n /** Dark mode behavior. */\n darkMode?: DarkMode;\n /** Enable client-side search/filter. */\n search?: boolean;\n /** Pagination configuration. */\n pagination?: boolean | { pageSize: number };\n /** Stick the first column during horizontal scroll. */\n stickyFirstColumn?: boolean;\n /** Compact mode. */\n compact?: boolean;\n /** Whether the table adapts to container width. */\n responsive?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Type inference\n// ---------------------------------------------------------------------------\n\n/** ISO 8601 date patterns we recognize for temporal inference. */\nconst ISO_DATE_RE = /^\\d{4}(-\\d{2}(-\\d{2}(T\\d{2}(:\\d{2}(:\\d{2})?)?)?)?)?$/;\n\n/**\n * Infer the encoding field type from data values.\n *\n * Samples up to 20 values from the data array for the given field.\n * - If all sampled values are numbers (or null/undefined), returns \"quantitative\".\n * - If all sampled string values match ISO date patterns, returns \"temporal\".\n * - Otherwise returns \"nominal\".\n */\nexport function inferFieldType(data: DataRow[], field: string): FieldType {\n const sampleSize = Math.min(data.length, 20);\n let hasNumber = false;\n let hasDateString = false;\n let hasOtherString = false;\n\n for (let i = 0; i < sampleSize; i++) {\n const value = data[i][field];\n\n // Skip null/undefined\n if (value == null) continue;\n\n if (typeof value === 'number') {\n hasNumber = true;\n } else if (typeof value === 'string') {\n if (ISO_DATE_RE.test(value) && !Number.isNaN(Date.parse(value))) {\n hasDateString = true;\n } else {\n hasOtherString = true;\n }\n } else if (value instanceof Date) {\n hasDateString = true;\n } else {\n hasOtherString = true;\n }\n }\n\n // Numbers-only: quantitative\n if (hasNumber && !hasDateString && !hasOtherString) return 'quantitative';\n // Date strings/Date objects only: temporal\n if (hasDateString && !hasNumber && !hasOtherString) return 'temporal';\n // Everything else: nominal\n return 'nominal';\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve a FieldRef into a full EncodingChannel.\n * If the ref is a string, infer the type from data.\n */\nfunction resolveField(ref: FieldRef, data: DataRow[]): EncodingChannel {\n if (typeof ref === 'string') {\n return { field: ref, type: inferFieldType(data, ref) };\n }\n return ref;\n}\n\n/** Build the encoding object from resolved channels. */\nfunction buildEncoding(\n channels: { x?: EncodingChannel; y?: EncodingChannel },\n options?: ChartBuilderOptions,\n data?: DataRow[],\n): Encoding {\n const encoding: Encoding = { ...channels };\n if (options?.color && data) {\n encoding.color = resolveField(options.color, data);\n }\n if (options?.size && data) {\n encoding.size = resolveField(options.size, data);\n }\n return encoding;\n}\n\n/** Build a ChartSpec from the resolved pieces. */\nfunction buildChartSpec(\n mark: MarkType | MarkDef,\n data: DataRow[],\n encoding: Encoding,\n options?: ChartBuilderOptions,\n): ChartSpec {\n // Cast needed because the generic discriminated union can't be constructed\n // from a runtime MarkType variable — the caller always pairs mark+encoding correctly.\n const spec = { mark, data, encoding } as ChartSpec;\n if (options?.chrome) spec.chrome = options.chrome;\n if (options?.annotations) spec.annotations = options.annotations;\n if (options?.responsive !== undefined) spec.responsive = options.responsive;\n if (options?.theme) spec.theme = options.theme;\n if (options?.darkMode) spec.darkMode = options.darkMode;\n return spec;\n}\n\n// ---------------------------------------------------------------------------\n// Builder functions\n// ---------------------------------------------------------------------------\n\n/**\n * Create a line chart spec.\n *\n * @param data - Array of data rows.\n * @param x - X-axis field (typically temporal or ordinal).\n * @param y - Y-axis field (typically quantitative).\n * @param options - Optional color, size, chrome, annotations, theme, etc.\n */\nexport function lineChart(\n data: DataRow[],\n x: FieldRef,\n y: FieldRef,\n options?: ChartBuilderOptions,\n): ChartSpec {\n const xChannel = resolveField(x, data);\n const yChannel = resolveField(y, data);\n const encoding = buildEncoding({ x: xChannel, y: yChannel }, options, data);\n return buildChartSpec('line', data, encoding, options);\n}\n\n/**\n * Create a bar chart spec (horizontal bars).\n *\n * Convention: category goes on y-axis, value on x-axis.\n * The `category` param maps to y, `value` maps to x.\n *\n * @param data - Array of data rows.\n * @param category - Category field (y-axis, nominal/ordinal).\n * @param value - Value field (x-axis, quantitative).\n * @param options - Optional color, size, chrome, annotations, theme, etc.\n */\nexport function barChart(\n data: DataRow[],\n category: FieldRef,\n value: FieldRef,\n options?: ChartBuilderOptions,\n): ChartSpec {\n const categoryChannel = resolveField(category, data);\n const valueChannel = resolveField(value, data);\n\n // Bar charts: category on y, value on x\n const encoding = buildEncoding({ x: valueChannel, y: categoryChannel }, options, data);\n return buildChartSpec('bar', data, encoding, options);\n}\n\n/**\n * Create a column chart spec (vertical columns).\n *\n * @param data - Array of data rows.\n * @param x - X-axis field (category or temporal).\n * @param y - Y-axis field (quantitative value).\n * @param options - Optional color, size, chrome, annotations, theme, etc.\n */\nexport function columnChart(\n data: DataRow[],\n x: FieldRef,\n y: FieldRef,\n options?: ChartBuilderOptions,\n): ChartSpec {\n const xChannel = resolveField(x, data);\n const yChannel = resolveField(y, data);\n const encoding = buildEncoding({ x: xChannel, y: yChannel }, options, data);\n return buildChartSpec({ type: 'bar' } as MarkDef, data, encoding, options);\n}\n\n/**\n * Create a pie chart spec.\n *\n * Convention: category maps to the color channel, value maps to theta.\n * Pie charts have no x-axis.\n *\n * @param data - Array of data rows.\n * @param category - Category field (color channel, nominal).\n * @param value - Value field (theta channel, quantitative).\n * @param options - Optional chrome, annotations, theme, etc.\n * Note: color option is ignored since category is used for color.\n */\nexport function pieChart(\n data: DataRow[],\n category: FieldRef,\n value: FieldRef,\n options?: ChartBuilderOptions,\n): ChartSpec {\n const categoryChannel = resolveField(category, data);\n const valueChannel = resolveField(value, data);\n\n // Pie charts: value on theta (canonical in v8), category on color (no x-axis)\n const encoding: Encoding = {\n theta: valueChannel,\n color: categoryChannel,\n };\n if (options?.size && data) {\n encoding.size = resolveField(options.size, data);\n }\n\n return buildChartSpec('arc', data, encoding, options);\n}\n\n/**\n * Create an area chart spec.\n *\n * @param data - Array of data rows.\n * @param x - X-axis field (typically temporal or ordinal).\n * @param y - Y-axis field (typically quantitative).\n * @param options - Optional color, size, chrome, annotations, theme, etc.\n */\nexport function areaChart(\n data: DataRow[],\n x: FieldRef,\n y: FieldRef,\n options?: ChartBuilderOptions,\n): ChartSpec {\n const xChannel = resolveField(x, data);\n const yChannel = resolveField(y, data);\n const encoding = buildEncoding({ x: xChannel, y: yChannel }, options, data);\n return buildChartSpec('area', data, encoding, options);\n}\n\n/**\n * Create a donut chart spec.\n *\n * Convention: category maps to the color channel, value maps to theta.\n * Donut charts have no x-axis.\n *\n * @param data - Array of data rows.\n * @param category - Category field (color channel, nominal).\n * @param value - Value field (theta channel, quantitative).\n * @param options - Optional chrome, annotations, theme, etc.\n * Note: color option is ignored since category is used for color.\n */\nexport function donutChart(\n data: DataRow[],\n category: FieldRef,\n value: FieldRef,\n options?: ChartBuilderOptions,\n): ChartSpec {\n const categoryChannel = resolveField(category, data);\n const valueChannel = resolveField(value, data);\n\n const encoding: Encoding = {\n theta: valueChannel,\n color: categoryChannel,\n };\n if (options?.size && data) {\n encoding.size = resolveField(options.size, data);\n }\n\n return buildChartSpec({ type: 'arc', innerRadius: 40 } as MarkDef, data, encoding, options);\n}\n\n/**\n * Create a dot chart spec (strip plot / dot plot).\n *\n * @param data - Array of data rows.\n * @param x - X-axis field (quantitative or temporal).\n * @param y - Y-axis field (nominal/categorical grouping).\n * @param options - Optional color, size, chrome, annotations, theme, etc.\n */\nexport function dotChart(\n data: DataRow[],\n x: FieldRef,\n y: FieldRef,\n options?: ChartBuilderOptions,\n): ChartSpec {\n const xChannel = resolveField(x, data);\n const yChannel = resolveField(y, data);\n const encoding = buildEncoding({ x: xChannel, y: yChannel }, options, data);\n return buildChartSpec('circle', data, encoding, options);\n}\n\n/**\n * Create a scatter chart spec.\n *\n * @param data - Array of data rows.\n * @param x - X-axis field (quantitative).\n * @param y - Y-axis field (quantitative).\n * @param options - Optional color, size, chrome, annotations, theme, etc.\n */\nexport function scatterChart(\n data: DataRow[],\n x: FieldRef,\n y: FieldRef,\n options?: ChartBuilderOptions,\n): ChartSpec {\n const xChannel = resolveField(x, data);\n const yChannel = resolveField(y, data);\n const encoding = buildEncoding({ x: xChannel, y: yChannel }, options, data);\n return buildChartSpec('point', data, encoding, options);\n}\n\n/**\n * Create a data table spec.\n *\n * If no columns are specified, auto-generates column configs from the\n * keys of the first data row.\n *\n * @param data - Array of data rows.\n * @param options - Column definitions, chrome, pagination, search, etc.\n */\nexport function dataTable(data: DataRow[], options?: TableBuilderOptions): TableSpec {\n // Auto-generate columns from data keys if not provided\n let columns = options?.columns;\n if (!columns && data.length > 0) {\n columns = Object.keys(data[0]).map((key): ColumnConfig => {\n // Infer alignment from data type\n const fieldType = inferFieldType(data, key);\n const align = fieldType === 'quantitative' ? ('right' as const) : ('left' as const);\n\n return {\n key,\n label: key,\n align,\n };\n });\n }\n\n const spec: TableSpec = {\n type: 'table',\n data,\n columns: columns ?? [],\n };\n\n if (options?.rowKey) spec.rowKey = options.rowKey;\n if (options?.chrome) spec.chrome = options.chrome;\n if (options?.theme) spec.theme = options.theme;\n if (options?.darkMode) spec.darkMode = options.darkMode;\n if (options?.search !== undefined) spec.search = options.search;\n if (options?.pagination !== undefined) spec.pagination = options.pagination;\n if (options?.stickyFirstColumn !== undefined) spec.stickyFirstColumn = options.stickyFirstColumn;\n if (options?.compact !== undefined) spec.compact = options.compact;\n if (options?.responsive !== undefined) spec.responsive = options.responsive;\n\n return spec;\n}\n\n// ---------------------------------------------------------------------------\n// Tile map builder\n// ---------------------------------------------------------------------------\n\n/** Options for the tileMap convenience builder. */\nexport interface TileMapBuilderOptions {\n /** Encoding channels (required when data is DataRow[]). */\n encoding?: TileMapEncoding;\n /** Sequential color palette name (quantitative mode only). */\n palette?: TileMapPalette;\n /** Custom category-to-color mapping. Forces categorical mode. */\n colors?: Record<string, string>;\n /** Editorial chrome. */\n chrome?: Chrome;\n /** Legend display configuration. */\n legend?: LegendConfig;\n /** Theme overrides. */\n theme?: ThemeConfig;\n /** Dark mode setting. */\n darkMode?: DarkMode;\n /** Whether to show the watermark. */\n watermark?: boolean;\n /** Animation configuration. */\n animation?: AnimationSpec;\n /** d3-format string for value formatting. */\n valueFormat?: string;\n}\n\n/**\n * Build a TileMapSpec from a state-to-value record or tabular data.\n *\n * @example Record map (simplest):\n * ```ts\n * tileMap({ CA: 4.4, TX: 17.6, NY: 4.5 })\n * ```\n *\n * @example Tabular data with encoding:\n * ```ts\n * tileMap(data, { encoding: { state: { field: 'code' }, value: { field: 'rate' } } })\n * ```\n */\nexport function tileMap(\n data: Record<string, number | string | null> | DataRow[],\n options?: TileMapBuilderOptions,\n): TileMapSpec {\n return {\n type: 'tilemap' as const,\n data,\n ...(options?.encoding && { encoding: options.encoding }),\n ...(options?.palette && { palette: options.palette }),\n ...(options?.colors && { colors: options.colors }),\n ...(options?.chrome && { chrome: options.chrome }),\n ...(options?.legend && { legend: options.legend }),\n ...(options?.theme && { theme: options.theme }),\n ...(options?.darkMode && { darkMode: options.darkMode }),\n ...(options?.watermark !== undefined && { watermark: options.watermark }),\n ...(options?.animation && { animation: options.animation }),\n ...(options?.valueFormat && { valueFormat: options.valueFormat }),\n };\n}\n","/**\n * Deep merge for applying partial patches onto a spec-shaped object.\n *\n * Used by the scrollytelling story layer (`vanilla/story`) to apply\n * cumulative deep-partial spec patches onto a base spec. Arrays REPLACE\n * rather than merge element-by-element: predictable behavior for things\n * like `annotations` or `data`, where index-wise merging would silently\n * produce nonsense. Plain objects merge key-by-key, recursively.\n */\n// biome-ignore lint/suspicious/noExplicitAny: recursive object merge requires dynamic typing\nexport function deepMergeSpec(target: any, source: any): any {\n if (source === undefined) return target;\n if (source === null) return source;\n if (\n typeof source !== 'object' ||\n Array.isArray(source) ||\n typeof target !== 'object' ||\n target === null ||\n Array.isArray(target)\n ) {\n return source;\n }\n\n const result = { ...target };\n for (const key of Object.keys(source)) {\n result[key] = deepMergeSpec(target[key], source[key]);\n }\n return result;\n}\n"],"mappings":"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AA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,GAAG;AAAA,EACH,KAAK;AAAA,EACL,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AA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","names":["rgb","rgb","rgb","relativeLuminance","mix","words","lines","current","words","lines","current"]}
|
|
1
|
+
{"version":3,"sources":["../src/types/encoding.ts","../src/types/events.ts","../src/types/spec.ts","../src/colors/colorblind.ts","../src/colors/contrast.ts","../src/styles/token-definitions.ts","../src/colors/neutral.ts","../src/colors/palettes.ts","../src/theme/dark-mode.ts","../src/layout/text-measure.ts","../src/responsive/breakpoints.ts","../src/responsive/metrics.ts","../src/theme/defaults.ts","../src/theme/presets.ts","../src/theme/resolve.ts","../src/layout/chrome.ts","../src/layout/text-wrap.ts","../src/labels/collision.ts","../src/locale/format.ts","../src/accessibility/alt-text.ts","../src/accessibility/aria.ts","../src/helpers/spec-builders.ts","../src/helpers/deep-merge.ts"],"sourcesContent":["/**\n * Per-mark-type encoding validation rules.\n *\n * Defines which encoding channels are required vs optional for each mark type.\n * The engine compiler uses these rules to validate specs at runtime (TypeScript\n * catches compile-time errors; these catch runtime JSON from Claude or APIs).\n */\n\nimport type { FieldType, MarkType } from './spec';\n\n// ---------------------------------------------------------------------------\n// Encoding rule types\n// ---------------------------------------------------------------------------\n\n/** Constraint on what field types are valid for an encoding channel. */\nexport interface ChannelRule {\n /** Whether this channel is required for the chart type. */\n required: boolean;\n /** Allowed field types. If empty, any field type is accepted. */\n allowedTypes: FieldType[];\n}\n\n/** Encoding rules for a single chart type: which channels are required/optional. */\nexport interface EncodingRule {\n x: ChannelRule;\n y: ChannelRule;\n x2?: ChannelRule;\n y2?: ChannelRule;\n color: ChannelRule;\n size: ChannelRule;\n opacity?: ChannelRule;\n strokeDash?: ChannelRule;\n text?: ChannelRule;\n tooltip?: ChannelRule;\n theta?: ChannelRule;\n detail: ChannelRule;\n}\n\n// ---------------------------------------------------------------------------\n// Mark encoding rules\n// ---------------------------------------------------------------------------\n\n/** Helper to create a required channel rule. */\nfunction required(...types: FieldType[]): ChannelRule {\n return { required: true, allowedTypes: types };\n}\n\n/** Helper to create an optional channel rule. */\nfunction optional(...types: FieldType[]): ChannelRule {\n return { required: false, allowedTypes: types };\n}\n\n/**\n * Encoding rules per mark type.\n *\n * Defines which channels are required and what field types they accept.\n * The compiler uses this map to validate user specs at runtime.\n *\n * Key design decisions:\n * - line/area: x is temporal/ordinal (the axis), y is quantitative (the value)\n * - bar: covers both horizontal and vertical orientations, so both axes accept\n * all relevant types. The engine validates the combination later (one axis\n * must be quantitative).\n * - arc: no x axis; y is the value (quantitative), color is the category\n * - circle: y is the category, x is quantitative\n * - point: both axes are quantitative\n * - text/rule: fully optional positioning\n * - tick/rect: both axes required, any type\n * - beeswarm: both axes optional at the channel level; the compiler enforces\n * the combination (exactly one quantitative value axis, the other axis an\n * optional nominal/ordinal lane channel)\n * - waffle: no positional axes; y is the share value (quantitative, with\n * theta accepted as an alias like arc), color is the category\n * - calendar: x is the daily date (temporal), color the per-day value\n * (quantitative); no y channel (the calendar owns its own geometry)\n * - parliament: no positional axes; theta (aliased to y) is the seat count\n * (quantitative), color the party\n */\nexport const MARK_ENCODING_RULES: Record<MarkType, EncodingRule> = {\n bar: {\n x: required('quantitative', 'nominal', 'ordinal', 'temporal'),\n y: required('quantitative', 'nominal', 'ordinal'),\n x2: optional('quantitative'),\n y2: optional('quantitative'),\n color: optional('nominal', 'ordinal', 'quantitative'),\n size: optional(),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n line: {\n x: required('temporal', 'ordinal'),\n y: required('quantitative'),\n color: optional('nominal', 'ordinal', 'quantitative'),\n size: optional(),\n opacity: optional('quantitative'),\n strokeDash: optional('nominal', 'ordinal'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n area: {\n // Quantitative x is what a density curve needs: the x values are computed\n // evaluation points, not categories or dates.\n x: required('temporal', 'ordinal', 'quantitative'),\n y: required('quantitative'),\n y2: optional('quantitative'),\n color: optional('nominal', 'ordinal', 'quantitative'),\n size: optional(),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n point: {\n x: required('quantitative', 'temporal', 'nominal', 'ordinal'),\n y: required('quantitative', 'temporal', 'nominal', 'ordinal'),\n color: optional('nominal', 'ordinal', 'quantitative'),\n size: optional('quantitative'),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n circle: {\n x: required('quantitative'),\n y: required('nominal', 'ordinal'),\n color: optional('nominal', 'ordinal', 'quantitative'),\n size: optional('quantitative'),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n lollipop: {\n x: required('quantitative'),\n y: required('nominal', 'ordinal'),\n color: optional('nominal', 'ordinal', 'quantitative'),\n size: optional('quantitative'),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n beeswarm: {\n x: optional('quantitative', 'nominal', 'ordinal'),\n y: optional('quantitative', 'nominal', 'ordinal'),\n color: optional('nominal', 'ordinal', 'quantitative'),\n size: optional('quantitative'),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n // Range: x+x2 with nominal y (horizontal) or y+y2 with nominal x (vertical).\n // The orientation-dependent x2/y2 requirement is enforced by a dedicated\n // check in the validator; the static table can only mark them optional.\n range: {\n x: required('quantitative', 'nominal', 'ordinal'),\n y: required('quantitative', 'nominal', 'ordinal'),\n x2: optional('quantitative'),\n y2: optional('quantitative'),\n color: optional('nominal', 'ordinal'),\n size: optional(),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n arc: {\n x: optional(),\n y: optional('quantitative'),\n theta: required('quantitative'),\n color: required('nominal', 'ordinal'),\n size: optional(),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n waffle: {\n x: optional(),\n y: required('quantitative'),\n color: required('nominal', 'ordinal'),\n size: optional(),\n opacity: optional('quantitative'),\n tooltip: optional(),\n theta: optional('quantitative'),\n detail: optional('nominal'),\n },\n // parliament: hemicycle seat chart. Like arc/waffle, no positional axes;\n // color is the party (required), theta the seat count (aliased to y in the\n // sugar pass). y is accepted so the theta alias resolves before the rules\n // check runs.\n parliament: {\n x: optional(),\n y: required('quantitative'),\n color: required('nominal', 'ordinal'),\n size: optional(),\n opacity: optional('quantitative'),\n tooltip: optional(),\n theta: optional('quantitative'),\n detail: optional('nominal'),\n },\n text: {\n x: optional(),\n y: optional(),\n color: optional(),\n size: optional('quantitative'),\n opacity: optional('quantitative'),\n text: required(),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n rule: {\n x: optional(),\n y: optional(),\n x2: optional(),\n y2: optional(),\n color: optional(),\n size: optional(),\n opacity: optional('quantitative'),\n strokeDash: optional('nominal', 'ordinal'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n tick: {\n x: required(),\n y: required(),\n color: optional(),\n size: optional(),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n rect: {\n x: required(),\n y: required(),\n x2: optional(),\n y2: optional(),\n color: optional(),\n size: optional(),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n // calendar: x is the daily date (temporal), color the per-day value\n // (quantitative). No y channel: the calendar computes its own\n // weeks-x-weekdays geometry, so y is left optional here and the dedicated\n // validator rejects it with a pointed message.\n calendar: {\n x: required('temporal'),\n y: optional(),\n color: required('quantitative'),\n size: optional(),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n // histogram: x is the raw quantitative field to bin. y is optional because\n // it defaults to the bin count; supply it only to plot something else per\n // bin. A color field groups the distributions and they overlap rather than\n // stack, which is the whole reason this mark exists.\n histogram: {\n x: required('quantitative'),\n y: optional('quantitative'),\n color: optional('nominal', 'ordinal'),\n size: optional(),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n // density: x is the raw quantitative field to estimate over. y is computed\n // by the density transform, never authored, so it stays optional. A color\n // field yields one translucent curve per group.\n density: {\n x: required('quantitative'),\n y: optional('quantitative'),\n color: optional('nominal', 'ordinal'),\n size: optional(),\n opacity: optional('quantitative'),\n tooltip: optional(),\n detail: optional('nominal'),\n },\n};\n\n// ---------------------------------------------------------------------------\n// Graph encoding rules\n// ---------------------------------------------------------------------------\n\n/** Encoding rule for a single graph visual channel. */\nexport interface GraphChannelRule {\n /** Whether this channel is required. */\n required: boolean;\n /** Allowed field types. Empty means any type. */\n allowedTypes: FieldType[];\n}\n\n/**\n * Encoding rules for graph visualizations.\n *\n * All graph encoding channels are optional since a graph can be rendered\n * with just nodes and edges (uniform appearance). Encoding channels add\n * visual differentiation based on data fields.\n */\nexport const GRAPH_ENCODING_RULES: Record<string, GraphChannelRule> = {\n nodeColor: { required: false, allowedTypes: ['nominal', 'ordinal'] },\n nodeSize: { required: false, allowedTypes: ['quantitative'] },\n edgeColor: { required: false, allowedTypes: ['nominal', 'ordinal'] },\n edgeWidth: { required: false, allowedTypes: ['quantitative'] },\n nodeLabel: { required: false, allowedTypes: [] },\n};\n","/**\n * Chart interaction event types.\n *\n * These types define the callback signatures for user interactions with\n * chart elements: clicking marks, hovering, legend toggles, and annotation clicks.\n *\n * Event handlers are optional and passed through MountOptions (vanilla) or\n * ChartProps (React). The vanilla adapter wires DOM event listeners to\n * mark elements and constructs these typed events from the raw browser events.\n */\n\nimport type {\n Annotation,\n AnnotationOffset,\n DataRow,\n RangeAnnotation,\n RefLineAnnotation,\n TextAnnotation,\n} from './spec';\n\n// ---------------------------------------------------------------------------\n// Chrome key identifiers\n// ---------------------------------------------------------------------------\n\n/** Identifies a specific chrome text element (title, subtitle, source, byline, footer). */\nexport type ChromeKey = 'title' | 'subtitle' | 'source' | 'byline' | 'footer';\n\n// ---------------------------------------------------------------------------\n// Element references (identity for selection/edit callbacks)\n// ---------------------------------------------------------------------------\n\n/**\n * Reference to an editable chart element.\n * Carries enough info to find the element in both the spec and the DOM.\n * The `annotation` variant uses two-tier identity: `id` (when the consumer provides one)\n * and `index` (always available, position in the spec's annotations array).\n */\nexport type ElementRef =\n | {\n type: 'annotation';\n index: number;\n id?: string;\n /** RFC 6901 JSON Pointer to the edited property in the spec. */\n path?: string;\n }\n | {\n type: 'chrome';\n key: ChromeKey;\n /** RFC 6901 JSON Pointer to the edited property in the spec. */\n path?: string;\n }\n | {\n type: 'series-label';\n series: string;\n /** RFC 6901 JSON Pointer to the edited property in the spec. */\n path?: string;\n }\n | {\n type: 'legend';\n /** RFC 6901 JSON Pointer to the edited property in the spec. */\n path?: string;\n }\n | {\n type: 'legend-entry';\n series: string;\n index: number;\n /** RFC 6901 JSON Pointer to the edited property in the spec. */\n path?: string;\n };\n\n/** Helper constructors for ergonomic ElementRef creation. */\nexport const elementRef = {\n annotation: (index: number, id?: string, path?: string): ElementRef => ({\n type: 'annotation',\n index,\n ...(id !== undefined ? { id } : {}),\n ...(path !== undefined ? { path } : {}),\n }),\n chrome: (key: ChromeKey, path?: string): ElementRef => ({\n type: 'chrome',\n key,\n ...(path !== undefined ? { path } : {}),\n }),\n seriesLabel: (series: string, path?: string): ElementRef => ({\n type: 'series-label',\n series,\n ...(path !== undefined ? { path } : {}),\n }),\n legend: (path?: string): ElementRef => ({\n type: 'legend',\n ...(path !== undefined ? { path } : {}),\n }),\n legendEntry: (series: string, index: number, path?: string): ElementRef => ({\n type: 'legend-entry',\n series,\n index,\n ...(path !== undefined ? { path } : {}),\n }),\n} as const;\n\n// ---------------------------------------------------------------------------\n// Element edit events\n// ---------------------------------------------------------------------------\n\n/**\n * Discriminated union of all element edit events.\n * Fired by the `onEdit` callback when any editable chart element is modified.\n * Covers repositioning (drag), deletion (Delete key), and text editing (double-click).\n * Selection events (onSelect/onDeselect) are separate callbacks, not part of this union.\n *\n * For the `annotation` variant:\n * - `annotation` is the ORIGINAL spec annotation (may have a stale or missing offset)\n * - `offset` is the NEW accumulated position after the drag completes\n * - To persist a drag edit back into your spec: `{ ...edit.annotation, offset: edit.offset }`\n */\nexport type ElementEdit =\n | {\n type: 'annotation';\n element: ElementRef;\n annotation: TextAnnotation;\n offset: AnnotationOffset;\n }\n | {\n type: 'annotation-connector';\n element: ElementRef;\n annotation: TextAnnotation;\n endpoint: 'from' | 'to';\n offset: AnnotationOffset;\n }\n | {\n type: 'range-label';\n element: ElementRef;\n annotation: RangeAnnotation;\n labelOffset: AnnotationOffset;\n }\n | {\n type: 'refline-label';\n element: ElementRef;\n annotation: RefLineAnnotation;\n labelOffset: AnnotationOffset;\n }\n | {\n type: 'annotation-anchor';\n element: ElementRef;\n annotation: TextAnnotation;\n x: string | number;\n y: string | number;\n }\n | { type: 'chrome'; key: ChromeKey; text: string; offset: AnnotationOffset }\n | { type: 'series-label'; series: string; offset: AnnotationOffset }\n | { type: 'legend'; offset: AnnotationOffset }\n /**\n * Transient legend visibility toggle. Reports the same runtime show/hide\n * a reader triggers; not persisted by openchart. To author default-hidden\n * series, set `spec.hiddenSeries` directly. Hosts building spec\n * persistence should generally not write legend-toggle edits back into\n * the authored spec. `hidden: true` means the series was just hidden.\n */\n | { type: 'legend-toggle'; series: string; hidden: boolean }\n | { type: 'add'; annotation: TextAnnotation }\n | { type: 'delete'; element: ElementRef }\n | { type: 'text-edit'; element: ElementRef; oldText: string; newText: string };\n\n// ---------------------------------------------------------------------------\n// Mark events\n// ---------------------------------------------------------------------------\n\n/**\n * Event fired when a user interacts with a data mark (bar, point, line segment, etc.).\n *\n * Contains the underlying data row, the series it belongs to (if multi-series),\n * the position within the chart container, and the raw browser MouseEvent.\n */\nexport interface MarkEvent {\n /** The data row associated with the mark that was interacted with. */\n datum: DataRow;\n /** Series identifier, if the chart has multiple series (e.g. multi-line). */\n series?: string;\n /** Position of the interaction relative to the chart container. */\n position: { x: number; y: number };\n /** The raw browser MouseEvent. */\n event: MouseEvent;\n}\n\n// ---------------------------------------------------------------------------\n// Chart event handler interface\n// ---------------------------------------------------------------------------\n\n/**\n * Event handler callbacks for chart interactions.\n *\n * All handlers are optional. Pass these through MountOptions (vanilla adapter)\n * or ChartProps (React component) to receive interaction callbacks.\n */\nexport interface ChartEventHandlers {\n /**\n * Explicitly enable or disable edit interactions (drag, select, keyboard\n * delete/edit). When true, the interaction layer is wired even without\n * onEdit. When false, all edit interactions are suppressed even when\n * onEdit is provided. When omitted, each interaction block uses its\n * existing callback-presence gate.\n */\n editable?: boolean;\n /** Called when a data mark is clicked. */\n onMarkClick?: (event: MarkEvent) => void;\n /** Called when the mouse enters a data mark. */\n onMarkHover?: (event: MarkEvent) => void;\n /** Called when the mouse leaves a data mark. */\n onMarkLeave?: () => void;\n /** Called when a legend entry is toggled (clicked to show/hide a series). */\n onLegendToggle?: (series: string, visible: boolean) => void;\n /**\n * Called when the series search (`seriesSearch`) changes the highlight set.\n * Receives the full set of highlighted values after the change, including\n * the authored `encoding.color.highlight` baseline.\n */\n onHighlightChange?: (values: string[]) => void;\n /** Called when an annotation element is clicked. */\n onAnnotationClick?: (annotation: Annotation, event: MouseEvent) => void;\n /** Called when a text annotation label is dragged to a new position. */\n onAnnotationEdit?: (annotation: TextAnnotation, updatedOffset: AnnotationOffset) => void;\n /** Unified edit callback. Fires for any spec-modifying edit (repositioning, deletion, text editing). */\n onEdit?: (edit: ElementEdit) => void;\n /** Fired when an element is selected via click or programmatic select(). */\n onSelect?: (element: ElementRef) => void;\n /** Fired when the current element is deselected (click empty, Escape, or new selection replaces old). */\n onDeselect?: (element: ElementRef) => void;\n /** Fired when inline text editing commits. Also flows through onEdit as a 'text-edit' event. */\n onTextEdit?: (element: ElementRef, oldText: string, newText: string) => void;\n /**\n * Fired when a \"you draw it\" (`youDrawIt`) drawing is revealed (button click\n * or programmatic reveal), with the reader's guess in data coordinates,\n * ordered by x. For newsrooms aggregating reader guesses.\n */\n onReveal?: (guess: Array<{ x: string | number; y: number }>) => void;\n}\n","/**\n * Spec types: the user-facing input contract.\n *\n * These types define what a user (or Claude) writes to describe a visualization.\n * The engine validates, normalizes, and compiles specs into layout objects.\n *\n * Encoding vocabulary follows Vega-Lite conventions (field/type/aggregate/mark)\n * with editorial extensions for chrome, annotations, responsive, and dark mode.\n */\n\n// Re-import for use in LegendConfig and overrides (avoids circular by importing from sibling)\nimport type { Breakpoint, LegendPosition } from '../responsive/breakpoints';\nimport type { SortState } from './layout';\nimport type { ColumnConfig } from './table';\nimport type { SeriesStrategy, TokenValue } from './theme';\n\n// ---------------------------------------------------------------------------\n// Mark type union (Vega-Lite aligned)\n// ---------------------------------------------------------------------------\n\n/**\n * Supported mark types, following Vega-Lite conventions.\n *\n * Mapping from previous OpenChart chart types:\n * - 'bar' covers both horizontal bars (old 'bar') and vertical columns (old 'column')\n * - 'arc' covers both pie (old 'pie') and donut (old 'donut')\n * - 'point' replaces old 'scatter'\n * - 'circle' replaces old 'dot'\n * - 'line' and 'area' unchanged\n *\n * New mark types not in previous OpenChart:\n * - 'text': data-positioned text labels\n * - 'rule': reference lines as data marks\n * - 'tick': strip/rug plot marks\n * - 'rect': heatmaps and 2D binned plots\n * - 'range': dumbbell / arrow / range-bar plots spanning two values per category\n * - 'waffle': unit grids (\"x of 100\") for part-to-whole counts\n * - 'calendar': GitHub-style calendar heatmap (weeks x weekdays, daily dates)\n * - 'parliament': hemicycle seat-dot chart for election/legislature results\n * - 'histogram': binned bars over a continuous field; sugar for a bar with\n * `x.bin` + `y: { aggregate: 'count' }`\n * - 'density': kernel density estimate drawn as a filled curve; sugar for an\n * area over a density transform\n */\nexport type MarkType =\n | 'bar'\n | 'line'\n | 'area'\n | 'point'\n | 'circle'\n | 'arc'\n | 'text'\n | 'rule'\n | 'tick'\n | 'rect'\n | 'lollipop'\n | 'beeswarm'\n | 'range'\n | 'waffle'\n | 'calendar'\n | 'parliament'\n | 'histogram'\n | 'density';\n\n// ---------------------------------------------------------------------------\n// Gradient definitions (Vega-aligned)\n// ---------------------------------------------------------------------------\n\n/** A single color stop in a gradient definition. */\nexport interface GradientStop {\n /** Position along the gradient, 0 to 1. */\n offset: number;\n /** CSS color string at this stop. */\n color: string;\n /** Opacity at this stop, 0 to 1. Maps to SVG stop-opacity. */\n opacity?: number;\n}\n\n/**\n * Linear gradient definition.\n * Coordinates are in [0,1] normalized space relative to the mark's bounding box\n * (maps to SVG gradientUnits=\"objectBoundingBox\").\n */\nexport interface LinearGradient {\n gradient: 'linear';\n /** Color stops from start to end. */\n stops: GradientStop[];\n /** Start x coordinate (0-1). Default: 0. */\n x1?: number;\n /** Start y coordinate (0-1). Default: 0. */\n y1?: number;\n /** End x coordinate (0-1). Default: 0. */\n x2?: number;\n /** End y coordinate (0-1). Default: 1 (top-to-bottom). */\n y2?: number;\n}\n\n/**\n * Radial gradient definition.\n * Coordinates are in [0,1] normalized space relative to the mark's bounding box.\n */\nexport interface RadialGradient {\n gradient: 'radial';\n /** Color stops from inner to outer. */\n stops: GradientStop[];\n /** Inner circle center x (0-1). Default: 0.5. */\n x1?: number;\n /** Inner circle center y (0-1). Default: 0.5. */\n y1?: number;\n /** Inner circle radius (0-1). Default: 0. */\n r1?: number;\n /** Outer circle center x (0-1). Default: 0.5. */\n x2?: number;\n /** Outer circle center y (0-1). Default: 0.5. */\n y2?: number;\n /** Outer circle radius (0-1). Default: 0.5. */\n r2?: number;\n}\n\n/** A gradient definition, either linear or radial. */\nexport type GradientDef = LinearGradient | RadialGradient;\n\n/** Type guard: check if a value is a GradientDef object. */\nexport function isGradientDef(value: unknown): value is GradientDef {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'gradient' in value &&\n 'stops' in value &&\n ((value as GradientDef).gradient === 'linear' || (value as GradientDef).gradient === 'radial')\n );\n}\n\n/**\n * Extract a single representative color from a fill value.\n * Returns the fill directly if it's a string, or the last stop color\n * if it's a gradient. Used by tooltips, labels, legends, and voronoi\n * overlays that need a flat color.\n */\nexport function getRepresentativeColor(fill: string | GradientDef): string {\n if (typeof fill === 'string') return fill;\n const stops = fill.stops;\n return stops.length > 0 ? stops[stops.length - 1].color : '#000000';\n}\n\n// ---------------------------------------------------------------------------\n// Mark definition (Vega-Lite aligned)\n// ---------------------------------------------------------------------------\n\n/**\n * Configuration for the scatter regression trend line. Passed as the object\n * form of `MarkDef.trendline`.\n */\nexport interface TrendlineConfig {\n /**\n * Stacking order relative to the point marks. `'below'` (default) draws the\n * line behind the dots; `'above'` draws it on top so it stays visible over a\n * dense cloud.\n */\n layer?: 'above' | 'below';\n /**\n * Stroke color. Defaults to the theme's high-contrast text color so the line\n * reads on both light and dark grounds. Set an explicit CSS color to override.\n */\n stroke?: string;\n /**\n * Stroke width in pixels. Defaults to 1.5. Over a dense cloud with `layer:\n * 'above'`, a heavier line (e.g. 2.5) stays legible where the thin default\n * disappears into the dots.\n */\n strokeWidth?: number;\n}\n\n/**\n * Mark definition object with visual properties.\n *\n * When `mark` is a string, it's shorthand for `{ type: markString }`.\n * When it's an object, it can include properties like interpolation,\n * point markers, orientation, and more.\n */\nexport interface MarkDef {\n /** The mark type. */\n type: MarkType;\n /**\n * Show point markers on line/area marks.\n * - true: filled circles at each data point\n * - 'transparent': invisible hover targets (legacy behavior)\n * - 'endpoints': show only first and last point per series (hollow dots)\n * - 'last' / 'first': show only the last (or first) point — filled dot,\n * no white halo. Useful for highlighting the latest reading on a line\n * chart and the default for sparkline mode.\n * - false: no point marks (default; uses voronoi overlay for tooltips)\n */\n point?: boolean | 'transparent' | 'endpoints' | 'last' | 'first';\n /**\n * Curve interpolation for line/area marks.\n * Maps to d3-shape curve factories.\n */\n interpolate?:\n | 'linear'\n | 'monotone'\n | 'step'\n | 'step-before'\n | 'step-after'\n | 'basis'\n | 'cardinal'\n | 'natural';\n /** Explicit orientation override for bar marks. */\n orient?: 'horizontal' | 'vertical';\n /** Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie. */\n innerRadius?: number;\n /** Outer radius for arc marks. */\n outerRadius?: number;\n /**\n * Start angle in radians for arc marks. Defaults to 0 (full pie/donut).\n * Combined with `endAngle`, restricts the pie/donut to a partial sweep,\n * e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut\n * (election-style results donut). Angles follow d3's convention: 0 is\n * straight up, increasing clockwise. Only meaningful when `type` is `'arc'`.\n */\n startAngle?: number;\n /**\n * End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut).\n * See `startAngle`. Only meaningful when `type` is `'arc'`.\n */\n endAngle?: number;\n /**\n * Donut center stat. A string renders one line; the object form adds a\n * caption under it. Decorative (the number is already in the slices), and\n * only meaningful when `type` is `'arc'` with an inner radius.\n */\n centerLabel?: string | { text: string; subtitle?: string };\n /** Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness. */\n cornerRadius?: number | 'pill';\n /**\n * Bar/column marks: fixed bar thickness in pixels. When set, bars are this\n * height (horizontal) or width (vertical), centered within the band.\n *\n * Point marks: the dot RADIUS in pixels (default 5, typical 2-12). This is\n * NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of\n * 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation\n * rejects values above 50 with the conversion.\n */\n size?: number;\n /** Whether the mark is filled (vs stroked only). */\n filled?: boolean;\n /** Default opacity (0-1). */\n opacity?: number;\n /**\n * Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n *\n * On a binned bar with more than one color group this defaults to 0.55 so\n * overlapping distributions stay readable; set it explicitly to override.\n * On an area, setting it switches the fill from the default gradient to a\n * flat translucent series color — gradients and overlapping fills don't mix.\n */\n fillOpacity?: number;\n /**\n * Number of bins. Only meaningful when `type` is `'histogram'`; desugars to\n * `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned\n * extent, write `x.bin` directly — it is the canonical form and takes\n * precedence over this shorthand.\n */\n binCount?: number;\n /**\n * Plot each group's share of its own total instead of a raw count. Only\n * meaningful when `type` is `'histogram'`. Makes two distributions of very\n * different size directly comparable.\n */\n normalize?: boolean;\n /**\n * Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`.\n * Omitted uses Silverman's rule of thumb.\n */\n bandwidth?: number;\n /**\n * Draw the cumulative distribution instead of the density. Only meaningful\n * when `type` is `'density'`.\n */\n cumulative?: boolean;\n /**\n * Number of points the density curve is evaluated at. Only meaningful when\n * `type` is `'density'`. Defaults to 200.\n */\n steps?: number;\n /** Default fill color or gradient. */\n fill?: string | GradientDef;\n /** Default stroke color. */\n stroke?: string;\n /** Default stroke width. */\n strokeWidth?: number;\n /** Tooltip behavior. null disables tooltips. */\n tooltip?: boolean | null;\n /** Clip marks to the chart area. */\n clip?: boolean;\n /**\n * Overlay a least-squares regression line on scatter (point) marks.\n * Defaults to true. Set `mark: { type: 'point', trendline: false }` to\n * suppress it when the chart already carries its own reference line (e.g. a\n * manual x=y diagonal in a separate layer), which would otherwise produce\n * two competing diagonals.\n *\n * Pass an object to control stacking order: the line renders `below` the\n * points by default, but a dense scatter (thousands of overlapping dots)\n * fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the\n * reference line over the cloud.\n */\n trendline?: boolean | TrendlineConfig;\n /**\n * Horizontal pixel offset from the data anchor. Only meaningful when `type`\n * is `'text'`. Use it to lift a label clear of the mark it annotates instead\n * of shifting the underlying data value.\n */\n dx?: number;\n /**\n * Vertical pixel offset from the data anchor (negative = up). Only meaningful\n * when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten\n * pixels above its point.\n */\n dy?: number;\n /** Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`. */\n align?: 'left' | 'center' | 'right';\n /** Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`. */\n baseline?: 'top' | 'middle' | 'bottom';\n /**\n * Font size in pixels for text marks. Ignored when the `size` encoding channel\n * is present (which scales font size from a data field). `size` is already\n * taken by bar thickness, hence the distinct name.\n */\n fontSize?: number;\n /**\n * Pattern-fill reinforcement for filled marks (bar, area, arc).\n *\n * - `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots,\n * crosshatch, vertical lines) layered over each series color so series\n * remain distinguishable without color vision. Marks too small to show\n * a legible pattern keep their solid fill.\n * - `'none'` (default): solid fills only.\n */\n fillPattern?: 'auto' | 'none';\n /**\n * Visual style for range marks. Only meaningful when `type` is `'range'`.\n * - 'dumbbell' (default): a dot at each end joined by a connector line.\n * The start dot is muted, the end dot carries the accent color.\n * - 'arrow': a line with an arrowhead at the x2/y2 end, the strongest\n * \"change between two points in time\" form.\n * - 'bar': a plain floating range bar spanning start to end.\n */\n style?: 'dumbbell' | 'arrow' | 'bar';\n /**\n * Color range marks by direction of change. Only meaningful when `type` is\n * `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's\n * `positive` semantic color and decreases use `negative`. A field-based\n * `encoding.color` takes precedence over direction coloring.\n */\n colorByDirection?: boolean;\n /**\n * Total number of cells in a waffle grid. Only meaningful when `type` is\n * `'waffle'`. Category values normalize to this many cells via\n * largest-remainder rounding so the grid always sums exactly. Default 100.\n */\n units?: number;\n /**\n * Number of columns in a waffle grid. Only meaningful when `type` is\n * `'waffle'`. Rows derive from `units / columns`. Default 10.\n */\n columns?: number;\n /**\n * Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is\n * `'waffle'`. Categories whose share of the total falls below `threshold`\n * (a fraction, so `0.05` is 5%) merge into one trailing category. Off by\n * default: bucketing is an editorial decision, not a layout default. Same\n * shape as `SankeySpec.other`.\n */\n other?: number | { threshold: number; label?: string };\n /**\n * First day of the week for calendar marks. Only meaningful when `type` is\n * `'calendar'`. Controls which weekday occupies the top row of each band.\n * Defaults to 'monday' (ISO week convention).\n */\n weekStart?: 'monday' | 'sunday';\n /**\n * Corner radius in pixels for calendar day cells. Only meaningful when\n * `type` is `'calendar'`. Defaults to 2.\n */\n cellRadius?: number;\n /**\n * Seat layout shape for parliament marks. Only meaningful when `type` is\n * `'parliament'`. `'hemicycle'` (default, and currently the only shape)\n * packs seats into concentric semicircular arcs, party-grouped left to\n * right.\n */\n shape?: 'hemicycle';\n /**\n * Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes\n * dots to fill the available hemicycle rings for the given seat count.\n * Only meaningful when `type` is `'parliament'`.\n */\n seatRadius?: number | 'auto';\n /**\n * The majority-threshold line and \"N to win\" label on parliament marks. Only\n * meaningful when `type` is `'parliament'`. `true` (default) draws the line at\n * a simple majority (half the seats plus one); `false` hides it. The object\n * form overrides the threshold and label: `{ seats }` sets a custom seat count\n * (e.g. a two-thirds supermajority) and `{ label }` overrides the default\n * \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls\n * back to the simple-majority default.\n */\n majorityLine?: boolean | { seats?: number; label?: string };\n}\n\n// ---------------------------------------------------------------------------\n// Encoding\n// ---------------------------------------------------------------------------\n\n/** Data field type, following Vega-Lite conventions. */\nexport type FieldType = 'quantitative' | 'temporal' | 'nominal' | 'ordinal';\n\n/** Aggregate function applied to a field before encoding. */\nexport type AggregateOp =\n | 'count'\n | 'sum'\n | 'mean'\n | 'median'\n | 'min'\n | 'max'\n | 'variance'\n | 'stdev'\n | 'distinct'\n | 'q1'\n | 'q3';\n\n/** Axis configuration for an encoding channel. */\nexport interface AxisConfig {\n /** Axis title text. If omitted, the field name is used. */\n title?: string;\n /**\n * Number format: d3-format string (e.g. \",.0f\"), or semantic keyword\n * `'percent'` / `'currency'`. When omitted, compact notation (1k, 2.5M)\n * is applied automatically.\n */\n format?: string;\n /** Override tick count. Engine picks a sensible default if omitted. */\n tickCount?: number;\n /** Whether to show gridlines for this axis. */\n grid?: boolean;\n /** Rotation angle in degrees for tick labels. Common values: -45, -90, 90. */\n labelAngle?: number;\n /** Axis orientation override. */\n orient?: 'top' | 'bottom' | 'left' | 'right';\n /** Explicit tick values. */\n values?: unknown[];\n /** How to handle overlapping labels. */\n labelOverlap?: boolean | 'parity' | 'greedy';\n /** Whether to flush labels to the axis edges. */\n labelFlush?: boolean;\n /** Whether to show the axis domain line. */\n domain?: boolean;\n /** Whether to show tick marks. */\n ticks?: boolean;\n /** Axis position offset in pixels. */\n offset?: number;\n /** Padding between axis title and axis. */\n titlePadding?: number;\n /** Padding between tick labels and axis. */\n labelPadding?: number;\n /** Color override for axis tick labels and title. Useful in dual-axis charts to match axis color to its series. */\n labelColor?: string;\n /** Literal string appended to every formatted tick label. e.g. \"B\" gives \"$4.5B\" when format is \"$,.1~f\". */\n labelSuffix?: string;\n /** Secondary data field to display alongside each tick label. Renders in lighter weight/color. Only effective on categorical y-axis labels (horizontal bar charts). */\n labelField?: string;\n /**\n * Where tick labels render relative to the chart area. Editorial line/area\n * y-axes default to `'inline'`: labels sit above their gridlines at the\n * chart's left edge, with no left gutter, axis line, or tick marks. Other\n * axis types default to `'gutter'` (the classic placement outside the chart\n * area).\n */\n tickPosition?: 'inline' | 'gutter';\n}\n\n/** Scale configuration for an encoding channel. */\nexport interface ScaleConfig {\n /**\n * Explicit domain override. Auto-derived from data if omitted.\n * Continuous scales take `[min, max]`; threshold scales take the full\n * list of class break values; categorical scales take the value list.\n */\n domain?: number[] | string[];\n /** Scale type override. Usually inferred from field type. */\n type?: ScaleType;\n /** Whether to nice-ify the domain for clean tick values. Defaults to true. */\n nice?: boolean;\n /** Whether the domain should include zero. Defaults to true for quantitative. */\n zero?: boolean;\n /** When true and domain is set, filter out data rows with values outside the domain range. */\n clip?: boolean;\n /** Explicit range override. */\n range?: unknown[];\n /**\n * Named color scheme (Vega-Lite aligned). Resolved against the core palette\n * registry (sequential: blue, green, orange, purple, teal; diverging:\n * redBlue, brownTeal; categorical) during the pre-validation sugar\n * expansion; common VL names (blues, greens, category10, ...) are accepted\n * as aliases. Unknown names fail validation with the supported list.\n * An explicit `range` wins over `scheme`.\n */\n scheme?: string;\n /** Reverse the range direction. */\n reverse?: boolean;\n /** Clamp output to the range. */\n clamp?: boolean;\n /**\n * Padding for band/point scales. Point-scale default (a categorical x/y on a\n * line/area/point mark) is 0 for line and area marks (the trend runs flush\n * to the plot edges) and 0.5 for other marks (e.g. scatter, so points don't\n * collide with the axis). `paddingOuter` is accepted as an alias here since\n * point scales have no inner bands.\n */\n padding?: number;\n /** Inner padding for band scales. */\n paddingInner?: number;\n /** Outer padding for band scales. */\n paddingOuter?: number;\n /** Exponent for pow scales. */\n exponent?: number;\n /** Base for log scales (default 10). */\n base?: number;\n /** Constant for symlog scales. */\n constant?: number;\n}\n\n/** Scale type, following Vega-Lite conventions. */\nexport type ScaleType =\n | 'linear'\n | 'log'\n | 'pow'\n | 'sqrt'\n | 'symlog'\n | 'time'\n | 'utc'\n | 'ordinal'\n | 'band'\n | 'point'\n | 'quantile'\n | 'quantize'\n | 'threshold';\n\n/**\n * Sort a categorical domain by the aggregated values of another field\n * (Vega-Lite EncodingSortField).\n */\nexport interface SortByField {\n /** Data field whose values determine the category order. */\n field: string;\n /** Aggregate applied per category before comparing. Defaults to 'sum'. */\n op?: AggregateOp;\n /** Sort direction. Defaults to 'ascending'. */\n order?: 'ascending' | 'descending';\n}\n\n/**\n * A constant value definition for an encoding channel (Vega-Lite aligned).\n * The VL idiom for a fixed visual property, e.g. `color: { value: '#1b7fa3' }`.\n * Expanded to the matching mark-level property (fill/stroke, size, opacity)\n * during the pre-validation sugar expansion.\n */\nexport interface ValueDef {\n /** The constant value: CSS color string, size number, opacity (0-1), or gradient. */\n value: string | number | boolean | null | GradientDef;\n}\n\n/**\n * A single encoding channel mapping a data field to a visual property.\n *\n * Follows the Vega-Lite encoding model: field identifies the column,\n * type determines how the engine interprets values, aggregate applies\n * a transformation before encoding.\n *\n * @template TData - The shape of a data row. When `ChartSpec<TData>` is used\n * with a typed data array, `field` is constrained to `keyof TData & string`,\n * giving IDE autocomplete and compile-time typo detection. Defaults to `DataRow`\n * which degrades to plain `string` (no constraint), keeping untyped specs working.\n */\nexport interface EncodingChannel<TData extends DataRow = DataRow> {\n /**\n * Data field name (column in the data array).\n *\n * When using `ChartSpec<TData>` with a typed data array, this is constrained\n * to the actual column names of `TData`. Typos fail at compile time and IDEs\n * autocomplete your column names.\n */\n field: keyof TData & string;\n /**\n * How to interpret the field values.\n * - quantitative: continuous numbers (scale: linear)\n * - temporal: dates/times (scale: time)\n * - nominal: unordered categories (scale: ordinal)\n * - ordinal: ordered categories (scale: ordinal)\n */\n type: FieldType;\n /** Optional aggregate to apply before encoding. */\n aggregate?: AggregateOp;\n /**\n * Axis configuration. Set to `false` to suppress axis entirely (no space\n * reserved). `null` is accepted as a VL-aligned alias for `false`.\n */\n axis?: AxisConfig | false | null;\n /** Scale configuration. */\n scale?: ScaleConfig;\n /**\n * Stacking behavior for quantitative channels (Vega-Lite aligned).\n *\n * Vega-Lite accepts `'zero' | 'normalize' | 'center' | null`. OpenChart adds\n * `true` (sugar for `'zero'`) and `false` (sugar for `null`) so callers can\n * write `stack: true` / `stack: false` without learning the string forms.\n * The string forms are still the canonical input — the boolean shorthand is\n * normalized to the matching string before reaching layout.\n *\n * - undefined: chart-type default (see below) -- stacked for bar/column/area\n * - true | 'zero': stack from zero baseline\n * - 'normalize': stack and normalize to fraction of total (0-1 per category)\n * - 'center': center stacks around zero (streamgraph style)\n * - null | false: no stacking -- renders grouped/dodged (bar) or overlap (area)\n *\n * **Defaults differ by chart type:**\n * - **Bar/Column**: defaults to stacked (`'zero'`), Vega-Lite aligned. Use\n * `stack: null` (or `false`) for grouped/side-by-side bars.\n * - **Area**: defaults to stacked (`'zero'`), Vega-Lite aligned. Use\n * `stack: null` (or `false`) to opt into overlap rendering, where each\n * series renders as a translucent gradient band anchored at the\n * y-domain baseline.\n * - **Line**: stacking is not applied (lines always overlap).\n *\n * @example\n * // Grouped horizontal bars (opt-out; default is stacked):\n * \"x\": { \"field\": \"revenue\", \"type\": \"quantitative\", \"stack\": null }\n *\n * @example\n * // Overlap area (opt-out; default is stacked):\n * \"y\": { \"field\": \"value\", \"type\": \"quantitative\", \"stack\": null }\n */\n stack?: boolean | 'zero' | 'normalize' | 'center' | null;\n /**\n * Encoding-level bin shorthand (Vega-Lite aligned).\n * When set, auto-generates a BinTransform during normalization and updates\n * the field reference to the binned output (convention: `bin_<fieldName>`).\n * - true: bin with default params\n * - BinParams: bin with custom params (maxbins, step, etc.)\n */\n bin?: boolean | BinParams;\n /**\n * Encoding-level timeUnit shorthand (Vega-Lite aligned).\n * When set, auto-generates a TimeUnitTransform during normalization and\n * updates the field reference to the output (convention: `<timeUnit>_<fieldName>`).\n */\n timeUnit?: TimeUnit;\n /**\n * Sort order for categorical (nominal/ordinal) scale domains (Vega-Lite aligned).\n * - 'ascending': sort domain values ascending (default VL behavior)\n * - 'descending': sort domain values descending\n * - null: use data order (no sorting)\n * - undefined: ascending (VL default)\n * - 'x' | '-x' | 'y' | '-y': sort by another channel's values ('-' prefix\n * for descending). The VL ranked-bar idiom: `sort: '-y'` orders categories\n * by value, largest first. Resolved into an explicit domain during the\n * pre-validation sugar expansion.\n * - string[]: explicit value order. Listed values come first (in the given\n * order); unlisted data values follow in data order.\n * - SortByField: sort by the aggregated values of another field.\n */\n sort?: 'ascending' | 'descending' | null | 'x' | '-x' | 'y' | '-y' | string[] | SortByField;\n /**\n * Legend configuration for this channel (Vega-Lite aligned).\n * Only meaningful on the `color` channel. `null` hides the legend (the VL\n * idiom for `legend.show = false`); a config object merges into the\n * top-level `legend` (top-level keys win on conflict).\n */\n legend?: LegendConfig | null;\n /**\n * Display title override (Vega-Lite aligned).\n * Used as the label in tooltips instead of the raw field name.\n * Also usable for axis titles, but `axis.title` takes precedence there.\n */\n title?: string;\n /**\n * Format string for values (Vega-Lite aligned).\n * For quantitative fields: d3-format string (e.g. \",.0f\", \"$,.2f\"),\n * or semantic keyword `'percent'` / `'currency'`.\n * For temporal fields: d3-time-format string (e.g. \"%Y\", \"%b %d\").\n * Used in tooltips; `axis.format` takes precedence for axis tick labels.\n */\n format?: string;\n /**\n * Emphasis highlight for the color channel. Names one or more series values\n * to visually emphasize; all other series are muted to a neutral gray.\n *\n * Only meaningful on the `color` encoding channel. On other channels this\n * property is ignored.\n *\n * @example\n * encoding: {\n * color: { field: 'region', type: 'nominal', highlight: 'West' }\n * }\n */\n highlight?: string | string[];\n}\n\n/**\n * Facet channel definition: partitions data into small-multiple panels.\n *\n * Unlike positional channels, facet only needs a field, a categorical type,\n * and optional layout hints (columns, sort). It does not carry scale/axis\n * config because it produces a grid, not a visual encoding.\n */\nexport interface FacetChannel<TData extends DataRow = DataRow> {\n /** Data field to partition by. Each unique value produces one panel. */\n field: keyof TData & string;\n /** Must be categorical. */\n type: 'nominal' | 'ordinal';\n /** Number of columns in the wrap grid. Auto-computed when omitted. */\n columns?: number;\n /** Sort order for panel values. Default ascending. */\n sort?: 'ascending' | 'descending' | null;\n}\n\n/**\n * Encoding object mapping visual channels to data fields.\n * Which channels are required depends on the mark type — see the per-mark\n * encoding interfaces (ArcEncoding, LineEncoding, etc.) and MARK_ENCODING_RULES\n * in encoding.ts for the full requirements table.\n *\n * @template TData - Propagated from ChartSpec<TData>. Constrains `field` in\n * every channel to `keyof TData & string` when a typed data row is provided.\n */\nexport interface Encoding<TData extends DataRow = DataRow> {\n /**\n * Horizontal position channel. Required for: bar, line, area, point, tick, rect, lollipop.\n * Maps a field to the x-axis. Use `type: 'temporal'` for dates, `'nominal'` for categories,\n * `'quantitative'` for numbers.\n */\n x?: EncodingChannel<TData>;\n /**\n * Vertical position channel. Required for: bar, line, area, point, tick, rect, arc, lollipop.\n * For arc marks, this is the value field (slice size). For all others it's the y-axis position.\n */\n y?: EncodingChannel<TData>;\n /**\n * Color channel. Required for arc marks (determines pie/donut slice coloring).\n * Optional for all other marks -- used for series differentiation on multi-series charts,\n * or heatmap intensity. Accepts a conditional definition to apply colors based on data predicates,\n * or a bare `{ value }` constant (VL aligned; expanded to the mark-level fill/stroke).\n */\n color?: EncodingChannel<TData> | ConditionalValueDef<TData> | ValueDef;\n /**\n * Size channel. Used by point/bubble charts to scale dots by a quantitative\n * field, area-proportionally (sqrt curve). The units are dot RADII in px:\n * `scale.range` defaults to [3, 30] for scatter, and a bare `{ value }`\n * constant expands to `mark.size`, also a radius. This differs from\n * Vega-Lite, where point size is an area in px^2 -- VL-scale values\n * (60-900) are rejected by validation with the conversion.\n * Accepts a conditional definition to vary size based on data predicates.\n */\n size?: EncodingChannel<TData> | ConditionalValueDef<TData> | ValueDef;\n /**\n * Detail channel. Groups data into multiple series without mapping to a visual property.\n * Useful when you want separate lines per category but don't need the color to differ.\n */\n detail?: EncodingChannel<TData>;\n /**\n * Identity channel for data-update transitions. Maps a field whose value\n * uniquely identifies each datum across updates. Mirrors Vega-Lite's key channel.\n * Not visually encoded. Optional: charts derive sensible default keys.\n */\n key?: EncodingChannel<TData>;\n /**\n * Secondary x position. Used with `x` to define a horizontal span (rect marks, error bars).\n * Both `x` and `x2` must be quantitative.\n */\n x2?: EncodingChannel<TData>;\n /**\n * Secondary y position. Used with `y` to define a vertical span (rect marks, error bands).\n * Both `y` and `y2` must be quantitative.\n */\n y2?: EncodingChannel<TData>;\n /**\n * Data-driven opacity (0-1 range). Accepts a conditional definition to vary opacity\n * based on data predicates (e.g., highlight selected points), or a bare `{ value }`\n * constant (VL aligned; expanded to `mark.opacity`).\n */\n opacity?: EncodingChannel<TData> | ConditionalValueDef<TData> | ValueDef;\n /**\n * Stroke dash pattern encoding. Maps a nominal/ordinal field to different\n * dash patterns on line and rule marks. Useful when color alone doesn't\n * distinguish series well.\n */\n strokeDash?: EncodingChannel<TData>;\n /** Rotation angle encoding for point marks. Maps a quantitative field to 0-360 degrees. */\n angle?: EncodingChannel<TData>;\n /**\n * Text content for `text` marks. Required when mark is `'text'`.\n * Not meaningful for other mark types.\n */\n text?: EncodingChannel<TData>;\n /**\n * Tooltip field(s). Shown on hover. Can be a single channel or an array for\n * multi-field tooltips. Independent of the x/y/color encoding.\n */\n tooltip?: EncodingChannel<TData> | EncodingChannel<TData>[];\n /**\n * Angular position (slice value) for arc marks (pie/donut), the canonical\n * value channel in v8. Waffle and parliament marks accept it too (the same\n * part-to-whole value channel). `y` is still accepted on these marks as a\n * deprecated alias for backward compatibility (a compile warning is emitted\n * and `y` is populated from `theta` internally). Not used by any other mark\n * type.\n */\n theta?: EncodingChannel<TData>;\n /**\n * Facet channel: partitions data into a grid of small-multiple panels.\n * Each unique value of the facet field produces one panel. Panels share\n * scales by default; use `resolve: { scale: { y: 'independent' } }` for\n * per-panel axes.\n */\n facet?: FacetChannel<TData>;\n /**\n * Row facet channel: partitions data into vertically-stacked panels.\n * Each unique value of the row field produces one panel, arranged in a\n * single column. Shares the x-axis scale by default; y-axis is independent.\n * Mutually exclusive with `facet` and `column`.\n */\n row?: FacetChannel<TData>;\n /**\n * Column facet channel: partitions data into side-by-side panels.\n * Each unique value of the column field produces one panel, arranged in a\n * single row. Shares the y-axis scale by default; x-axis is independent.\n * Mutually exclusive with `facet` and `row`.\n */\n column?: FacetChannel<TData>;\n}\n\n// ---------------------------------------------------------------------------\n// Graph-specific encoding\n// ---------------------------------------------------------------------------\n\n/** Encoding channel for graph nodes and edges. */\nexport interface GraphEncodingChannel {\n /** Data field name on the node/edge object. */\n field: string;\n /** How to interpret the field values. */\n type?: FieldType;\n /** Scale configuration. Auto-derived from data if omitted. */\n scale?: ScaleConfig;\n /**\n * Category ordering for the color domain and legend. Graph channels default\n * `'ascending'` (VL-aligned, deterministic); `string[]` pins an explicit\n * order; `null` keeps data order.\n *\n * NOTE: chart encoding channels keep data-order — this graph default is a\n * deliberate divergence this release (see docs/migrating-v8.md).\n */\n sort?: 'ascending' | 'descending' | null | string[];\n /**\n * Category values to emphasize on load; the rest dim. Mirrors the chart\n * `encoding.color.highlight` value list. Distinct from the `highlight()`\n * runtime verb and `interaction.hover.mode`.\n */\n highlight?: string | string[];\n}\n\n/** Graph-specific encoding mapping visual properties to node/edge data fields. */\nexport interface GraphEncoding {\n /** Color mapping for nodes. */\n nodeColor?: GraphEncodingChannel;\n /**\n * Size mapping for nodes. `scale.type` defaults to `'sqrt'` (area-perceptual,\n * today's behavior); set `scale.type: 'linear'` for a linear radius ramp.\n * `scale.range` sets the pixel radius extent (default `[3, 12]`).\n */\n nodeSize?: GraphEncodingChannel;\n /**\n * Opacity mapping for nodes (VL opacity channel). Quantitative fields map\n * linearly to `[0.25, 1]`; `scale.range` overrides.\n *\n * NOTE: there is deliberately no `edgeOpacity` channel — edge alpha is owned\n * by the interaction alpha-tier machinery (hover dimming, search).\n */\n nodeOpacity?: GraphEncodingChannel;\n /** Color mapping for edges. */\n edgeColor?: GraphEncodingChannel;\n /** Width mapping for edges. */\n edgeWidth?: GraphEncodingChannel;\n /** Style mapping for edges (solid, dashed, dotted). */\n edgeStyle?: GraphEncodingChannel;\n /** Label field for nodes. */\n nodeLabel?: GraphEncodingChannel;\n /**\n * Label priority mapping for nodes. Maps a quantitative field to a 0-1\n * priority range; higher values are shown first as the user zooms in.\n * Omit to use the default degree-based priority.\n * `nodeOverrides[id].alwaysShowLabel` still wins as the hard override.\n */\n nodeLabelPriority?: GraphEncodingChannel;\n}\n\n/** Layout algorithm for graph visualization. */\nexport interface GraphLayoutConfig {\n /** Layout algorithm type. */\n type: 'force' | 'radial' | 'hierarchical';\n /** Optional clustering configuration. */\n clustering?: {\n /** Field to group nodes by for cluster forces. */\n field: string;\n };\n /** Charge strength for force layout. Negative values create repulsion. */\n chargeStrength?: number;\n /** Target distance between linked nodes. */\n linkDistance?: number;\n /** Extra px added to node radius for collision detection (default 2). */\n collisionPadding?: number;\n /** Link force strength override. */\n linkStrength?: number;\n /** Whether to apply center force (default true). */\n centerForce?: boolean;\n /**\n * Deterministic layout seed. Same spec + seed ⇒ identical settled layout\n * within a given execution path (worker vs sync parity is not guaranteed).\n */\n seed?: number;\n /**\n * Repulsion preset (plain-language, no d3 jargon):\n * `gentle`/`balanced`/`energetic` → chargeStrength −150/−300/−600 and\n * velocityDecay 0.5/0.4/0.3. A raw `chargeStrength` wins over the preset.\n */\n energy?: 'gentle' | 'balanced' | 'energetic';\n /**\n * Settle-speed preset: `quick`/`balanced`/`thorough` → alphaDecay\n * 0.05/0.0228/0.01. Higher = faster cool-down, less final refinement.\n */\n settle?: 'quick' | 'balanced' | 'thorough';\n /**\n * Headless settle ticks run before the first paint. `true` → 100 ticks under\n * a 250ms budget; a number sets an explicit tick count; `false` disables.\n *\n * Lives in `layout`, NOT `animation`, on purpose: warmup *reduces* motion, so\n * it must survive `animation: false` (opt-out consumers get less settling\n * chaos, not more).\n */\n warmup?: boolean | number;\n}\n\n// ---------------------------------------------------------------------------\n// Chrome (editorial text elements)\n// ---------------------------------------------------------------------------\n\n/** Style overrides for a chrome text element. */\nexport interface ChromeTextStyle {\n /** Font size in pixels. */\n fontSize?: number;\n /** Font weight (400 = normal, 600 = semibold, 700 = bold). */\n fontWeight?: number;\n /** Font family override. */\n fontFamily?: string;\n /** Text color (CSS color string). */\n color?: string;\n /**\n * Maximum number of lines this chrome text may wrap to. Additional lines are\n * dropped and the last kept line is truncated with an ellipsis. Bounds the\n * chrome's contribution to layout height at any container width. Applies\n * whether the font size comes from the theme default or an explicit override.\n * Caps height only, not horizontal overflow of a single unbreakable word.\n */\n maxLines?: number;\n}\n\n/** A chrome text element with optional style overrides. */\nexport interface ChromeText {\n /** The text content to display. */\n text: string;\n /** Optional style overrides. Theme defaults are used for any omitted property. */\n style?: ChromeTextStyle;\n /** Pixel offset for fine-tuning position. */\n offset?: AnnotationOffset;\n}\n\n/**\n * Editorial chrome elements: eyebrow, title, subtitle, source attribution, byline, footer.\n * These are first-class structural elements, not string-only afterthoughts.\n * Each element can be a simple string or a ChromeText object with style overrides.\n */\nexport interface Chrome {\n /**\n * Editorial kicker/category label rendered above the title. Typically\n * uppercase, tracked, and tinted with the accent color (e.g.\n * \"Equities · Single Ticker\"). Term follows IBM Carbon and Atlassian\n * Design System conventions; not part of Vega-Lite's title model.\n */\n eyebrow?: string | ChromeText;\n /** Main title displayed above the visualization. */\n title?: string | ChromeText;\n /** Subtitle displayed below the title, typically providing context. */\n subtitle?: string | ChromeText;\n /** Data source attribution, displayed below the chart area. */\n source?: string | ChromeText;\n /** Author or organization byline. */\n byline?: string | ChromeText;\n /** Footer text, displayed at the very bottom. */\n footer?: string | ChromeText;\n /**\n * Right-anchored brand block on the footer row, paired with a small accent\n * dot to its left. Visually balances the source/byline left-anchored text.\n * When set, suppresses the default `OpenData` watermark for this chart.\n */\n brand?: string | ChromeText;\n}\n\n// ---------------------------------------------------------------------------\n// Annotations\n// ---------------------------------------------------------------------------\n\n/** Pixel offset for fine-grained annotation positioning. */\nexport interface AnnotationOffset {\n /** Horizontal pixel offset. */\n dx?: number;\n /** Vertical pixel offset. */\n dy?: number;\n}\n\n/** Anchor direction for annotation label placement relative to the data point. */\nexport type AnnotationAnchor = 'top' | 'bottom' | 'left' | 'right' | 'auto';\n\n/** Connector type for annotation labels. */\nexport type ConnectorType = 'straight' | 'curve' | 'drop-line';\n\n/** Object form of the connector config, allowing arrow control. */\nexport interface ConnectorConfig {\n /** Connector style. */\n type: ConnectorType;\n /** Whether to draw an arrowhead at the data-point end. Default: true for curve, false for straight. Drop-line never takes a head. */\n arrow?: boolean;\n}\n\n/** Style overrides for the dot marker drawn at the data point. */\nexport interface AnnotationDot {\n /** Circle radius in pixels. Default 4. */\n radius?: number;\n /** Fill color. Defaults to theme background for an \"open ring\" look. */\n fill?: string;\n /** Stroke color. Defaults to the connector's resolved stroke, so marker and leader read as one system. */\n stroke?: string;\n /** Stroke width in pixels. Default 1.5. */\n strokeWidth?: number;\n}\n\n/** Base properties shared by all annotation types. */\ninterface AnnotationBase {\n /** Stable identifier for selection and edit callbacks. When provided, edit events include this ID for reliable element matching. */\n id?: string;\n /** Human-readable label for the annotation. */\n label?: string;\n /** Fill color for the annotation element. */\n fill?: string;\n /** Stroke color for the annotation element. */\n stroke?: string;\n /** Opacity from 0 to 1. */\n opacity?: number;\n /** Z-index for render ordering. Higher values render on top. */\n zIndex?: number;\n /** When false, the annotation is always shown even at compact breakpoints. Default true. */\n responsive?: boolean;\n}\n\n/**\n * Text annotation positioned at a data coordinate.\n * Shows a callout label at a specific point in the chart.\n */\nexport interface TextAnnotation extends AnnotationBase {\n type: 'text';\n /** X-axis data value or position. */\n x: string | number;\n /** Y-axis data value or position. */\n y: string | number;\n /**\n * The annotation text. Required for text annotations.\n *\n * Supports inline `**bold**` spans, so emphasis lands on the key phrase rather\n * than the whole block: `'Inflation peaked at **8.5%**'`. Unmatched `**` (and\n * empty `****`) render literally. `\\n` starts a new line.\n */\n text: string;\n /** Thinning priority (lower = kept longer at narrow widths). When omitted, spec order is used. */\n priority?: number;\n /**\n * Optional muted second-tone text rendered below the primary `text`.\n * Used for supporting context (e.g. methodology, source). Newlines in\n * `text` still produce multi-line primary; subtitle is a separate block.\n *\n * Supports the same inline `**bold**` spans as `text`. When a subtitle is\n * present and no `fontWeight` is set, the primary `text` resolves to weight\n * 700 — the lede + context stack.\n */\n subtitle?: string;\n /**\n * Open-ring marker drawn on the data point itself (not on the pulled-back\n * connector tip).\n *\n * Left unset, a default marker appears whenever a connector is enabled and\n * carries no arrowhead — the leader points, the ring lands. An arrowed\n * connector gets no marker unless you ask for one: the head already marks the\n * spot. `dot: false` is always bare; an explicit `dot` always wins.\n *\n * `true` uses the default style; pass an object to override radius, fill,\n * stroke, or strokeWidth.\n */\n dot?: boolean | AnnotationDot;\n /** Font size override. */\n fontSize?: number;\n /** Font weight override. */\n fontWeight?: number;\n /** Pixel offset from the computed position. */\n offset?: AnnotationOffset;\n /** Anchor direction for label placement relative to the data point. */\n anchor?: AnnotationAnchor;\n /**\n * Connector from label to anchor point.\n * - `true` (default): straight line, no arrow\n * - `'straight'`: straight line, no arrow\n * - `'curve'`: curved arrow with arrowhead\n * - `'drop-line'`: vertical line through the data point's x\n * - `{ type, arrow? }`: object form for explicit arrow control\n * - `false`: no connector\n *\n * The connector leaves the text block on the side facing the data point, with\n * a small standoff gap. A leader shorter than 8px is dropped as noise (the\n * marker alone reads better), as is a connector whose target sits inside the\n * text block. Widen `offset` if you want the line back.\n */\n connector?: boolean | ConnectorType | ConnectorConfig;\n /** Per-endpoint offsets for the connector line. Allows fine-tuning where the connector starts and ends. */\n connectorOffset?: {\n /** Offset for the label-end of the connector. */\n from?: AnnotationOffset;\n /** Offset for the data-point-end of the connector. */\n to?: AnnotationOffset;\n };\n /**\n * Background plate behind the text, for readability over chart lines.\n *\n * `true` uses the theme surface, so the plate follows light/dark mode. A\n * color string sets it explicitly. Prefer `true`: a hardcoded '#ffffff' stays\n * white in dark mode and the text turns light-gray-on-white.\n */\n background?: string | true;\n /** Whether to show the paint-order stroke halo behind text. Default true. Set false for white text on colored backgrounds. */\n halo?: boolean;\n}\n\n/**\n * Range annotation highlighting a region of the chart.\n * Defined by x1/x2 (vertical band) or y1/y2 (horizontal band) or both (rectangle).\n */\nexport interface RangeAnnotation extends AnnotationBase {\n type: 'range';\n /** Start of the range on the x-axis. */\n x1?: string | number;\n /** End of the range on the x-axis. */\n x2?: string | number;\n /** Start of the range on the y-axis. */\n y1?: string | number;\n /** End of the range on the y-axis. */\n y2?: string | number;\n /**\n * For band/point (ordinal) x or y scales, whether the range extends from the\n * data point's center out to the band/step edge. Default `true`: a range on a\n * bar chart spans full columns, and a range on a line chart extends half a step\n * past the first/last point so the band reaches the plot edge. Set `false` to\n * anchor the range exactly at the data point centers instead — useful when you\n * want the band inset from the axis (e.g. starting at the first data point\n * rather than flush against the y-axis guide). No effect on linear/time scales.\n */\n extendToEdges?: boolean;\n /** Pixel offset for the range label. */\n labelOffset?: AnnotationOffset;\n /** Anchor direction for the range label. */\n labelAnchor?: AnnotationAnchor;\n /** Font size override for the range label (px). Default: 11. */\n fontSize?: number;\n /** Font weight override for the range label. Default: 500. */\n fontWeight?: number;\n}\n\n/**\n * Reference line annotation: a horizontal or vertical line at a data value.\n * Useful for baselines (zero), targets, or thresholds.\n */\nexport interface RefLineAnnotation extends AnnotationBase {\n /**\n * Discriminant. `'refline'` is canonical and the only value in the type.\n * `'rule'` (it collides with the `rule` mark type) is still accepted at\n * runtime for backward compatibility -- the spec-sugar pass rewrites it to\n * `'refline'` with a deprecation warning before validation -- but it is not\n * part of this type. Use `'refline'`.\n */\n type: 'refline';\n /** X-axis value for a vertical reference line. */\n x?: string | number;\n /** Y-axis value for a horizontal reference line. */\n y?: string | number;\n /** Line style. */\n style?: 'solid' | 'dashed' | 'dotted';\n /** Raw SVG dash pattern override, e.g. [4, 4]. Takes precedence over style. */\n strokeDash?: number[];\n /** Line width in pixels. */\n strokeWidth?: number;\n /** Pixel offset for the reference line label. */\n labelOffset?: AnnotationOffset;\n /** Anchor direction for the reference line label. */\n labelAnchor?: AnnotationAnchor;\n /** Label font size in pixels. Default: 11. */\n fontSize?: number;\n /** Label font weight. Default: 400. */\n fontWeight?: number;\n}\n\n/** Discriminated union of all annotation types. */\nexport type Annotation = TextAnnotation | RangeAnnotation | RefLineAnnotation;\n\n// ---------------------------------------------------------------------------\n// Theme + Dark Mode\n// ---------------------------------------------------------------------------\n\n/**\n * Dark mode behavior.\n *\n * - `\"auto\"` - Checks the `prefers-color-scheme` media query to detect the\n * user's system-level preference. This does NOT detect class-based dark mode\n * toggles (e.g. `document.documentElement.classList.toggle('dark')`). If your\n * app uses class-based dark mode, use VizThemeProvider with `\"force\"` or\n * `\"off\"` instead of `\"auto\"` and toggle based on your app's state.\n * - `\"force\"` - Always render in dark mode regardless of system preference.\n * - `\"off\"` - Always render in light mode (default).\n *\n * All components (Chart, Sankey, Graph) inherit darkMode from VizThemeProvider\n * when no explicit prop is passed.\n */\nexport type DarkMode = 'auto' | 'force' | 'off';\n\n/**\n * Per-element chrome theme override. Pass a plain string (color only,\n * backward-compatible) or an object with full typography control.\n */\nexport interface ChromeThemeOverride {\n /** Text color. Accepts a TokenValue for light/dark pairs. */\n color?: TokenValue;\n /** Font size in pixels. */\n fontSize?: number;\n /** Font weight. */\n fontWeight?: number;\n /** Line height multiplier. */\n lineHeight?: number;\n}\n\n/**\n * User-facing theme configuration for overriding defaults.\n * All fields are optional. The engine deep-merges these onto the default theme.\n */\nexport interface ThemeConfig {\n /**\n * Color palette overrides.\n * Pass a flat string[] as shorthand for categorical colors,\n * or an object for full control over categorical, sequential, diverging, etc.\n */\n colors?:\n | string[]\n | {\n /** Categorical stroke palette for nominal data (array of CSS color strings). */\n categorical?: string[];\n /**\n * Categorical palette for area-filling marks (bar, area, arc, waffle,\n * calendar, rect). Defaults to `categorical` when only that is set.\n */\n categoricalFill?: string[];\n /** Hairline color for axis lines and separators. Accepts a TokenValue for light/dark pairs. */\n hairline?: TokenValue;\n /** Sequential palettes keyed by name. Each is an array of color stops. */\n sequential?: Record<string, string[]>;\n /** Diverging palettes keyed by name. Each is an array of color stops with a neutral midpoint. */\n diverging?: Record<string, string[]>;\n /** Background color. Accepts a TokenValue for light/dark pairs. */\n background?: TokenValue;\n /** Default text color. Accepts a TokenValue for light/dark pairs. */\n text?: TokenValue;\n /** Gridline color. Accepts a TokenValue for light/dark pairs. */\n gridline?: TokenValue;\n /** Axis line and tick color. Accepts a TokenValue for light/dark pairs. */\n axis?: TokenValue;\n /** Annotation fill color. Accepts a TokenValue for light/dark pairs. */\n annotationFill?: TokenValue;\n /** Annotation text color. Accepts a TokenValue for light/dark pairs. */\n annotationText?: TokenValue;\n /** Semantic color for positive/up-trend values. Accepts a TokenValue for light/dark pairs. */\n positive?: TokenValue;\n /** Semantic color for negative/down-trend values. Accepts a TokenValue for light/dark pairs. */\n negative?: TokenValue;\n };\n /** Font overrides. */\n fonts?: {\n /** Primary font family. */\n family?: string;\n /** Monospace font family (for tabular numbers). */\n mono?: string;\n /** Font size overrides in pixels. Partial — only specified keys are overridden. */\n sizes?: {\n /** Chart title. Default: 26. */\n title?: number;\n /** Subtitle below the title. Default: 14. */\n subtitle?: number;\n /** Body text (tooltips, legend labels). Default: 13. */\n body?: number;\n /** Small text (source line, footer). Default: 11. */\n small?: number;\n /** Axis tick labels. Default: 11. */\n axisTick?: number;\n /** KPI metric uppercase label. Default: 10. */\n metricLabel?: number;\n /** KPI metric primary value. Default: 22. Delta/secondary derive from this. */\n metricValue?: number;\n };\n /** Font weight overrides. Partial — only specified keys are overridden. */\n weights?: {\n /** Normal text weight. Default: 400. */\n normal?: number;\n /** Medium text weight. Default: 500. */\n medium?: number;\n /** Semibold text weight. Default: 600. */\n semibold?: number;\n /** Bold text weight. Default: 700. */\n bold?: number;\n };\n };\n /** Spacing overrides in pixels. */\n spacing?: {\n /** Padding inside the chart container. */\n padding?: number;\n /** Gap between chrome elements (title to subtitle, etc.). */\n chromeGap?: number;\n /** Gap between the last chrome element and the chart area. */\n chromeToChart?: number;\n /** Gap between chart area and source/footer below. */\n chartToFooter?: number;\n /** Internal padding within the chart area (axes margins). */\n axisMargin?: number;\n /** Height reserved below chart area for x-axis tick labels. Increase when large axisTick font sizes cause label clipping. */\n xAxisHeight?: number;\n /** Gap in pixels between the x-axis line and tick label text. Increase when larger axisTick fonts sit too close to the axis line. */\n xAxisLabelPadding?: number;\n };\n /** Border radius for chart container and tooltips. */\n borderRadius?: number;\n /**\n * Per-element chrome style overrides. Pass a plain string for color-only\n * (backward-compatible), or a ChromeThemeOverride object for full\n * typography control (color, fontSize, fontWeight, lineHeight).\n */\n chrome?: {\n /** Eyebrow (kicker) style override. */\n eyebrow?: string | ChromeThemeOverride;\n /** Title style override. */\n title?: string | ChromeThemeOverride;\n /** Subtitle style override. */\n subtitle?: string | ChromeThemeOverride;\n /** Source/attribution style override. */\n source?: string | ChromeThemeOverride;\n /** Byline style override. */\n byline?: string | ChromeThemeOverride;\n /** Footer style override. */\n footer?: string | ChromeThemeOverride;\n };\n /**\n * Editorial rule above the chrome block: a short colored bar (the\n * Economist/FT masthead device). Omit for no rule.\n */\n rule?: {\n /** Rule color. Accepts a TokenValue for light/dark pairs. */\n color: TokenValue;\n /** Rule length in pixels. */\n width: number;\n /** Rule height in pixels. */\n thickness: number;\n };\n /**\n * Series color assignment strategy.\n * Default: 'palette' (full categorical palette always, zero visual drift).\n */\n seriesStrategy?: SeriesStrategy;\n}\n\n// ---------------------------------------------------------------------------\n// Label configuration\n// ---------------------------------------------------------------------------\n\n/**\n * Label density mode controlling how many data labels are shown.\n * - 'all': show every label, skip collision detection\n * - 'auto': show labels with collision detection (default)\n * - 'endpoints': show only first and last per series (useful for line charts)\n * - 'none': hide all labels (rely on tooltips and legend)\n */\nexport type LabelDensity = 'all' | 'auto' | 'endpoints' | 'none';\n\n/** Label display configuration for chart data labels. */\nexport interface LabelConfig {\n /** How many labels to show. Defaults to 'auto'. */\n density?: LabelDensity;\n /** Number format override for label values (d3-format string, e.g. \",.0f\"). */\n format?: string;\n /** Literal string prepended to each formatted label value (e.g. \"-\" or \"$\"). */\n prefix?: string;\n /** Literal string appended to each formatted label value (e.g. \"%\" or \"x\"). */\n suffix?: string;\n /** Fixed CSS color for all labels. Overrides the default fill-derived color. */\n color?: string;\n /**\n * Per-series pixel offsets for fine-tuning label positions, keyed by series name.\n * @deprecated Per-series pixel offsets are a workaround for collision bugs\n * now fixed by the layout engine. Will be removed in a future version.\n */\n offsets?: Record<string, AnnotationOffset>;\n /** Font size in pixels for bar/column value labels. */\n fontSize?: number;\n}\n\n/** Shorthand: `false` disables all labels, `true` uses defaults, or pass a full config object. */\nexport type LabelSpec = boolean | LabelConfig;\n\n// ---------------------------------------------------------------------------\n// Legend configuration\n// ---------------------------------------------------------------------------\n\n/** Legend display configuration. Overrides the responsive-default position. */\nexport interface LegendConfig {\n /** Override the legend position. If omitted, the responsive strategy decides. */\n position?: LegendPosition;\n /** Pixel offset for fine-tuning legend position. */\n offset?: AnnotationOffset;\n /** Whether to show the legend. Defaults to true. Set to false to hide. */\n show?: boolean;\n /** Number of columns for horizontal legend layout. Overrides the default row limit. */\n columns?: number;\n /** Max number of legend entries before truncation. Remaining entries show as \"+N more\". */\n symbolLimit?: number;\n /** Maximum number of rows for top-positioned legends before truncation. Defaults to 2. */\n maxRows?: number;\n /** Series names to exclude from the legend. Excluded series still render in the chart. */\n exclude?: string[];\n}\n\n/**\n * Configuration for the searchable series highlight input (`seriesSearch`).\n * The boolean form (`seriesSearch: true`) uses the defaults.\n */\nexport interface SeriesSearchConfig {\n /** Placeholder text for the search input. Defaults to \"Find a series\". */\n placeholder?: string;\n}\n\n/**\n * Configuration for the \"you draw it\" interactive engagement format\n * (`youDrawIt`). The reader draws their guess of the trend from `from` to the\n * right edge before the real line reveals. Line marks only, single-series only.\n */\nexport interface YouDrawItConfig {\n /**\n * The x value (matching `encoding.x`'s domain) where drawing starts. Data\n * before this point renders normally; data at or after it is hidden behind\n * a hatched \"draw here\" region until reveal.\n */\n from: string | number;\n /** Prompt text shown in the hatched drawing region. Defaults to \"Draw your guess\". */\n prompt?: string;\n /** Label for the skip-to-reveal button. Defaults to \"Show me\". */\n revealLabel?: string;\n /**\n * Optional extra comparison line (e.g. \"what everyone else guessed\"),\n * supplied by the host app. Rendered in a third, visually distinct style\n * alongside the reader's guess and the real line after reveal.\n */\n comparisonLine?: Array<{ x: string | number; y: number }>;\n}\n\n/**\n * Content composition for an endpoint-label entry: the series name, the\n * formatted value, or both joined on one line. See\n * {@link EndpointLabelsConfig.content}.\n */\nexport type EndpointLabelContent = 'label' | 'value' | 'label value';\n\n/**\n * Configuration for the endpoint labels column rendered at the chart's right edge\n * for multi-series line/area charts. Each entry pairs the series name with its\n * last formatted value, optionally anchored to the line by an open-circle marker.\n *\n * The column is independent of the traditional `legend` and the legacy\n * end-of-line labels. Together with `legend.show`, the three suppression toggles\n * follow this truth table for ≥2-series line/area charts:\n *\n * | `legend.show` | `endpointLabels` | Traditional legend | Endpoint column | End-of-line labels |\n * |--|--|--|--|--|\n * | unset | unset | hidden (auto-suppressed) | shown (default) | hidden |\n * | true | unset | shown | shown | hidden |\n * | unset | false | shown (auto-suppress revoked) | hidden | hidden |\n * | false | false | hidden | hidden | shown (last-resort) |\n * | true | false | shown | hidden | hidden |\n * | false | true | hidden | shown | hidden |\n * | true | true | shown | shown | hidden |\n *\n * Single-series charts: column is hidden by default (nothing to identify).\n *\n * The implementation is in `packages/engine/src/legend/suppression.ts` —\n * `resolveSuppression` is the single source of truth. The table above is\n * a user-facing mirror; if the two ever diverge, the engine wins. Tests\n * in `legend/__tests__/suppression.test.ts` enforce every cell.\n */\nexport interface EndpointLabelsConfig {\n /** Explicit on/off. When undefined, the chart auto-decides based on series count. */\n show?: boolean;\n /** Maximum number of series before endpoint labels switch to a legend. Default 8. */\n maxSeries?: number;\n /**\n * Which ends of the chart get label columns.\n * - `'end'` (default): labels on the right (trailing) edge only.\n * - `'both'`: labels on both left (leading) and right (trailing) edges.\n */\n ends?: 'end' | 'both';\n /**\n * What each entry renders.\n * - `'label'`: series name only.\n * - `'value'`: formatted value only.\n * - `'label value'`: series name and value joined on one line.\n *\n * When omitted, entries render the series name with the formatted value on\n * a second line (the classic endpoint column). Pass an object to set the\n * leading and trailing columns independently, e.g. the classic slope-chart\n * look is `{ leading: 'label value', trailing: 'value' }`.\n */\n content?:\n | EndpointLabelContent\n | { leading?: EndpointLabelContent; trailing?: EndpointLabelContent };\n /** Field to read the displayed value from. Defaults to `encoding.y.field`. */\n valueField?: string;\n /** d3-format string for the value. Defaults to `encoding.y.axis.format`. */\n format?: string;\n /** Max wrap width in pixels for long series names. Default 96. */\n width?: number;\n /** Render an open-circle marker on the line at the right edge. Default true. */\n showMarker?: boolean;\n /** Override the marker style. */\n markerStyle?: {\n fill?: string;\n stroke?: string;\n strokeWidth?: number;\n radius?: number;\n };\n /**\n * Render a thin leader line from the swatch row back to the line endpoint\n * when collision-sweep displaces a label off its data point. Default false:\n * the marker on the line plus the swatch in the column already pair label\n * to data; the connector tends to add visual noise for small displacements.\n * Opt in when labels collide hard and you need the explicit tie-back.\n */\n showLeader?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Spec types (the top-level discriminated union)\n// ---------------------------------------------------------------------------\n\n/** Data row: a plain object with string keys. */\nexport type DataRow = Record<string, unknown>;\n\n/** Per-series visual style overrides for line/area charts. */\nexport interface SeriesStyle {\n /** Line dash style. Defaults to 'solid'. */\n lineStyle?: 'solid' | 'dashed' | 'dotted';\n /** Whether to show data point markers. Defaults to true. */\n showPoints?: boolean;\n /** Stroke width override. */\n strokeWidth?: number;\n /** Opacity override (0-1). */\n opacity?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Animation\n// ---------------------------------------------------------------------------\n\n/**\n * Named easing presets for entrance animations.\n * Uses CSS linear() curves. Named 'ease' (not 'easing') to match Vega convention.\n */\nexport type AnimationEase = 'smooth' | 'snappy';\n\n/** Stagger configuration for sequential element reveal. */\nexport interface AnimationStagger {\n /** Delay between each element in ms. Default: 80 */\n delay?: number;\n /** Ordering strategy. Default: 'index' (DOM order) */\n order?: 'index' | 'value' | 'reverse';\n}\n\n/** Fine-grained responsive behavior configuration. */\nexport interface ResponsiveConfig {\n /**\n * Automatic annotation thinning at narrow widths. When true (default),\n * annotations that can't be placed without overlap are demoted to numbered\n * dot markers with footnote text below the chart. Set false to restore\n * the pre-auto-thinning behavior (annotations hidden at compact breakpoints).\n */\n autoThin?: boolean;\n}\n\n/**\n * Animation phase config. Follows Vega's enter/update/exit model.\n * Each phase can be true (use defaults) or a config object.\n */\nexport interface AnimationPhaseConfig {\n /** Duration in ms. Default: 500 for enter. */\n duration?: number;\n /** Easing preset. Default: 'smooth'. */\n ease?: AnimationEase;\n /** Stagger config. true = defaults, false = no stagger. Default: true for enter. */\n stagger?: AnimationStagger | boolean;\n}\n\n/**\n * Data-update phase config. Adds `maxMarks` on top of the shared phase\n * options; the cap is meaningless for enter/exit, which animate via CSS.\n */\nexport interface UpdatePhaseConfig extends AnimationPhaseConfig {\n /**\n * Maximum mark count that still runs a tweened data-update transition.\n * Default: 500. Above the cap the chart swaps instantly instead, because\n * per-frame SVG attribute writes on thousands of elements drop frames on\n * low-end devices. Raise it deliberately when the audience is desktop-heavy\n * and you have measured the frame budget.\n */\n maxMarks?: number;\n}\n\n/**\n * Full animation config object. Structured as enter/update/exit phases\n * following Vega's encoding set model.\n */\nexport interface AnimationConfig {\n /** Entrance animation when chart first renders. */\n enter?: AnimationPhaseConfig | boolean;\n /** Transition animation when data updates. */\n update?: UpdatePhaseConfig | boolean;\n /** Exit animation when marks are removed. */\n exit?: AnimationPhaseConfig | boolean;\n /** Delay before annotations animate in (ms after marks). Default: 200. */\n annotationDelay?: number;\n}\n\n/**\n * Animation spec property.\n * - true: enable entrance animation with sensible defaults\n * - false/omitted: no animation (current behavior)\n * - AnimationConfig: full control via enter/update/exit phases\n */\nexport type AnimationSpec = boolean | AnimationConfig;\n\n/**\n * Chart display mode.\n * - `'full'` (default): standard chart with chrome, axes, legend, padding.\n * - `'sparkline'`: minimal inline mini-chart. Strips chrome, axes, legend,\n * watermark, animation, and reduces margins to a tiny safety pad. The mark\n * fills the container edge-to-edge. Best with `mark: 'line' | 'area' | 'bar'`.\n * Explicit user fields (chrome, legend, encoding.x.axis, etc.) still render\n * when set, so users can opt in to specific elements.\n */\nexport type Display = 'full' | 'sparkline';\n\n/**\n * Breakpoint-conditional overrides for chart specs.\n *\n * Allows specifying different chrome, labels, legend, or annotations\n * per breakpoint. Merged shallowly into the base spec at compile time\n * when the container matches the breakpoint.\n */\nexport interface ChartSpecOverride {\n /** Override editorial chrome at this breakpoint. */\n chrome?: Chrome;\n /** Override label configuration at this breakpoint. */\n labels?: LabelSpec;\n /** Override legend configuration at this breakpoint. */\n legend?: LegendConfig;\n /** Override annotations at this breakpoint. */\n annotations?: Annotation[];\n /** Override animation at this breakpoint. */\n animation?: AnimationSpec;\n /** Override display mode (`'full'` or `'sparkline'`) at this breakpoint. */\n display?: Display;\n /** Override watermark visibility at this breakpoint. */\n watermark?: boolean;\n /** Override crosshair behavior at this breakpoint. */\n crosshair?: boolean;\n}\n\n/**\n * A KPI/metric cell rendered above the chart in a horizontal row.\n *\n * Used for editorial dashboards (e.g. \"CLOSE $186.10 +1.4%\") where the chart\n * is paired with summary statistics. Cells lay out evenly across the chart\n * width. Hidden in sparkline mode; auto-stripped when the container can't\n * fit the laid-out values.\n */\nexport interface Metric {\n /** Uppercase eyebrow label, e.g. \"CLOSE\". */\n label: string;\n /** Primary numeric value, e.g. \"$186.10\". */\n value: string;\n /** Optional change indicator, e.g. \"+1.4%\". Rendered next to the value. */\n delta?: string;\n /** Tone for the delta. 'up' = positive (green), 'down' = negative (red). Default 'up'. */\n deltaTone?: 'up' | 'down';\n /** Optional secondary value (e.g. multiplier \"10.3×\"). Rendered after the delta. */\n secondary?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Per-mark encoding interfaces (Task 2)\n// Required channels derived directly from MARK_ENCODING_RULES in encoding.ts.\n// TypeScript types must stay in sync with those runtime rules.\n// ---------------------------------------------------------------------------\n\n/**\n * Encoding for arc marks (pie/donut charts).\n * - `theta`: required (quantitative — the canonical slice value channel)\n * - `color`: required (nominal/ordinal — the category)\n * - `y`: deprecated alias for `theta`, accepted for backward compatibility\n */\nexport interface ArcEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n /** Canonical value channel for arc marks (quantitative — the slice value). */\n theta: EncodingChannel<TData>;\n color: EncodingChannel<TData> | ConditionalValueDef<TData>;\n}\n\n/**\n * Encoding for line marks.\n * - `x`: required (temporal or ordinal — the time/category axis)\n * - `y`: required (quantitative — the value axis)\n */\nexport interface LineEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x: EncodingChannel<TData>;\n y: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for bar marks (vertical columns and horizontal bars).\n * - `x`: required\n * - `y`: required\n */\nexport interface BarEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x: EncodingChannel<TData>;\n y: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for area marks.\n * - `x`: required (temporal or ordinal)\n * - `y`: required (quantitative)\n */\nexport interface AreaEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x: EncodingChannel<TData>;\n y: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for point marks (scatter plots).\n * - `x`: required\n * - `y`: required\n */\nexport interface PointEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x: EncodingChannel<TData>;\n y: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for circle marks (dot plots).\n * - `x`: required (quantitative)\n * - `y`: required (nominal/ordinal — the category axis)\n */\nexport interface CircleEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x: EncodingChannel<TData>;\n y: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for lollipop marks.\n * - `x`: required (quantitative)\n * - `y`: required (nominal/ordinal)\n */\nexport interface LollipopEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x: EncodingChannel<TData>;\n y: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for beeswarm marks (dodged distribution dots).\n * - Exactly one positional channel is quantitative: the value axis\n * (x = horizontal swarm, y = vertical swarm).\n * - The other positional channel is optional (nominal/ordinal): grouped\n * swarms, one lane per category. The cross axis has no scale; the dodge\n * layout computes pixel offsets around each lane center.\n */\nexport interface BeeswarmEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x?: EncodingChannel<TData>;\n y?: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for range marks (dumbbell / arrow / range-bar plots).\n * - Horizontal (the common editorial form): `y` nominal/ordinal category,\n * `x` quantitative start, `x2` quantitative end.\n * - Vertical: `x` nominal/ordinal category, `y` quantitative start,\n * `y2` quantitative end.\n */\nexport interface RangeEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x: EncodingChannel<TData>;\n y: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for waffle marks (unit grids for part-to-whole counts).\n * - `color`: required (nominal/ordinal, the category)\n * - `theta`: the canonical quantitative share, the same part-to-whole value\n * channel arc marks use. `y` is a deprecated alias, accepted for backward\n * compatibility.\n */\nexport interface WaffleEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n color: EncodingChannel<TData> | ConditionalValueDef<TData>;\n}\n\n/**\n * Encoding for parliament marks (hemicycle seat-dot charts).\n * - `color`: required (nominal/ordinal, the party)\n * - `theta`: the canonical quantitative seat count, the same part-to-whole\n * value channel arc/waffle marks use. `y` is a deprecated alias, accepted\n * for backward compatibility.\n */\nexport interface ParliamentEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n color: EncodingChannel<TData> | ConditionalValueDef<TData>;\n}\n\n/**\n * Encoding for histogram marks (binned bars over a continuous field).\n * - `x`: required quantitative, the raw values to bin (not pre-binned)\n * - `y`: optional; defaults to the per-bin count\n * - `color`: optional grouping. Groups overlap at the same bins rather than\n * dodging or stacking, so two distributions can be compared by shape.\n */\nexport interface HistogramEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for density marks (kernel density estimates).\n * - `x`: required quantitative, the raw values to estimate over\n * - `y`: computed by the density transform, not authored\n * - `color`: optional grouping, one translucent curve per group\n */\nexport interface DensityEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for calendar marks (GitHub-style calendar heatmaps).\n * - `x`: required (temporal, daily dates, one row per day)\n * - `color`: required (quantitative, the per-day value)\n *\n * The calendar computes its own weeks-x-weekdays geometry: no positional\n * scales or axes. Multi-year data partitions into one stacked band per year,\n * all bands sharing a single color scale and legend.\n */\nexport interface CalendarEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x: EncodingChannel<TData>;\n color: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for text marks (data-positioned labels).\n * - `text`: required (the field to render as text)\n * - `x`, `y`: optional positioning\n */\nexport interface TextEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n text: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for tick marks (strip/rug plots).\n * - `x`: required\n * - `y`: required\n */\nexport interface TickEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x: EncodingChannel<TData>;\n y: EncodingChannel<TData>;\n}\n\n/**\n * Encoding for rect marks (heatmaps, 2D binned plots).\n * - `x`: required\n * - `y`: required\n */\nexport interface RectEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {\n x: EncodingChannel<TData>;\n y: EncodingChannel<TData>;\n}\n\n// ---------------------------------------------------------------------------\n// Accessibility configuration\n// ---------------------------------------------------------------------------\n\n/**\n * Author-facing accessibility overrides.\n *\n * Everything here is optional; when omitted, openchart auto-generates alt\n * text, ARIA labels, and a screen-reader data table from the spec and data.\n */\nexport interface A11yConfig {\n /**\n * Alt text announced by screen readers for the chart as a whole. Replaces\n * the auto-generated description. Also settable via the top-level\n * `description` field (Vega-Lite convention); this field wins when both\n * are present.\n */\n description?: string;\n /**\n * Hide the chart from assistive technology entirely (`aria-hidden`).\n * Use only when the chart is decorative or the surrounding content\n * already conveys the same information. Also suppresses the hidden\n * screen-reader data table and keyboard mark navigation.\n */\n hidden?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Shared (non-mark-specific) ChartSpec properties\n// ---------------------------------------------------------------------------\n\n/**\n * Properties shared across all ChartSpec mark variants.\n * Extracted to avoid repeating them in every union member.\n *\n * @internal\n */\ninterface BaseChartSpec<TData extends DataRow = DataRow> {\n /**\n * Data array: each element is a row with field values.\n * The Vega-Lite object form `{ values: [...] }` is also accepted at runtime\n * and unwrapped during the pre-validation sugar expansion. `{ url }` is not\n * supported (openchart does not fetch data).\n */\n data: TData[];\n /** Data transforms applied in order before encoding (filter, bin, calculate, timeUnit). */\n transform?: Transform[];\n /** Editorial chrome (title, subtitle, source, etc.). */\n chrome?: Chrome;\n /**\n * How chrome height interacts with the container height budget.\n *\n * - `'subtract'` (default): the container height is fixed and chrome text\n * (title/subtitle/source) is subtracted from it, shrinking the plot. This\n * is the historical behavior.\n * - `'grow'`: the container height is treated as the *plot* budget, and the\n * rendered SVG grows taller by the measured chrome height. The plot keeps\n * its full budget (minus axis/padding overhead) regardless of how many\n * lines the title wraps to.\n *\n * `'grow'` is a no-op for faceted specs in this version.\n */\n chromeLayout?: 'subtract' | 'grow';\n /**\n * Top-level title sugar (Vega-Lite aligned). Expanded into `chrome.title`\n * (and `chrome.subtitle` for the object form). `chrome.title` wins when\n * both are set.\n */\n title?: string | { text: string; subtitle?: string };\n /** Top-level subtitle sugar. Expanded into `chrome.subtitle`; `chrome.subtitle` wins. */\n subtitle?: string;\n /**\n * Alt text for the rendered chart (Vega-Lite aligned). Sugar for\n * `a11y.description`; `a11y.description` wins when both are set.\n * When absent, alt text is auto-generated from the spec and data.\n */\n description?: string;\n /** Accessibility overrides. See {@link A11yConfig}. */\n a11y?: A11yConfig;\n /**\n * Fixed render width in pixels (Vega-Lite aligned). Overrides the\n * container-derived width. When both `width` and `height` are set,\n * `responsive: false` is implied unless `responsive` is set explicitly.\n * Omit for the default container-driven sizing.\n */\n width?: number;\n /** Fixed render height in pixels (Vega-Lite aligned). See `width`. */\n height?: number;\n /** Accepted for Vega-Lite compatibility and ignored (with a compile warning). */\n $schema?: string;\n /**\n * KPI/metric cells rendered as a horizontal row between subtitle and chart\n * area. Each cell shows a label/value pair with optional delta and secondary\n * value. Hidden in sparkline mode and auto-stripped when the container is\n * too narrow or short, or when value text would overflow its cell.\n */\n metrics?: Metric[];\n /** Data annotations (text callouts, highlighted ranges, reference lines). */\n annotations?: Annotation[];\n /** Label display configuration. `false` disables all labels, `true` uses defaults. */\n labels?: LabelSpec;\n /** Legend display configuration (position override). */\n legend?: LegendConfig;\n /**\n * Right-side endpoint labels column for multi-series line/area charts.\n *\n * - `true` or `EndpointLabelsConfig`: render the column.\n * - `false`: hide the column.\n * - omitted: auto-enable for multi-series line/area charts, hide otherwise.\n *\n * See {@link EndpointLabelsConfig} for the full suppression truth table that\n * relates this flag to `legend.show` and the legacy end-of-line labels.\n */\n endpointLabels?: boolean | EndpointLabelsConfig;\n /**\n * Responsive behavior. `true`/`false` enables/disables container-width\n * adaptation. Pass a `ResponsiveConfig` object for fine-grained control\n * (e.g. `{ autoThin: false }` to disable automatic annotation thinning).\n */\n responsive?: boolean | ResponsiveConfig;\n /** Theme configuration overrides. */\n theme?: ThemeConfig;\n /** Dark mode behavior. Defaults to \"off\". */\n darkMode?: DarkMode;\n /**\n * Whether to show the OpenData watermark. Defaults to true; auto-hides\n * when the container is under 200px tall (the cramped height class) unless\n * explicitly set.\n */\n watermark?: boolean;\n /**\n * Series names to hide from rendering by default. This is the authored\n * persistence target for legend visibility. Runtime legend toggles\n * (legend-toggle edit events) layer on top of this via ephemeral runtime\n * sets but never modify it.\n */\n hiddenSeries?: string[];\n /** Per-series visual overrides, keyed by series name (the color field value). */\n seriesStyles?: Record<string, SeriesStyle>;\n /**\n * Breakpoint-conditional overrides. Keys are breakpoint names.\n * At compile time, if the container matches a breakpoint, its overrides\n * are shallow-merged into the spec before layout computation.\n */\n overrides?: Partial<Record<Breakpoint, ChartSpecOverride>>;\n /**\n * Animation configuration.\n * - true: enable entrance animation with sensible defaults\n * - false/omitted: no animation (current behavior)\n * - AnimationConfig: full control via enter/update/exit phases\n */\n animation?: AnimationSpec;\n /**\n * Show a vertical crosshair line that tracks the nearest data point on\n * line and area charts. Only active when a voronoi overlay is present.\n * Defaults to false. In sparkline display mode, defaults to false unless\n * explicitly set.\n */\n crosshair?: boolean;\n /**\n * Searchable series highlight (\"find your country\"). Renders a compact\n * search input above the chart with typeahead over the color channel's\n * distinct values. Selecting a value adds it to the highlight set\n * (multi-select chips, removable); clearing restores the authored\n * `encoding.color.highlight` baseline. Requires a categorical color\n * encoding. Mutually exclusive with edit mode (search wins).\n */\n seriesSearch?: boolean | SeriesSearchConfig;\n /**\n * \"You draw it\" interactive engagement format: the reader draws their\n * guess of the trend before the real line reveals. Line marks only,\n * single-series only. Mutually exclusive with edit mode (validation\n * warning if both are enabled; youDrawIt is disabled).\n */\n youDrawIt?: YouDrawItConfig;\n /**\n * Display mode controlling how much chart chrome is rendered.\n *\n * - `'full'` (default): full publication chart with chrome, axes, legend, padding.\n * - `'sparkline'`: inline mini-chart for dashboards/KPI cards. Strips chrome,\n * axes, legend, watermark, animation, and crosshair. The mark fills the\n * container edge-to-edge with a small safety margin. Best with\n * `mark: 'line' | 'area' | 'bar' | 'point'`.\n *\n * **Override precedence:** explicit user fields always win, even in sparkline\n * mode. Setting `chrome.title` on a sparkline still renders the title.\n * Setting `legend: { show: true }` still renders the legend. This applies at\n * top-level and per-breakpoint overrides.\n */\n display?: Display;\n /**\n * Resolution strategy for shared/independent scales and axes across facet\n * panels. Only meaningful when `encoding.facet` is present. Shared scales\n * (the default) let readers compare across panels; independent scales give\n * each panel its own domain.\n */\n resolve?: ResolveConfig;\n /**\n * Render order within a LayerSpec. Higher values render on top.\n * When omitted, layers render in array order (later layers paint on top).\n * Axis assignment (left/right y) is always determined by array position,\n * not zIndex.\n */\n zIndex?: number;\n}\n\n/**\n * Chart specification: the primary input for standard chart types.\n *\n * Uses the Vega-Lite `mark` property instead of `type` to specify\n * the visualization mark. The mark can be a string shorthand or an\n * object with additional properties (interpolation, point markers, etc.).\n *\n * This is a discriminated union — the `mark` value determines which encoding\n * channels are required. TypeScript enforces required channels at compile time,\n * matching the runtime rules in `MARK_ENCODING_RULES`.\n *\n * @template TData - The shape of a single data row. When provided, `encoding.*.field`\n * values are constrained to `keyof TData` and IDEs autocomplete your column names.\n * Defaults to `DataRow` (no constraint) — existing untyped specs work unchanged.\n *\n * @example\n * // Typed: field autocomplete + typo detection\n * type SalesRow = { date: string; revenue: number; region: string };\n * const spec: ChartSpec<SalesRow> = {\n * mark: 'line',\n * data: rows,\n * encoding: {\n * x: { field: 'date', type: 'temporal' }, // autocompletes\n * y: { field: 'revenue', type: 'quantitative' }, // typos fail at compile time\n * }\n * };\n *\n * @example\n * // Untyped: works exactly as before (no migration needed)\n * const spec: ChartSpec = {\n * mark: 'bar',\n * data: myData,\n * encoding: { x: { field: 'category', type: 'nominal' }, y: { field: 'value', type: 'quantitative' } }\n * };\n */\nexport type ChartSpec<TData extends DataRow = DataRow> =\n | (BaseChartSpec<TData> & {\n mark: 'arc' | (MarkDef & { type: 'arc' });\n encoding: ArcEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'line' | (MarkDef & { type: 'line' });\n encoding: LineEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'bar' | (MarkDef & { type: 'bar' });\n encoding: BarEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'area' | (MarkDef & { type: 'area' });\n encoding: AreaEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'point' | (MarkDef & { type: 'point' });\n encoding: PointEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'circle' | (MarkDef & { type: 'circle' });\n encoding: CircleEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'lollipop' | (MarkDef & { type: 'lollipop' });\n encoding: LollipopEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'beeswarm' | (MarkDef & { type: 'beeswarm' });\n encoding: BeeswarmEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'range' | (MarkDef & { type: 'range' });\n encoding: RangeEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'waffle' | (MarkDef & { type: 'waffle' });\n encoding: WaffleEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'text' | (MarkDef & { type: 'text' });\n encoding: TextEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'tick' | (MarkDef & { type: 'tick' });\n encoding: TickEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'rect' | (MarkDef & { type: 'rect' });\n encoding: RectEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'rule' | (MarkDef & { type: 'rule' });\n encoding: Encoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'calendar' | (MarkDef & { type: 'calendar' });\n encoding: CalendarEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'parliament' | (MarkDef & { type: 'parliament' });\n encoding: ParliamentEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'histogram' | (MarkDef & { type: 'histogram' });\n encoding: HistogramEncoding<TData>;\n })\n | (BaseChartSpec<TData> & {\n mark: 'density' | (MarkDef & { type: 'density' });\n encoding: DensityEncoding<TData>;\n });\n\n/** Row density for tables. Maps to 40 / 48 / 56px row heights. */\nexport type TableDensity = 'condensed' | 'regular' | 'relaxed';\n\n/**\n * Table specification: input for data table visualizations.\n *\n * Tables are a visualization type, not just an HTML grid. They support\n * heatmap coloring, inline sparklines, sorted columns, search, and pagination.\n */\nexport interface TableSpec {\n /** Discriminant: always \"table\". */\n type: 'table';\n /** Data array: each element is a row. */\n data: DataRow[];\n /** Column definitions controlling display, sorting, formatting, and mini-charts. */\n columns: ColumnConfig[];\n /** Optional field to use as a unique row identifier. */\n rowKey?: string;\n /** Editorial chrome. */\n chrome?: Chrome;\n /** Theme configuration overrides. */\n theme?: ThemeConfig;\n /** Dark mode behavior. */\n darkMode?: DarkMode;\n /** Whether to show the OpenData watermark. Defaults to true. */\n watermark?: boolean;\n /** Enable client-side search/filter. */\n search?: boolean;\n /** Pagination configuration. True for defaults, or an object with pageSize. */\n pagination?: boolean | { pageSize: number };\n /** Whether to stick the first column during horizontal scroll. */\n stickyFirstColumn?: boolean;\n /**\n * Row density: `'condensed'` (40px rows), `'regular'` (48px, default),\n * `'relaxed'` (56px). An explicit value always wins over the responsive\n * auto-condense.\n */\n density?: TableDensity;\n /** Zebra striping on body rows. Defaults to false (hairlines only). */\n striped?: boolean;\n /** Initial sort. Falls back to the first inline-bar column, descending. */\n sort?: SortState;\n /**\n * Sticky footer summing every quantitative column over the filtered rows\n * (the whole result set, not just the current page). `true` uses the label\n * \"Total\".\n */\n totalRow?: boolean | { label?: string };\n /**\n * @deprecated Use `density: 'condensed'`. Still honored, with a warning.\n */\n compact?: boolean;\n /** Whether the table adapts to container width. Defaults to true. */\n responsive?: boolean;\n /** Animation configuration for entrance animations. */\n animation?: AnimationSpec;\n}\n\n/** Graph node: must have an id, plus arbitrary data fields. */\nexport interface GraphNode {\n /** Unique identifier for the node. */\n id: string;\n /** Arbitrary data fields. */\n [key: string]: unknown;\n}\n\n/** Graph edge: connects two nodes by id. */\nexport interface GraphEdge {\n /** Source node id. */\n source: string;\n /** Target node id. */\n target: string;\n /** Arbitrary data fields (weight, type, confidence, etc.). */\n [key: string]: unknown;\n}\n\n/** Per-node visual overrides, keyed by node id. */\nexport interface NodeOverride {\n /** Override fill color. */\n fill?: string;\n /** Override radius. */\n radius?: number;\n /** Override stroke width. */\n strokeWidth?: number;\n /** Override stroke color. */\n stroke?: string;\n /** Force label to always show regardless of zoom/priority. */\n alwaysShowLabel?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Graph animation + interaction\n// ---------------------------------------------------------------------------\n\n/**\n * Per-phase and per-behavior motion config for a graph.\n *\n * DEFAULT-ON semantics (deliberately different from charts, which are opt-in):\n * omit `animation` → the resolver returns full defaults; `animation: false` →\n * no choreography (but `layout.warmup` still applies); a per-phase `false`\n * disables just that phase. Graphs are default-on because physics motion\n * already exists on load and choreography reduces the chaos.\n */\nexport interface GraphAnimationConfig {\n /** Node/edge reveal on first render. Default `{ duration: 500, ease: 'smooth', stagger: true }`. */\n enter?: AnimationPhaseConfig | boolean;\n /** Data-update enter-fade for newly added marks. Default `{ duration: 300, ease: 'smooth' }`. */\n update?: AnimationPhaseConfig | boolean;\n /** Ghost fade-out for removed marks. Default `{ duration: 300, ease: 'smooth' }`. */\n exit?: AnimationPhaseConfig | boolean;\n /** Camera flight easing. Default duration `'auto'` (derived from the zoom distance). */\n camera?: { duration?: number; ease?: AnimationEase } | boolean;\n /** Hover emphasis crossfade. Default `{ duration: 150, ease: 'smooth' }`. */\n hover?: { duration?: number; ease?: AnimationEase } | boolean;\n}\n\n/**\n * Graph motion spec.\n * - omitted → full defaults (default-ON)\n * - `false` → no choreography (warmup still applies)\n * - object → per-phase control\n *\n * `prefers-reduced-motion` is always respected regardless of this value.\n */\nexport type GraphAnimationSpec = boolean | GraphAnimationConfig;\n\n/** Interaction behavior for a graph (hover mode, select-fly, opt-in physics). */\nexport interface GraphInteractionConfig {\n /**\n * Hover emphasis behavior. `neighbors` (default) lights the hovered node and\n * its adjacency; `category` lights same-category nodes; `node` lights only the\n * hovered node; `none` disables. `dimOpacity` (default 0.3) is the node dim\n * tier. Named `mode` (not `highlight`) to avoid colliding with the three other\n * meanings of \"highlight\" (the encoding value list, the `highlight()` verb).\n */\n hover?: { mode?: 'neighbors' | 'category' | 'node' | 'none'; dimOpacity?: number };\n /** Selecting a node flies the camera to it. Default `false`. */\n select?: { flyTo?: boolean };\n /**\n * Opt-in cursor repulsion (nodes drift away from the pointer). Disabled under\n * `prefers-reduced-motion` and above the node-count gate.\n */\n cursorRepulsion?: boolean | { radius?: number; strength?: number };\n /** Opt-in springy node drag (user-initiated; no reduced-motion gate). */\n springyDrag?: boolean;\n}\n\nexport interface GraphSpec {\n /** Discriminant: always \"graph\". */\n type: 'graph';\n /** Node array. Each node must have an id field. */\n nodes: GraphNode[];\n /** Edge array. Each edge connects source and target node ids. */\n edges: GraphEdge[];\n /** Graph-specific encoding mapping visual properties to node/edge fields. */\n encoding?: GraphEncoding;\n /** Layout algorithm configuration. */\n layout?: GraphLayoutConfig;\n /** Per-node visual overrides, keyed by node id. */\n nodeOverrides?: Record<string, NodeOverride>;\n /**\n * Node treated as the graph's seed/root: gets a distinct ring and an\n * always-on label without needing a `nodeOverrides` entry, and stays lit when\n * a category highlight or legend filter dims everything else.\n *\n * String shorthand is the node id; the object form adds styling that layers\n * under any `nodeOverrides` entry for the same id (an explicit override\n * always wins). The seed's radius is deliberately left to the `nodeSize`\n * encoding — set `style.radius` to make it bigger.\n *\n * NOT related to `layout.seed`, which is the deterministic RNG seed for the\n * force layout. This field names a node; `layout.seed` is a number.\n *\n * An id that isn't in `nodes` warns and is ignored (it does not throw), so a\n * filtered or paginated node update can't crash the host.\n */\n seedNode?: string | { id: string; style?: NodeOverride };\n /** Editorial chrome. */\n chrome?: Chrome;\n /** Annotations. */\n annotations?: Annotation[];\n /** Theme configuration overrides. */\n theme?: ThemeConfig;\n /** Dark mode behavior. */\n darkMode?: DarkMode;\n /**\n * Motion configuration. DEFAULT-ON: omit for full defaults, `false` to opt\n * out of choreography (layout warmup still applies).\n */\n animation?: GraphAnimationSpec;\n /** Interaction behavior (hover mode, select-fly, opt-in physics). */\n interaction?: GraphInteractionConfig;\n /**\n * Built-in legend. Interactive by default. Set `false` if you render your own\n * legend, so the built-in filter UI doesn't compete with it.\n */\n legend?: boolean | { interactive?: boolean; counts?: boolean };\n /** Whether to show the OpenData watermark. Defaults to true. */\n watermark?: boolean;\n /**\n * Render in two or three dimensions. Defaults to `2` (the Canvas renderer).\n *\n * `3` renders with WebGL. The 3D renderer ships on its own subpath so\n * three.js never enters the default bundle, and it must be imported before\n * the graph mounts:\n *\n * ```ts\n * import '@opendata-ai/openchart-vanilla/graph-3d';\n * ```\n *\n * (Framework hosts import their own package's mirror of the subpath:\n * `@opendata-ai/openchart-react/graph-3d`,\n * `@opendata-ai/openchart-vue/graph-3d`,\n * `@opendata-ai/openchart-svelte/graph-3d`.) Mounting a `dimensions: 3` spec\n * without that import throws.\n *\n * 3D differs from 2D in these ways:\n * - Labels use a fixed budget re-ranked by camera distance, not the 2D\n * priority declutter, so distant labels drop out.\n * - The simulation runs on the main thread (no worker, no wall-clock warmup\n * budget).\n * - Keyboard navigation, SVG export, `interaction.cursorRepulsion` and\n * `interaction.springyDrag` are unsupported. Each warns and is ignored.\n * (`layout.type` of `'radial'` or `'hierarchical'` is rejected by spec\n * validation in both dimensions, so it never reaches either renderer.)\n * - `getCamera()` returns a pose rather than a zoom transform: `x`/`y` are\n * the look-at point in world units, alongside `position` and `target`. In\n * 2D they are the zoom transform's translate in pixels. `onCameraChange`\n * carries the same payload, so persisted camera state has to be keyed on\n * the dimension it came from. Both are typed `GraphCamera` (vanilla), whose\n * `position`/`target` are optional because only 3D sets them.\n * - A structural `update()` reheats the whole layout globally, so settled\n * nodes drift; 2D applies a local impulse instead.\n * - Above 2000 nodes the spec warns and falls back to 2D.\n * - `nodeOverrides[*].stroke` and `strokeWidth` are ignored (no ring).\n */\n dimensions?: 2 | 3;\n}\n\n// ---------------------------------------------------------------------------\n// Layer spec (multi-layer composition)\n// ---------------------------------------------------------------------------\n\n/**\n * Resolution strategy for shared resources across layers.\n * 'shared' (default): union domains, single axis/legend.\n * 'independent': each layer gets its own scale/axis/legend.\n */\nexport type ResolveMode = 'shared' | 'independent';\n\n/**\n * Per-channel resolution config. Controls whether scales, axes, and legends\n * are shared or independent across layers.\n */\nexport interface ResolveConfig {\n scale?: Partial<Record<'x' | 'y' | 'color' | 'size', ResolveMode>>;\n axis?: Partial<Record<'x' | 'y', ResolveMode>>;\n legend?: Partial<Record<'color' | 'size', ResolveMode>>;\n}\n\n/**\n * Layer specification: composites multiple chart layers into a single view.\n *\n * Each element in `layer` is either a ChartSpec or another LayerSpec (nested).\n * Shared data, encoding, and transforms at the LayerSpec level are inherited\n * by children that don't define their own.\n *\n * @template TData - The shape of a single data row. When all layers share the\n * same data shape, pass it here to get field autocomplete across the layer.\n * For layers with different data shapes per child, omit TData (defaults to\n * `DataRow`) — children can be independently typed.\n */\nexport interface LayerSpec<TData extends DataRow = DataRow> {\n /** Array of child layers (ChartSpec or nested LayerSpec). */\n layer: (ChartSpec<TData> | LayerSpec<TData>)[];\n /** Shared data inherited by children without their own data. */\n data?: TData[];\n /** Shared encoding inherited by children (overridden per-channel by child). */\n encoding?: Encoding<TData>;\n /** Shared transforms. Parent transforms run before child transforms. */\n transform?: Transform[];\n /** Editorial chrome (title, subtitle, source, etc.). */\n chrome?: Chrome;\n /** Top-level title sugar (VL aligned). Expanded into `chrome.title`; `chrome.title` wins. */\n title?: string | { text: string; subtitle?: string };\n /** Top-level subtitle sugar. Expanded into `chrome.subtitle`; `chrome.subtitle` wins. */\n subtitle?: string;\n /** Fixed render width in pixels (VL aligned). Overrides the container-derived width. */\n width?: number;\n /** Fixed render height in pixels (VL aligned). Overrides the container-derived height. */\n height?: number;\n /** Accepted for Vega-Lite compatibility and ignored (with a compile warning). */\n $schema?: string;\n /**\n * Annotations for the layered chart. When present, these are the\n * annotations rendered and available for editing. When absent, falls back\n * to the first leaf layer's annotations. Annotations on second and later\n * leaf layers are not rendered.\n *\n * For edit persistence: the `data-annotation-index` stamped on each\n * rendered annotation maps to this array (or the first leaf's if this is\n * unset). Place annotations here for reliable index-based edit matching.\n */\n annotations?: Annotation[];\n /** Label display configuration. `false` disables all labels, `true` uses defaults. */\n labels?: LabelSpec;\n /** Legend display configuration. */\n legend?: LegendConfig;\n /** Whether the chart adapts to container width. Defaults to true. */\n responsive?: boolean;\n /** Theme configuration overrides. */\n theme?: ThemeConfig;\n /** Dark mode behavior. Defaults to \"off\". */\n darkMode?: DarkMode;\n /** Whether to show the OpenData watermark. Defaults to true. */\n watermark?: boolean;\n /** Resolution strategy for shared scales/axes/legends. */\n resolve?: ResolveConfig;\n /**\n * Series names to hide from rendering by default. This is the authored\n * persistence target for legend visibility. Runtime legend toggles\n * (legend-toggle edit events) layer on top of this via ephemeral runtime\n * sets but never modify it.\n */\n hiddenSeries?: string[];\n /** Endpoint labels column, inherited by child layers that don't set their own. */\n endpointLabels?: boolean | EndpointLabelsConfig;\n /** Animation configuration. */\n animation?: AnimationSpec;\n}\n\n// ---------------------------------------------------------------------------\n// Sankey spec (encoding-centric flow diagram)\n// ---------------------------------------------------------------------------\n\n/** Node alignment strategy for sankey layout. */\nexport type SankeyNodeAlign = 'left' | 'right' | 'center' | 'justify';\n\n/** Link coloring strategy for sankey diagrams. */\nexport type SankeyLinkColor = 'gradient' | 'source' | 'target' | 'neutral';\n\n/** Encoding channels specific to sankey diagrams. */\nexport interface SankeyEncoding {\n /** Source node field (required, nominal). */\n source: EncodingChannel;\n /** Target node field (required, nominal). */\n target: EncodingChannel;\n /** Flow value field (required, quantitative). */\n value: EncodingChannel;\n /** Color encoding for nodes/links (optional, nominal). */\n color?: EncodingChannel;\n /** Tooltip encoding (optional). */\n tooltip?: EncodingChannel | EncodingChannel[];\n}\n\nexport interface SankeySpec {\n /** Discriminant: always \"sankey\". */\n type: 'sankey';\n /** Tabular flow data. Each row is a source-target-value link. */\n data: DataRow[];\n /** Encoding channels mapping data fields to visual properties. */\n encoding: SankeyEncoding;\n /** Width of node rectangles in pixels. Defaults to 12. */\n nodeWidth?: number;\n /** Vertical padding between nodes in pixels. Defaults to 16. */\n nodePadding?: number;\n /** Node alignment algorithm. Defaults to 'justify'. */\n nodeAlign?: SankeyNodeAlign;\n /** Number of layout relaxation iterations. Defaults to 6. */\n iterations?: number;\n /** Link coloring strategy. Defaults to 'gradient'. */\n linkStyle?: SankeyLinkColor;\n /** Link fill opacity (0-1). Defaults to 0.5 in light mode, 0.6 in dark mode. */\n linkOpacity?: number;\n /**\n * Bucket sub-threshold nodes into a single \"Other\" node per column. Opt-in:\n * omitted, every node in the data is drawn.\n *\n * A number is the threshold as a share of the node's own column total\n * (`0.05` = under 5% of that column). The object form adds a custom `label`.\n * A column with only one sub-threshold node is left alone, since renaming one\n * node \"Other\" hides its identity without buying any space. Bucketing never\n * changes the diagram's total flow.\n */\n other?: number | { threshold: number; label?: string };\n /**\n * Which side of each node to place labels. 'auto' uses the default heuristic\n * (left-column right, right-column left, middle right). 'right' forces all\n * labels to the right of their nodes. 'left' forces all labels to the left.\n * Defaults to 'auto'.\n */\n nodeLabelAlign?: 'auto' | 'left' | 'right';\n /**\n * Explicit vertical ordering of nodes. An array of node IDs listed from\n * top to bottom. Nodes sharing the same column are sorted by their position\n * in this array. Nodes not in the array are placed after those that are.\n */\n nodeSort?: string[];\n /** Editorial chrome (title, subtitle, source, byline, footer). */\n chrome?: Chrome;\n /** Legend display configuration. */\n legend?: LegendConfig;\n /** Theme configuration overrides. */\n theme?: ThemeConfig;\n /** Dark mode behavior. Defaults to \"off\". */\n darkMode?: DarkMode;\n /** Whether to show the OpenData watermark. Defaults to true. */\n watermark?: boolean;\n /** Animation configuration for entrance animations. */\n animation?: AnimationSpec;\n /**\n * d3-format string applied to flow values in tooltips and ARIA labels.\n * Uses the literal suffix extension: \".0f%\" appends \"%\" to the formatted\n * number (data value 28 renders as \"28%\"). For currency: \"$,.0f\" or \"$~s\".\n * For SI suffixes: \"~s\" (renders 1000 as \"1k\"). When not set, values use\n * the default number formatter.\n * @deprecated Use the encoding channel's `format` field instead. This\n * property is expanded into the encoding during compilation.\n */\n valueFormat?: string;\n}\n\n// ---------------------------------------------------------------------------\n// TileMap spec (US state tile grid map)\n// ---------------------------------------------------------------------------\n\n/** Encoding channels specific to tile map visualizations. */\nexport interface TileMapEncoding {\n /** State code field (required, nominal). Maps to US state abbreviations. */\n state: EncodingChannel;\n /**\n * Value field. For quantitative mode, maps to a sequential color scale.\n * For categorical mode (when color channel is present or values are strings),\n * maps to category labels shown in tooltips.\n */\n value: EncodingChannel;\n /** Color encoding channel (optional, nominal). When present, enables categorical coloring. */\n color?: EncodingChannel;\n /** Tooltip encoding (optional). */\n tooltip?: EncodingChannel | EncodingChannel[];\n}\n\n/** Sequential color palette names available for tile maps. */\nexport type TileMapPalette = 'blue' | 'green' | 'orange' | 'purple' | 'teal';\n\nexport interface TileMapSpec {\n /** Discriminant: always \"tilemap\". */\n type: 'tilemap';\n /**\n * Data for the tile map. Accepts either:\n * - A record mapping state codes to numeric values: `{ \"CA\": 12000, \"TX\": 8500 }`\n * - A record mapping state codes to string categories: `{ \"CA\": \"high\", \"TX\": \"low\" }`\n * - Tabular data rows with state and value fields (requires encoding)\n */\n data: Record<string, number | string | null> | DataRow[];\n /**\n * Encoding channels mapping data fields to visual properties.\n * Required when data is DataRow[]. Auto-generated when data is a record map.\n */\n encoding?: TileMapEncoding;\n /** Sequential color palette. Defaults to 'blue'. Only used in quantitative mode. */\n palette?: TileMapPalette;\n /**\n * Custom category-to-color mapping for categorical tilemaps.\n * Keys are category values, values are CSS color strings.\n * When provided, forces categorical mode regardless of encoding.\n * Example: `{ \"medical_only\": \"#ee4a73\", \"religious\": \"#e07d00\", \"philosophical\": \"#06b6d4\" }`\n */\n colors?: Record<string, string>;\n /** Editorial chrome (title, subtitle, source, byline, footer). */\n chrome?: Chrome;\n /** Legend display configuration. */\n legend?: LegendConfig;\n /** Theme configuration overrides. */\n theme?: ThemeConfig;\n /** Dark mode behavior. Defaults to \"off\". */\n darkMode?: DarkMode;\n /** Whether to show the OpenData watermark. Defaults to true. */\n watermark?: boolean;\n /** Animation configuration for entrance animations. */\n animation?: AnimationSpec;\n /**\n * d3-format string applied to tile values, legend labels, and tooltips.\n * Examples: \".1f\" for one decimal, \"$,.0f\" for currency, \"~s\" for SI.\n * @deprecated Use the encoding channel's `format` field instead. This\n * property is expanded into the encoding during compilation.\n */\n valueFormat?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Map spec (choropleth / symbol maps)\n// ---------------------------------------------------------------------------\n\n/** Supported map projections. */\nexport type GeoMapProjection = 'albersUsa' | 'mercator' | 'equalEarth' | 'identity';\n\n/** Focus target for map camera: feature id(s) to zoom/pan to. */\n/**\n * Fit the point layer's cluster. `true` fits every point; `{ field, value }`\n * fits only the points whose data row has `row[field] === value` (e.g. only the\n * campuses rated \"F\"), which lets a story pan between sub-clusters.\n */\nexport type GeoMapPointsFocus = true | { field: string; value: string | number };\n\nexport type GeoMapFocus =\n | string\n | number\n | Array<string | number>\n | { features: string | number | Array<string | number>; padding?: number }\n | { points: GeoMapPointsFocus; padding?: number };\n\n/** Geo configuration for map specs. */\nexport interface GeoMapGeo {\n /** TopoJSON topology object. User imports this from us-atlas, world-atlas, or their own source. */\n features: unknown;\n /** Field in the TopoJSON feature properties to use as the join key. Defaults to 'id'. */\n idField?: string;\n /**\n * Map projection. Inferred from the topology when omitted: pre-projected\n * coordinates get 'identity', a near-global longitude span gets the\n * equal-area 'equalEarth', anything else gets 'albersUsa'.\n */\n projection?: GeoMapProjection;\n /**\n * Focus the camera. A feature id (or array) fits those features; `{ features, padding }`\n * adds breathing room; `{ points: true, padding }` fits the points layer's cluster\n * instead of any feature (use when points occupy a small part of a large feature, so\n * fitting the feature would leave the cluster small and off-center);\n * `{ points: { field, value }, padding }` fits only the matching point subset, so a\n * story can pan between sub-clusters. null clears focus from a prior story step.\n */\n focus?: GeoMapFocus | null;\n}\n\n/** Encoding channels specific to map visualizations. */\nexport interface GeoMapEncoding {\n /** Join key field in the data. Required for choropleth; optional in basemap-only mode (points layer with empty data). */\n key?: EncodingChannel;\n /** Color encoding (quantitative or nominal). Drives fill color of features. Optional when a points layer is present (basemap-only mode). */\n color?: EncodingChannel;\n /** Tooltip encoding (optional). */\n tooltip?: EncodingChannel | EncodingChannel[];\n}\n\n/** Point/symbol layer rendered above choropleth features. */\nexport interface GeoMapPointsLayer {\n /** Tabular data for the points. Independent of the choropleth data join. */\n data: DataRow[];\n /** Field holding longitude (x coordinate). */\n longitude: EncodingChannel;\n /** Field holding latitude (y coordinate). */\n latitude: EncodingChannel;\n /** Optional quantitative size encoding. Area-proportional: r proportional to sqrt(value). No size legend is rendered in v1; explain the scale in chrome.subtitle if needed. */\n size?: EncodingChannel;\n /** Optional color encoding (nominal or quantitative). Independent scale from choropleth. */\n color?: EncodingChannel;\n /** Tooltip channel(s), same shape as GeoMapEncoding.tooltip. */\n tooltip?: EncodingChannel | EncodingChannel[];\n /** Stable id for event callbacks, and the key used to match points across\n * renders so fill, radius, and position tween on data update. */\n key?: EncodingChannel;\n /** Fill opacity for point circles. Default 0.65. Range 0-1. */\n opacity?: number;\n}\n\nexport interface GeoMapSpec {\n /** Discriminant: always \"map\". */\n type: 'map';\n /** Geo configuration: TopoJSON features, join key, and projection. */\n geo: GeoMapGeo;\n /** Tabular data to join to geo features. */\n data: DataRow[];\n /** Encoding channels mapping data fields to visual properties. */\n encoding: GeoMapEncoding;\n /** Editorial chrome (title, subtitle, source, byline, footer). */\n chrome?: Chrome;\n /** Legend display configuration. */\n legend?: LegendConfig;\n /** Theme configuration overrides. */\n theme?: ThemeConfig;\n /** Dark mode behavior. Defaults to \"off\". */\n darkMode?: DarkMode;\n /** Whether to show the OpenData watermark. Defaults to true. */\n watermark?: boolean;\n /** Optional point/symbol layer rendered above choropleth features. */\n points?: GeoMapPointsLayer;\n /** Animation configuration for entrance animations. */\n animation?: AnimationSpec;\n /**\n * d3-format string applied to color values in tooltips and legend.\n * @deprecated Use the encoding channel's `format` field instead.\n */\n valueFormat?: string;\n}\n\n// ---------------------------------------------------------------------------\n// BarList spec (ranked horizontal bar list)\n// ---------------------------------------------------------------------------\n\nexport interface BarListSpec {\n /** Discriminant: always \"barlist\". */\n type: 'barlist';\n /**\n * Data rows. Each row must have at least a label field and a value field.\n * Rendered as a ranked list of horizontal bars.\n */\n data: DataRow[];\n /** Encoding channels mapping data fields to visual properties. */\n encoding: BarListEncoding;\n /** Bar height in pixels. Defaults to 6. */\n barHeight?: number;\n /** Corner radius: number in px or \"pill\" for fully rounded ends. Defaults to \"pill\". */\n cornerRadius?: number | 'pill';\n /** Maximum number of rows to show. Defaults to 20. */\n maxItems?: number;\n /** Editorial chrome (title, subtitle, source, byline, footer). */\n chrome?: Chrome;\n /** Theme configuration overrides. */\n theme?: ThemeConfig;\n /** Dark mode behavior. Defaults to \"off\". */\n darkMode?: DarkMode;\n /** Whether to show the OpenData watermark. Defaults to true. */\n watermark?: boolean;\n /** Animation configuration for entrance animations. */\n animation?: AnimationSpec;\n /**\n * d3-format string applied to bar values and tooltips.\n * Examples: \".1f\" for one decimal, \"$,.0f\" for currency, \"~s\" for SI.\n * @deprecated Use the encoding channel's `format` field instead. This\n * property is expanded into the encoding during compilation.\n */\n valueFormat?: string;\n}\n\n/** Encoding channels for a barlist visualization. */\nexport interface BarListEncoding {\n /** Label field (required, nominal). The category name for each row. */\n label: EncodingChannel;\n /** Value field (required, quantitative). The numeric value that drives bar width. */\n value: EncodingChannel;\n /** Subtitle field (optional, nominal). Secondary text shown beside the label in lighter weight. */\n subtitle?: EncodingChannel;\n /** Color field (optional, nominal). Maps categories to colors from the palette. When omitted, colors cycle by row index. */\n color?: EncodingChannel;\n /** Tooltip encoding (optional). */\n tooltip?: EncodingChannel | EncodingChannel[];\n}\n\n/**\n * Top-level visualization spec: union discriminated by structural shape.\n *\n * - ChartSpec: has `mark` field (no `type`, no `layer`)\n * - LayerSpec: has `layer` field\n * - TableSpec: has `type: 'table'`\n * - GraphSpec: has `type: 'graph'`\n * - SankeySpec: has `type: 'sankey'`\n * - TileMapSpec: has `type: 'tilemap'`\n * - GeoMapSpec: has `type: 'map'`\n * - BarListSpec: has `type: 'barlist'`\n *\n * Election parliament (hemicycle) charts are `ChartSpec` with `mark:\n * 'parliament'`, not a separate top-level type: seat data (party + seats)\n * maps naturally onto encoding.color/y like arc and waffle, and riding the\n * existing chart pipeline gets scales, legend, tooltips, a11y, and dark mode\n * for free instead of a second bespoke compile path (see\n * `packages/engine/src/charts/parliament/`).\n */\nexport type VizSpec =\n | ChartSpec\n | LayerSpec\n | TableSpec\n | GraphSpec\n | SankeySpec\n | TileMapSpec\n | GeoMapSpec\n | BarListSpec;\n\n/**\n * Chart spec without runtime data, for persistence/storage.\n * Generic: `ChartSpecWithoutData<MyRow>` constrains encoding field names.\n */\nexport type ChartSpecWithoutData<TData extends DataRow = DataRow> = Omit<ChartSpec<TData>, 'data'>;\n/** Table spec without runtime data and columns, for persistence/storage. Columns can be auto-generated via dataTable(). */\nexport type TableSpecWithoutData = Omit<TableSpec, 'data' | 'columns'>;\n/** Graph spec without runtime data, for persistence/storage. */\nexport type GraphSpecWithoutData = Omit<GraphSpec, 'nodes' | 'edges'>;\n/** Sankey spec without runtime data, for persistence/storage. */\nexport type SankeySpecWithoutData = Omit<SankeySpec, 'data'>;\n/** TileMap spec without runtime data, for persistence/storage. */\nexport type TileMapSpecWithoutData = Omit<TileMapSpec, 'data'>;\n/** Map spec without runtime data, for persistence/storage. */\nexport type GeoMapSpecWithoutData = Omit<GeoMapSpec, 'data' | 'points'> & {\n points?: Omit<GeoMapPointsLayer, 'data'>;\n};\n/** BarList spec without runtime data, for persistence/storage. */\nexport type BarListSpecWithoutData = Omit<BarListSpec, 'data'>;\n/** Union of data-stripped spec types for persistence/storage. */\nexport type StoredVizSpec =\n | ChartSpecWithoutData\n | TableSpecWithoutData\n | GraphSpecWithoutData\n | SankeySpecWithoutData\n | TileMapSpecWithoutData\n | GeoMapSpecWithoutData\n | BarListSpecWithoutData;\n\n// ---------------------------------------------------------------------------\n// Transforms (Vega-Lite aligned)\n// ---------------------------------------------------------------------------\n\n/** Logical AND combinator for filter predicates. */\nexport interface LogicalAnd<T> {\n and: T[];\n}\n\n/** Logical OR combinator for filter predicates. */\nexport interface LogicalOr<T> {\n or: T[];\n}\n\n/** Logical NOT combinator for filter predicates. */\nexport interface LogicalNot<T> {\n not: T;\n}\n\n/** A relative-time reference that resolves against the data extent. */\nexport interface RelativeTimeRef {\n anchor: 'max' | 'min';\n offset: number;\n unit: 'day' | 'week' | 'month' | 'quarter' | 'year';\n}\n\n/** A predicate that tests a field value against a condition. */\nexport interface FieldPredicate {\n /**\n * Data field to test.\n * Note: FieldPredicate is used in transforms (FilterTransform) which operate\n * on the raw DataRow type — field is typed as string here since transforms\n * can reference computed/derived fields not present in the original TData.\n */\n field: string;\n /** Equals comparison. */\n equal?: unknown;\n /** Less than. */\n lt?: number | RelativeTimeRef;\n /** Less than or equal. */\n lte?: number | RelativeTimeRef;\n /** Greater than. */\n gt?: number | RelativeTimeRef;\n /** Greater than or equal. */\n gte?: number | RelativeTimeRef;\n /** Inclusive range [min, max]. */\n range?: [number | RelativeTimeRef, number | RelativeTimeRef];\n /** Value is one of these. */\n oneOf?: unknown[];\n /** Whether the value is valid (non-null, non-undefined, non-NaN). */\n valid?: boolean;\n}\n\n/**\n * A filter predicate: a field predicate or logical combination of predicates.\n */\nexport type FilterPredicate =\n | FieldPredicate\n | LogicalAnd<FilterPredicate>\n | LogicalOr<FilterPredicate>\n | LogicalNot<FilterPredicate>;\n\n/** Parameters for binning a field. */\nexport interface BinParams {\n /** Maximum number of bins. */\n maxbins?: number;\n /** Exact step size between bins. */\n step?: number;\n /** Whether to nice-ify bin boundaries. */\n nice?: boolean;\n /** Explicit extent [min, max] for binning. */\n extent?: [number, number];\n}\n\n/** Expression for calculate transforms. */\nexport interface CalculateExpression {\n /** Arithmetic operation to apply. */\n op: '+' | '-' | '*' | '/' | 'abs' | 'round' | 'floor' | 'ceil' | 'log' | 'sqrt';\n /** Primary field to operate on. */\n field: string;\n /** Secondary field for binary operations (+, -, *, /). */\n field2?: string;\n /** Constant value for binary operations when field2 is not provided. */\n value?: number;\n}\n\n/**\n * Time unit granularity for temporal field transformations.\n * Follows Vega-Lite conventions.\n */\nexport type TimeUnit =\n | 'year'\n | 'quarter'\n | 'month'\n | 'week'\n | 'day'\n | 'dayofyear'\n | 'date'\n | 'hours'\n | 'minutes'\n | 'seconds'\n | 'milliseconds'\n | 'yearmonth'\n | 'yearmonthdate'\n | 'monthdate'\n | 'hoursminutes';\n\n/** Filter transform: removes rows that don't match the predicate. */\nexport interface FilterTransform {\n filter: FilterPredicate;\n}\n\n/** Bin transform: adds binned field(s) to each row. */\nexport interface BinTransform {\n bin: true | BinParams;\n field: string;\n as: string | [string, string];\n}\n\n/** Calculate transform: adds a computed field to each row. */\nexport interface CalculateTransform {\n calculate: CalculateExpression;\n as: string;\n}\n\n/** Time unit transform: extracts a time unit from a temporal field. */\nexport interface TimeUnitTransform {\n timeUnit: TimeUnit;\n field: string;\n as: string;\n}\n\n/**\n * Density transform: kernel density estimate over a quantitative field\n * (Vega-Lite aligned).\n *\n * Produces a new dataset — one row per evaluation point, per group — rather\n * than adding fields to the input rows. The output is what an area mark plots.\n */\nexport interface DensityTransform {\n /** Field of raw values to estimate the density of. */\n density: string;\n /** Group the estimate by these fields; one curve per group. */\n groupby?: string[];\n /** Gaussian kernel bandwidth. Omitted or 0 uses Silverman's rule of thumb. */\n bandwidth?: number;\n /** [min, max] to evaluate over. Defaults to the data extent, padded to close the tails. */\n extent?: [number, number];\n /** Emit the cumulative distribution instead of the density. */\n cumulative?: boolean;\n /** Scale the curve by the group's row count instead of integrating to 1. */\n counts?: boolean;\n /** Number of evaluation points. Default 200. */\n steps?: number;\n /** Output field names. Default ['value', 'density']. */\n as?: [string, string];\n}\n\n/**\n * Join-aggregate transform: compute group summaries and write them back onto\n * every row of the group (VL aligned).\n *\n * The difference from `AggregateTransform` is the row count: `aggregate`\n * collapses a group to one row, `joinaggregate` keeps every row and adds the\n * group's summary as a new field. That is what makes \"this row's value as a\n * share of its group's total\" expressible — compute the total, then divide.\n */\nexport interface JoinAggregateTransform {\n joinaggregate: Array<{ op: AggregateOp; field: string; as: string }>;\n /** Fields to group by. Omitted or empty treats all rows as one group. */\n groupby?: string[];\n}\n\n/**\n * Aggregate transform: group rows and compute summary statistics (VL aligned).\n * Produces one row per group with the groupby fields and computed aggregates.\n */\nexport interface AggregateTransform {\n aggregate: Array<{ op: AggregateOp; field: string; as: string }>;\n groupby: string[];\n}\n\n/**\n * Fold transform: unpivot wide-format columns into key/value rows (VL aligned).\n * For each input row, produces N output rows (one per fold field) with all\n * non-fold fields copied plus a key column (field name) and value column (field value).\n */\nexport interface FoldTransform {\n fold: string[];\n /** Output field names for [key, value]. Defaults to ['key', 'value']. */\n as?: [string, string];\n}\n\n/** Window operation types for computing values relative to other rows. */\nexport type WindowOp = 'lag' | 'lead' | 'diff' | 'pct_change' | 'cumsum' | 'rank' | 'first_value';\n\n/** Sort field definition for window transforms. */\nexport interface WindowSortField {\n field: string;\n order?: 'ascending' | 'descending';\n}\n\n/** Window field definition specifying which operation to compute. */\nexport interface WindowFieldDef {\n op: WindowOp;\n field: string;\n /** Row offset for lag/lead/diff/pct_change. Defaults to 1. */\n offset?: number;\n as: string;\n}\n\n/** Window transform: computes values relative to other rows in sort order within a partition. */\nexport interface WindowTransform {\n window: WindowFieldDef[];\n /** Fields to sort by within each partition. */\n sort: WindowSortField[];\n /** Fields to partition (group) by. Each group is windowed independently. */\n groupby?: string[];\n}\n\n/** Discriminated union of all transform types. */\nexport type Transform =\n | FilterTransform\n | BinTransform\n | CalculateTransform\n | TimeUnitTransform\n | AggregateTransform\n | JoinAggregateTransform\n | DensityTransform\n | FoldTransform\n | WindowTransform;\n\n// ---------------------------------------------------------------------------\n// Conditional encoding (Vega-Lite aligned)\n// ---------------------------------------------------------------------------\n\n/**\n * A single condition with a test predicate and resulting value/field.\n * When the test passes for a datum, the condition's value/field is used.\n *\n * @template TData - Propagated from ChartSpec<TData>. Constrains `field` to\n * `keyof TData & string` when a typed data row is provided.\n */\nexport interface Condition<TData extends DataRow = DataRow> {\n /** Predicate to test against each datum. */\n test: FilterPredicate;\n /**\n * Static value to use when the condition is true.\n * Accepted values: CSS color string, opacity (0-1), size number, boolean flag,\n * or a gradient definition for color channels.\n */\n value?: string | number | boolean | null | GradientDef;\n /**\n * Data field to use when the condition is true.\n * Constrained to column names of `TData` when using `ChartSpec<TData>`.\n */\n field?: keyof TData & string;\n /** Field type for the conditional field. */\n type?: FieldType;\n}\n\n/**\n * A conditional value definition for an encoding channel.\n * Evaluates conditions in order, falling back to the default value.\n *\n * @template TData - Propagated from ChartSpec<TData>.\n */\nexport interface ConditionalValueDef<TData extends DataRow = DataRow> {\n /** One or more conditions to evaluate. */\n condition: Condition<TData> | Condition<TData>[];\n /**\n * Default value when no condition matches.\n * Accepted values: CSS color string, opacity (0-1), size number, boolean flag,\n * or a gradient definition for color channels.\n */\n value?: string | number | boolean | null | GradientDef;\n}\n\n/**\n * Check if a channel definition is a regular EncodingChannel (has 'field' at top level).\n * Use this to narrow `EncodingChannel | ConditionalValueDef | ValueDef` in encoding\n * channels that support conditional encoding (color, size, opacity).\n */\nexport function isEncodingChannel<TData extends DataRow = DataRow>(\n def: EncodingChannel<TData> | ConditionalValueDef<TData> | ValueDef | undefined,\n): def is EncodingChannel<TData> {\n if (!def) return false;\n return 'field' in def && !('condition' in def);\n}\n\n/**\n * Check if a channel definition is a ConditionalValueDef.\n */\nexport function isConditionalDef<TData extends DataRow = DataRow>(\n def: EncodingChannel<TData> | ConditionalValueDef<TData> | ValueDef | undefined,\n): def is ConditionalValueDef<TData> {\n if (!def) return false;\n return 'condition' in def;\n}\n\n/**\n * Check if a channel definition is a bare ValueDef (constant value, no field\n * or condition). These are expanded to mark-level properties during the\n * pre-validation sugar expansion.\n */\nexport function isValueDef<TData extends DataRow = DataRow>(\n def: EncodingChannel<TData> | ConditionalValueDef<TData> | ValueDef | undefined,\n): def is ValueDef {\n if (!def) return false;\n return 'value' in def && !('condition' in def) && !('field' in def);\n}\n\n// ---------------------------------------------------------------------------\n// Mark type helpers\n// ---------------------------------------------------------------------------\n\n/** All valid mark type strings for runtime checking. */\nexport const MARK_TYPES: ReadonlySet<string> = new Set<MarkType>([\n 'bar',\n 'line',\n 'area',\n 'point',\n 'circle',\n 'arc',\n 'text',\n 'rule',\n 'tick',\n 'rect',\n 'lollipop',\n 'beeswarm',\n 'range',\n 'waffle',\n 'calendar',\n 'parliament',\n 'histogram',\n 'density',\n]);\n\n/**\n * Mark types that lay out inside the chart area without positional axes\n * (no axis or gridline computation; margins shrink to padding).\n */\nexport function isAxislessMark(markType: MarkType): boolean {\n return (\n markType === 'arc' ||\n markType === 'waffle' ||\n markType === 'calendar' ||\n markType === 'parliament'\n );\n}\n\n/**\n * Extract the mark type string from a mark field (string or MarkDef).\n */\nexport function resolveMarkType(mark: MarkType | MarkDef): MarkType {\n return typeof mark === 'string' ? mark : mark.type;\n}\n\n/**\n * Extract the full MarkDef from a mark field, filling in defaults for string shorthand.\n */\nexport function resolveMarkDef(mark: MarkType | MarkDef): MarkDef {\n return typeof mark === 'string' ? { type: mark } : mark;\n}\n\n// ---------------------------------------------------------------------------\n// Type guards\n// ---------------------------------------------------------------------------\n\n/** Check if a spec is a ChartSpec (has `mark` field, not a layer/table/graph). */\nexport function isChartSpec(spec: VizSpec | Record<string, unknown>): spec is ChartSpec {\n return 'mark' in spec && !('layer' in spec);\n}\n\n/** Check if a spec is a LayerSpec (has `layer` array). */\nexport function isLayerSpec(spec: VizSpec | Record<string, unknown>): spec is LayerSpec {\n return 'layer' in spec && Array.isArray((spec as Record<string, unknown>).layer);\n}\n\n/** Check if a spec is a TableSpec. */\nexport function isTableSpec(spec: VizSpec | Record<string, unknown>): spec is TableSpec {\n return 'type' in spec && (spec as Record<string, unknown>).type === 'table';\n}\n\n/** Check if a spec is a GraphSpec. */\nexport function isGraphSpec(spec: VizSpec | Record<string, unknown>): spec is GraphSpec {\n return 'type' in spec && (spec as Record<string, unknown>).type === 'graph';\n}\n\n/** Check if a spec is a SankeySpec. */\nexport function isSankeySpec(spec: VizSpec | Record<string, unknown>): spec is SankeySpec {\n return 'type' in spec && (spec as Record<string, unknown>).type === 'sankey';\n}\n\n/** Check if a spec is a TileMapSpec. */\nexport function isTileMapSpec(spec: VizSpec | Record<string, unknown>): spec is TileMapSpec {\n return 'type' in spec && (spec as Record<string, unknown>).type === 'tilemap';\n}\n\n/** Check if a spec is a BarListSpec. */\nexport function isBarListSpec(spec: VizSpec | Record<string, unknown>): spec is BarListSpec {\n return 'type' in spec && (spec as Record<string, unknown>).type === 'barlist';\n}\n\n/** Check if a spec is a GeoMapSpec. */\nexport function isGeoMapSpec(spec: VizSpec | Record<string, unknown>): spec is GeoMapSpec {\n return 'type' in spec && (spec as Record<string, unknown>).type === 'map';\n}\n\n// ---------------------------------------------------------------------------\n// Annotation type guards\n// ---------------------------------------------------------------------------\n\n/** Check if an annotation is a TextAnnotation. */\nexport function isTextAnnotation(annotation: Annotation): annotation is TextAnnotation {\n return annotation.type === 'text';\n}\n\n/** Check if an annotation is a RangeAnnotation. */\nexport function isRangeAnnotation(annotation: Annotation): annotation is RangeAnnotation {\n return annotation.type === 'range';\n}\n\n/**\n * Check if an annotation is a RefLineAnnotation. Also accepts the deprecated\n * `'rule'` type at runtime (pre-sugar-expansion input), since `annotation.type`\n * may not yet be narrowed to the canonical `'refline'` form.\n */\nexport function isRefLineAnnotation(annotation: Annotation): annotation is RefLineAnnotation {\n const type: string = annotation.type;\n return type === 'refline' || type === 'rule';\n}\n\n// ---------------------------------------------------------------------------\n// Display name mapping for accessibility\n// ---------------------------------------------------------------------------\n\n/** Human-readable display names for mark types (used in alt text, error messages). */\nexport const MARK_DISPLAY_NAMES: Record<MarkType, string> = {\n bar: 'Bar chart',\n line: 'Line chart',\n area: 'Area chart',\n point: 'Scatter plot',\n circle: 'Dot plot',\n arc: 'Pie chart', // overridden to \"Donut chart\" when innerRadius > 0\n text: 'Text chart',\n rule: 'Rule chart',\n tick: 'Tick plot',\n rect: 'Heatmap',\n lollipop: 'Lollipop chart',\n beeswarm: 'Beeswarm chart',\n range: 'Range plot',\n waffle: 'Waffle chart',\n calendar: 'Calendar heatmap',\n parliament: 'Parliament chart',\n histogram: 'Histogram',\n density: 'Density plot',\n};\n","/**\n * Color blindness simulation and palette distinguishability checks.\n *\n * Uses Brettel, Vienot, and Mollon (1997) simulation matrices for\n * protanopia, deuteranopia, and tritanopia.\n *\n * These are approximations suitable for checking palette accessibility,\n * not medical-grade simulations.\n */\n\nimport { rgb } from 'd3-color';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** The four common types of color vision deficiency. */\nexport type ColorBlindnessType = 'protanopia' | 'deuteranopia' | 'tritanopia' | 'achromatopsia';\n\n// ---------------------------------------------------------------------------\n// Simulation matrices\n// ---------------------------------------------------------------------------\n\n// 3x3 color transformation matrices for each deficiency type.\n// Applied in linear RGB space to simulate how colors appear.\n// Source: Brettel, Vienot & Mollon (1997), simplified.\n\ntype Matrix3x3 = readonly [\n readonly [number, number, number],\n readonly [number, number, number],\n readonly [number, number, number],\n];\n\nconst PROTAN_MATRIX: Matrix3x3 = [\n [0.567, 0.433, 0.0],\n [0.558, 0.442, 0.0],\n [0.0, 0.242, 0.758],\n];\n\nconst DEUTAN_MATRIX: Matrix3x3 = [\n [0.625, 0.375, 0.0],\n [0.7, 0.3, 0.0],\n [0.0, 0.3, 0.7],\n];\n\nconst TRITAN_MATRIX: Matrix3x3 = [\n [0.95, 0.05, 0.0],\n [0.0, 0.433, 0.567],\n [0.0, 0.475, 0.525],\n];\n\n// Total color blindness: every channel collapses to Rec. 709 relative\n// luminance, computed in linear RGB like the other matrices.\nconst ACHROMAT_MATRIX: Matrix3x3 = [\n [0.2126, 0.7152, 0.0722],\n [0.2126, 0.7152, 0.0722],\n [0.2126, 0.7152, 0.0722],\n];\n\nconst MATRICES: Record<ColorBlindnessType, Matrix3x3> = {\n protanopia: PROTAN_MATRIX,\n deuteranopia: DEUTAN_MATRIX,\n tritanopia: TRITAN_MATRIX,\n achromatopsia: ACHROMAT_MATRIX,\n};\n\n// ---------------------------------------------------------------------------\n// sRGB linearization helpers\n// ---------------------------------------------------------------------------\n\nfunction linearize(v: number): number {\n const s = v / 255;\n return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;\n}\n\nfunction delinearize(v: number): number {\n const s = v <= 0.0031308 ? v * 12.92 : 1.055 * v ** (1 / 2.4) - 0.055;\n return Math.round(Math.max(0, Math.min(255, s * 255)));\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Simulate how a color appears under a given color blindness type.\n * Returns a hex color string.\n */\nexport function simulateColorBlindness(color: string, type: ColorBlindnessType): string {\n const c = rgb(color);\n if (c == null) return color;\n\n const lin = [linearize(c.r), linearize(c.g), linearize(c.b)];\n const m = MATRICES[type];\n\n const r = m[0][0] * lin[0] + m[0][1] * lin[1] + m[0][2] * lin[2];\n const g = m[1][0] * lin[0] + m[1][1] * lin[1] + m[1][2] * lin[2];\n const b = m[2][0] * lin[0] + m[2][1] * lin[1] + m[2][2] * lin[2];\n\n return rgb(delinearize(r), delinearize(g), delinearize(b)).formatHex();\n}\n\n/**\n * Check if colors in a palette are distinguishable under a given\n * color blindness type.\n *\n * Uses a minimum perceptual distance threshold in simulated space.\n * Returns true if all pairs of colors are sufficiently different.\n */\nexport function checkPaletteDistinguishability(\n colors: string[],\n type: ColorBlindnessType,\n minDistance = 30,\n): boolean {\n const simulated = colors.map((c) => {\n const s = rgb(simulateColorBlindness(c, type));\n return s ? [s.r, s.g, s.b] : [0, 0, 0];\n });\n\n for (let i = 0; i < simulated.length; i++) {\n for (let j = i + 1; j < simulated.length; j++) {\n const dr = simulated[i][0] - simulated[j][0];\n const dg = simulated[i][1] - simulated[j][1];\n const db = simulated[i][2] - simulated[j][2];\n const dist = Math.sqrt(dr * dr + dg * dg + db * db);\n if (dist < minDistance) return false;\n }\n }\n\n return true;\n}\n","/**\n * WCAG contrast ratio utilities.\n *\n * Uses d3-color for color space parsing and manipulation.\n * All functions accept CSS color strings (hex, rgb, hsl, named colors).\n */\n\nimport { rgb } from 'd3-color';\n\n// ---------------------------------------------------------------------------\n// Relative luminance (WCAG 2.1)\n// ---------------------------------------------------------------------------\n\n/**\n * Compute the relative luminance of a color per WCAG 2.1 definition.\n * https://www.w3.org/TR/WCAG21/#dfn-relative-luminance\n */\nexport function relativeLuminance(color: string): number {\n const c = rgb(color);\n if (c == null) return 0;\n\n const srgb = [c.r / 255, c.g / 255, c.b / 255];\n const linear = srgb.map((v) => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4));\n return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Check whether a color string parses to a fully opaque color.\n * Contrast ratios are only meaningful against opaque backgrounds; the\n * default chart background ('transparent') defers to the host page and\n * can't be checked at compile time.\n */\nexport function isOpaqueColor(color: string): boolean {\n const c = rgb(color);\n return c != null && !Number.isNaN(c.r) && c.opacity === 1;\n}\n\n/**\n * Compute the WCAG contrast ratio between two colors.\n * Returns a value between 1 (identical) and 21 (black on white).\n */\nexport function contrastRatio(fg: string, bg: string): number {\n const l1 = relativeLuminance(fg);\n const l2 = relativeLuminance(bg);\n const lighter = Math.max(l1, l2);\n const darker = Math.min(l1, l2);\n return (lighter + 0.05) / (darker + 0.05);\n}\n\n/**\n * Pick a legible label color (white or near-black) for text placed on top of `bg`.\n *\n * Uses a perceptual luminance threshold rather than a strict 4.5:1 contrast\n * gate. WCAG's 4.5:1 ratio is calibrated for body text on a page; bold value\n * labels sitting on a saturated filled bar read fine at lower ratios. A pure\n * contrast-ratio gate sends mid-tone fills (e.g. slate `#94a3b8`, L≈0.36) to\n * dark text even though white reads cleaner on them.\n *\n * The threshold is mode-dependent because of simultaneous contrast: the same\n * bar fill looks lighter against a dark canvas than against a white page, so\n * dark text reads more grounded on it. Dark mode therefore uses a lower\n * threshold (L < 0.30) to pivot mid-tone fills to dark text sooner.\n *\n * - Light mode (L < 0.42 → white): white on saturated and mid-tone fills;\n * dark text only on genuinely light fills (`#b0b0b0` and lighter).\n * - Dark mode (L < 0.30 → white): saturated fills keep white; mid-tone fills\n * (slate `#94a3b8`, cyan `#06b6d4`, mid-grey) pivot to dark text.\n */\nexport function pickLabelColor(bg: string, darkMode = false): string {\n const WHITE = '#ffffff';\n const DARK = '#111111';\n const threshold = darkMode ? 0.3 : 0.42;\n return relativeLuminance(bg) < threshold ? WHITE : DARK;\n}\n\n/**\n * Check if two colors meet WCAG AA contrast requirements.\n * Normal text: 4.5:1, large text (18px+ bold or 24px+): 3:1.\n */\nexport function meetsAA(fg: string, bg: string, largeText = false): boolean {\n const ratio = contrastRatio(fg, bg);\n return largeText ? ratio >= 3 : ratio >= 4.5;\n}\n\n/**\n * Find an accessible variant of `baseColor` against `bg`.\n *\n * Preserves the hue and saturation of baseColor but adjusts lightness\n * until the target contrast ratio is met. Returns the original color\n * if it already meets the target.\n */\nexport function findAccessibleColor(baseColor: string, bg: string, targetRatio = 4.5): string {\n if (contrastRatio(baseColor, bg) >= targetRatio) {\n return baseColor;\n }\n\n const c = rgb(baseColor);\n if (c == null) return baseColor;\n\n const bgLum = relativeLuminance(bg);\n const baseLum = relativeLuminance(baseColor);\n\n // Try both directions: prefer the one matching bg luminance, but fall back\n // to the other if the base color is already at the extreme (e.g. white on\n // a medium-luminance background can't be lightened, so darken instead).\n const preferDarken = bgLum > 0.5;\n const directions = preferDarken ? [true, false] : [false, true];\n\n for (const darken of directions) {\n // Skip impossible directions: can't lighten white or darken black.\n if (!darken && baseLum > 0.95) continue;\n if (darken && baseLum < 0.05) continue;\n\n let lo = 0;\n let hi = 1;\n let best: string | null = null;\n\n for (let i = 0; i < 20; i++) {\n const mid = (lo + hi) / 2;\n const adjusted = darken\n ? rgb(c.r * (1 - mid), c.g * (1 - mid), c.b * (1 - mid))\n : rgb(c.r + (255 - c.r) * mid, c.g + (255 - c.g) * mid, c.b + (255 - c.b) * mid);\n\n const hex = adjusted.formatHex();\n const ratio = contrastRatio(hex, bg);\n\n if (ratio >= targetRatio) {\n best = hex;\n hi = mid;\n } else {\n lo = mid;\n }\n }\n\n if (best) return best;\n }\n\n return baseColor;\n}\n","export interface CssTokenDef {\n name: string;\n light: string;\n dark?: string;\n comment?: string;\n consumers?: string;\n section?: string;\n}\n\nexport const CSS_TOKEN_ROOT_SELECTORS: readonly string[] = [\n '.oc-root',\n '.oc-chart-root',\n '.oc-table-wrapper',\n '.oc-table-root',\n '.oc-graph-wrapper',\n '.oc-graph-root',\n '.oc-sankey-root',\n '.oc-tilemap-root',\n '.oc-barlist-root',\n '.oc-map-root',\n];\n\n/**\n * Text levels. Names invert across modes: in light mode \"muted\" sits at\n * a lighter zinc step than \"subtle\" because both are picked relative to\n * the active background, not from a fixed lightness ladder. The intent\n * is \"muted = first step away from primary text\"; \"subtle = next step\n * down\"; etc.\n *\n * Light mode: text=zinc-950, secondary=zinc-700, muted=zinc-500,\n * subtle=zinc-400, faint=zinc-300\n * Dark mode (dark.css): inverts the surface tokens but keeps the same\n * muted -> subtle -> faint progression away from primary.\n */\n\nexport const CSS_TOKENS: readonly CssTokenDef[] = [\n {\n name: '--oc-font-family',\n light:\n '\\n \"Inter Variable\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif',\n },\n {\n name: '--oc-font-mono',\n light: '\"JetBrains Mono\", \"Fira Code\", \"Cascadia Code\", monospace',\n },\n\n {\n section: 'Animation easing presets via CSS linear()',\n name: '--oc-ease-smooth',\n light:\n 'linear(\\n 0,\\n 0.157,\\n 0.438,\\n 0.64,\\n 0.766,\\n 0.85,\\n 0.906,\\n 0.941,\\n 0.964,\\n 0.978,\\n 0.988,\\n 0.994,\\n 0.998,\\n 1\\n )',\n },\n {\n name: '--oc-ease-snappy',\n light:\n 'linear(\\n 0,\\n 0.012,\\n 0.048,\\n 0.108,\\n 0.194,\\n 0.302,\\n 0.426,\\n 0.559,\\n 0.69,\\n 0.808,\\n 0.905,\\n 0.973,\\n 1.013,\\n 1.028,\\n 1.023,\\n 1.006,\\n 0.984,\\n 0.966,\\n 0.957,\\n 0.957,\\n 0.964,\\n 0.975,\\n 0.986,\\n 0.995,\\n 1,\\n 1.003,\\n 1.002,\\n 1,\\n 0.998,\\n 0.998,\\n 0.999,\\n 1\\n )',\n consumers: 'EASE_VAR_MAP in vanilla renderers',\n },\n\n { section: 'Animation timing defaults', name: '--oc-animation-duration', light: '450ms' },\n { name: '--oc-animation-stagger', light: '30ms' },\n { name: '--oc-annotation-delay', light: '150ms' },\n\n { section: 'Typography scale (editorial design system)', name: '--oc-title-size', light: '26px' },\n {\n name: '--oc-title-weight',\n light: '600',\n comment: 'one ladder: 400 regular / 500 medium / 600 semibold / 700 bold',\n },\n { name: '--oc-title-tracking', light: '-0.022em' },\n { name: '--oc-subtitle-size', light: '14px' },\n { name: '--oc-subtitle-weight', light: '400' },\n { name: '--oc-source-size', light: '11px' },\n { name: '--oc-source-weight', light: '400' },\n { name: '--oc-body-size', light: '13px' },\n { name: '--oc-eyebrow-size', light: '11px' },\n { name: '--oc-eyebrow-weight', light: '500' },\n { name: '--oc-eyebrow-tracking', light: '0.08em' },\n\n { section: 'Surfaces (light mode defaults)', name: '--oc-bg', light: '#ffffff', dark: '#09090b' },\n { name: '--oc-card', light: '#ffffff', dark: '#111113' },\n {\n name: '--oc-secondary',\n light: '#f4f4f5',\n dark: '#27272a',\n comment: 'zinc-100, raised surface',\n },\n\n {\n section: `Text levels. Names invert across modes: in light mode \"muted\" sits at\n * a lighter zinc step than \"subtle\" because both are picked relative to\n * the active background, not from a fixed lightness ladder. The intent\n * is \"muted = first step away from primary text\"; \"subtle = next step\n * down\"; etc.\n *\n * Light mode: text=zinc-950, secondary=zinc-700, muted=zinc-500,\n * subtle=zinc-400, faint=zinc-300\n * Dark mode (dark.css): inverts the surface tokens but keeps the same\n * muted -> subtle -> faint progression away from primary.`,\n name: '--oc-text',\n light: '#09090b',\n dark: '#f7f8f8',\n },\n { name: '--oc-text-secondary', light: '#3f3f46', dark: '#d0d6e0' },\n { name: '--oc-text-muted', light: '#71717a', dark: '#a1a1aa' },\n { name: '--oc-text-faint', light: '#d4d4d8', dark: '#52525b' },\n\n {\n section: `Derived neutral ramp. Named by the share of *text* in a text-toward-\n * background mix: 100 is nearly the background, 800 is nearly the text.\n * These static values are the mix against the default surfaces; mounts\n * restamp them from ResolvedTheme.colors.neutral, which a custom warm or\n * cool theme derives from its own text/background pair.`,\n name: '--oc-gray-100',\n light: '#f0f0f0',\n dark: '#171719',\n },\n { name: '--oc-gray-200', light: '#e1e1e2', dark: '#262627' },\n { name: '--oc-gray-300', light: '#c9c9c9', dark: '#3d3e3f' },\n { name: '--oc-gray-400', light: '#9d9d9d', dark: '#68696a' },\n { name: '--oc-gray-600', light: '#787879', dark: '#8c8c8d' },\n { name: '--oc-gray-800', light: '#474748', dark: '#bcbcbd' },\n\n {\n section: 'Lines',\n name: '--oc-gridline',\n light: 'rgba(0, 0, 0, 0.08)',\n dark: 'rgba(255, 255, 255, 0.06)',\n },\n {\n name: '--oc-axis',\n light: 'rgba(0, 0, 0, 0.14)',\n dark: 'rgba(255, 255, 255, 0.14)',\n comment: 'hairline = theme.colors.hairline, not the tick-label ink',\n },\n {\n name: '--oc-border',\n light: 'rgba(0, 0, 0, 0.08)',\n dark: 'rgba(255, 255, 255, 0.1)',\n },\n {\n name: '--oc-border-radius',\n light: '8px',\n comment: 'containers and tooltips; marks use --oc-radius-sm',\n },\n\n { section: 'Radius ladder', name: '--oc-radius-sm', light: '2px' },\n { name: '--oc-radius-md', light: '6px' },\n { name: '--oc-radius-lg', light: 'var(--oc-border-radius)' },\n { name: '--oc-radius-full', light: '999px' },\n\n {\n section: 'Brand and semantic',\n name: '--oc-accent',\n light: '#06b6d4',\n dark: '#06b6d4',\n },\n {\n name: '--oc-accent-strong',\n light: '#0891b2',\n dark: '#06b6d4',\n comment: 'darker cyan for line strokes on light bg',\n consumers: 'stamped via adaptForLightLineStroke',\n },\n {\n name: '--oc-positive',\n light: '#15803d',\n dark: '#4ade80',\n comment: 'kept equal to DEFAULT_THEME.colors.positive (token-theme-parity test)',\n },\n { name: '--oc-negative', light: '#dc2626', dark: '#f87171' },\n {\n name: '--oc-positive-tint',\n light: 'color-mix(in srgb, var(--oc-positive) 10%, var(--oc-bg))',\n comment: 'delta chip background',\n },\n {\n name: '--oc-negative-tint',\n light: 'color-mix(in srgb, var(--oc-negative) 10%, var(--oc-bg))',\n },\n { name: '--oc-focus', light: '#3b82f6', dark: '#60a5fa' },\n {\n name: '--oc-focus-ring',\n light: 'rgba(59, 130, 246, 0.1)',\n comment: 'static, intentionally NOT derived from --oc-focus and NOT flipped in dark mode',\n },\n {\n name: '--oc-focus-ring-strong',\n light: 'rgba(59, 130, 246, 0.25)',\n },\n {\n name: '--oc-editable-hover',\n light: 'rgba(79, 70, 229, 0.35)',\n comment: 'edit-mode hover outline, indigo one-off',\n },\n\n {\n section: 'Spacing scale (4px base) — only --oc-space-2/4 are consumed (JS-stamped)',\n name: '--oc-space-2',\n light: '8px',\n consumers:\n 'stamped at mount from theme.spacing (theme-tokens.ts); component CSS must NOT consume these',\n },\n {\n name: '--oc-space-4',\n light: '16px',\n consumers:\n 'stamped at mount from theme.spacing (theme-tokens.ts); component CSS must NOT consume these',\n },\n\n {\n section: `Hover language. The hovered element goes to full ink; the rest of its\n * group drops to --oc-hover-dim over --oc-hover-duration. Maps override the\n * dim locally (3000 county paths at 0.3 reads as a blackout, not a hint).`,\n name: '--oc-hover-dim',\n light: '0.3',\n },\n { name: '--oc-hover-duration', light: '140ms' },\n { name: '--oc-map-hover-dim', light: '0.75' },\n\n {\n section: 'Interactive states',\n name: '--oc-hover-bg',\n light: 'rgba(0, 0, 0, 0.025)',\n dark: 'rgba(255, 255, 255, 0.05)',\n },\n {\n name: '--oc-tooltip-bg',\n light: 'rgba(255, 255, 255, 0.88)',\n dark: 'rgba(17, 17, 19, 0.92)',\n },\n {\n name: '--oc-tooltip-border',\n light: 'rgba(0, 0, 0, 0.08)',\n dark: 'rgba(255, 255, 255, 0.08)',\n },\n {\n name: '--oc-tooltip-shadow',\n light: '0 2px 8px rgba(0, 0, 0, 0.08), 0 0 1px rgba(0, 0, 0, 0.12)',\n dark: '0 2px 8px rgba(0, 0, 0, 0.3), 0 0 1px rgba(0, 0, 0, 0.4)',\n },\n { name: '--oc-tooltip-text', light: '#09090b', dark: '#f7f8f8' },\n { name: '--oc-legend-text', light: '#3f3f46', dark: '#d0d6e0' },\n];\n\nconst TOKEN_MAP = new Map(CSS_TOKENS.map((t) => [t.name, t]));\n\nexport function cssTokenDefault(name: string, mode: 'light' | 'dark'): string {\n const token = TOKEN_MAP.get(name);\n if (!token) throw new Error(`Unknown CSS token: ${name}`);\n if (mode === 'dark' && token.dark !== undefined) return token.dark;\n return token.light;\n}\n","/**\n * Derived neutral ramp.\n *\n * Every secondary gray in the system is the theme's text color mixed toward\n * its background, rather than a fixed zinc ladder. A custom warm or cool\n * theme therefore gets warm or cool grays for free instead of zinc showing\n * up uninvited beside its palette.\n *\n * The ramp is computed once in `resolveTheme`/`adaptTheme` and carried on\n * `ResolvedTheme.colors.neutral`. Mounts stamp the `--oc-gray-*`,\n * `--oc-text-secondary`, `--oc-text-faint` and `--oc-border` CSS tokens from\n * it. The direction is one-way: theme -> CSS. Engine code reads\n * `theme.colors.neutral.*` and never `cssTokenDefault`.\n */\n\nimport { cssTokenDefault } from '../styles/token-definitions';\nimport { isOpaqueColor } from './contrast';\n\n/**\n * Neutral steps, named by the fraction of *text* in the text/background mix:\n * `100` is nearly the background, `800` is nearly the text.\n */\nexport interface NeutralRamp {\n 100: string;\n 200: string;\n 300: string;\n 400: string;\n 600: string;\n 800: string;\n /** Body-level secondary text. Same value as `800`. */\n secondary: string;\n /** Faintest legible ink (disabled states, empty cells). Same value as `300`. */\n faint: string;\n /** Container border / hairline on non-chart surfaces. Same value as `100`. */\n border: string;\n /**\n * The opaque canvas the theme paints on. Equal to the theme background when\n * that background has a color of its own; a transparent theme resolves to\n * the static `--oc-bg` token for the mode. Knockout rings, stacked-segment\n * seams, table cell fills and graph node rings all read this so they are cut\n * in the same color the surface actually renders as.\n */\n surface: string;\n}\n\n/** Mix weight (share of `text`) for each numbered step. */\nconst STEPS = [\n [100, 0.06],\n [200, 0.12],\n [300, 0.22],\n [400, 0.4],\n [600, 0.55],\n [800, 0.75],\n] as const;\n\n/** Parse `#rgb`/`#rrggbb` to an [r, g, b] triple, or null when unparseable. */\nfunction parseHex(color: string): [number, number, number] | null {\n const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color.trim());\n if (!m) return null;\n const hex =\n m[1].length === 3\n ? m[1]\n .split('')\n .map((c) => c + c)\n .join('')\n : m[1];\n return [\n parseInt(hex.slice(0, 2), 16),\n parseInt(hex.slice(2, 4), 16),\n parseInt(hex.slice(4, 6), 16),\n ];\n}\n\n/** Mix `text` toward `bg` in sRGB; `t` is the share of `text` in the result. */\nfunction mix(text: [number, number, number], bg: [number, number, number], t: number): string {\n const c = (i: number) => Math.round(text[i] * t + bg[i] * (1 - t));\n const hex = (n: number) => Math.max(0, Math.min(255, n)).toString(16).padStart(2, '0');\n return `#${hex(c(0))}${hex(c(1))}${hex(c(2))}`;\n}\n\n/** The ramp the static CSS token defaults carry, for a given mode. */\nfunction staticRamp(mode: 'light' | 'dark'): NeutralRamp {\n return {\n 100: cssTokenDefault('--oc-gray-100', mode),\n 200: cssTokenDefault('--oc-gray-200', mode),\n 300: cssTokenDefault('--oc-gray-300', mode),\n 400: cssTokenDefault('--oc-gray-400', mode),\n 600: cssTokenDefault('--oc-gray-600', mode),\n 800: cssTokenDefault('--oc-gray-800', mode),\n secondary: cssTokenDefault('--oc-text-secondary', mode),\n faint: cssTokenDefault('--oc-text-faint', mode),\n border: cssTokenDefault('--oc-border', mode),\n surface: cssTokenDefault('--oc-bg', mode),\n };\n}\n\n/**\n * Derive the neutral ramp for a text/background pair.\n *\n * Falls back to the static CSS token defaults when the background has no\n * intrinsic color (`transparent`, `none`, alpha-zero) or either color is not\n * a plain hex — the default theme's background is transparent, so this\n * fallback is the common path and is what keeps the theme and the generated\n * `tokens.css` in agreement.\n */\nexport function deriveNeutralRamp(text: string, bg: string, isDark = false): NeutralRamp {\n const mode = isDark ? 'dark' : 'light';\n // The surface is the background itself whenever that background paints\n // something; only a transparent/none background falls back to the token.\n const surface = isOpaqueColor(bg) ? bg : cssTokenDefault('--oc-bg', mode);\n if (!isOpaqueColor(bg)) return staticRamp(mode);\n const t = parseHex(text);\n const b = parseHex(bg);\n if (!t || !b) return { ...staticRamp(mode), surface };\n\n const ramp = Object.fromEntries(STEPS.map(([step, w]) => [step, mix(t, b, w)])) as Pick<\n NeutralRamp,\n 100 | 200 | 300 | 400 | 600 | 800\n >;\n return {\n ...ramp,\n secondary: ramp[800],\n faint: ramp[300],\n border: ramp[100],\n surface,\n };\n}\n","/**\n * Color palettes for @opendata-ai.\n *\n * Categorical palette leads with cyan and uses an OKLCH-balanced multi-hue\n * ramp tuned to L≈0.70, C≈0.13–0.15. Hex values are precomputed sRGB\n * (rather than raw `oklch()` strings) because raw OKLCH strings parse\n * unreliably through d3-color and the dark-mode adapter's hsl-based\n * binary search. Source OKLCH values are documented inline so the ramp\n * can be regenerated if needed.\n *\n * Convention for low-cardinality charts (1 / 2-4 / 5+ series) is available\n * as an opt-in via `theme.seriesStrategy`. The default strategy is 'palette'\n * (full categorical ramp always). The accent-neutral convention is:\n * - 1 series: cyan only\n * - 2-4 series: cyan + zinc-400/500/600 (achromatic)\n * - 5+ series: full multi-hue ramp below\n *\n * Sequential palettes: 5-7 stops from light to dark.\n * Diverging palettes: 7 stops with a neutral midpoint.\n */\n\n// ---------------------------------------------------------------------------\n// Achromatic ramp (zinc-based, dark-mode canonical)\n// ---------------------------------------------------------------------------\n\n/**\n * Achromatic surface and supporting-series ramp. Values are the canonical\n * dark-mode tokens; light-mode equivalents are derived per token in\n * `theme/dark-mode.ts` and the CSS partials, not mirrored here.\n */\nexport const ACHROMATIC_RAMP = {\n fg: '#f7f8f8', // primary text\n fg2: '#d0d6e0', // body text\n fgMuted: '#a1a1aa', // secondary series (zinc-400)\n fgSubtle: '#71717a', // tertiary series (zinc-500)\n fgFaint: '#52525b', // quaternary series (zinc-600)\n secondary: '#27272a', // hover / raised surface\n card: '#111113', // card surface\n bg: '#09090b', // canvas\n} as const;\n\n// ---------------------------------------------------------------------------\n// Categorical\n// ---------------------------------------------------------------------------\n\n/**\n * Quiet ordered editorial categorical palette: six hues, cyan in slot 1.\n *\n * Construction rules (the contract; the hexes below are output, not input):\n * - Hue order cyan 208 -> ochre 70 -> blue 262 -> rose 350 -> green 155 ->\n * violet 300, so every adjacent pair including the wrap is >= 80° apart.\n * - Lightness varies rather than matching: colour-vision deficiency\n * collapses hue and only lightness survives, so a matched-lightness ramp\n * fails the repo's own Brettel simulator on ochre/rose and blue/violet.\n * - Strokes carry more chroma than fills; a fill is the same hue one step\n * lighter and quieter, so a bar or slice never shouts as loud as a line.\n * - Dark variants raise L and trim chroma instead of reusing the light hexes.\n *\n * Hex values are precomputed sRGB via the Ottosson OKLab -> linear sRGB ->\n * sRGB pipeline (raw `oklch()` strings parse unreliably through d3-color and\n * the dark-mode adapter's hsl-based binary search). Each entry carries its\n * *measured* OKLCH so the ramp can be regenerated; do not edit hexes directly.\n *\n * Every array here clears `checkPaletteDistinguishability` at the default\n * minDistance 30 for deuteranopia and protanopia — see\n * `colors/__tests__/palette-a11y.test.ts`, which is the guard on any change.\n */\nexport const CATEGORICAL_PALETTE = [\n '#06b6d4', // cyan — oklch(0.715 0.126 215) — sRGB literal, primary accent\n '#e29e47', // ochre — oklch(0.750 0.130 70)\n '#4170cb', // blue — oklch(0.559 0.150 262)\n '#b2417f', // rose — oklch(0.550 0.161 350)\n '#3ca368', // green — oklch(0.640 0.130 155)\n '#a584dc', // violet — oklch(0.679 0.130 300)\n] as const;\n\n/**\n * Fill counterparts of {@link CATEGORICAL_PALETTE}: same hues, ~0.03 lighter\n * and ~0.03 lower chroma. Used for area-filling marks (bar, area, arc,\n * waffle, calendar, rect) so large blocks of colour read quieter than the\n * strokes of a line chart drawn beside them.\n */\nexport const CATEGORICAL_FILL_PALETTE = [\n '#51bccc', // cyan — oklch(0.739 0.100 209)\n '#dda96b', // ochre — oklch(0.770 0.100 70)\n '#587fc8', // blue — oklch(0.601 0.120 262)\n '#bf6291', // rose — oklch(0.619 0.131 350)\n '#62ab7d', // green — oklch(0.681 0.101 155)\n '#ae96da', // violet — oklch(0.719 0.100 300)\n] as const;\n\n/** Dark-mode stroke palette: L raised ~0.10, chroma trimmed. */\nexport const CATEGORICAL_PALETTE_DARK = [\n '#22d3ee', // cyan — oklch(0.797 0.134 212) — sRGB literal\n '#f9be78', // ochre — oklch(0.840 0.111 70)\n '#6591e1', // blue — oklch(0.660 0.129 262)\n '#d771a4', // rose — oklch(0.680 0.140 350)\n '#6ebf8c', // green — oklch(0.739 0.109 155)\n '#bca1ed', // violet — oklch(0.760 0.110 300)\n] as const;\n\n/** Dark-mode fill palette: the dark strokes one step quieter. */\nexport const CATEGORICAL_FILL_PALETTE_DARK = [\n '#72cedc', // cyan — oklch(0.800 0.090 208)\n '#f3c898', // ochre — oklch(0.859 0.079 70)\n '#7698d6', // blue — oklch(0.679 0.100 262)\n '#d281a8', // rose — oklch(0.700 0.111 350)\n '#82ba95', // green — oklch(0.740 0.079 155)\n '#c2afe7', // violet — oklch(0.789 0.081 300)\n] as const;\n\n/**\n * Slots 7-12. Only reached past slot 6, and reaching it emits a warning:\n * a chart with more than six categorical series is a chart that should be\n * bucketed, faceted, or direct-labelled instead.\n *\n * Built by interleaving: each base hue + 40° (which lands each entry near the\n * midpoint of a gap in the base ramp) with lightness alternating ±0.12 from\n * its base slot. The array is rotated so slot 7 sits 105° from cyan and never\n * neighbours it on a seven-slice pie wrap; the two entries that do land near\n * cyan in hue are pushed to slots 10 and 12.\n */\nexport const CATEGORICAL_EXTENDED_PALETTE = [\n '#8e8f19', // olive — oklch(0.629 0.130 110)\n '#b086eb', // periwinkle— oklch(0.701 0.149 302)\n '#ac3225', // brick — oklch(0.501 0.161 30)\n '#00bfc0', // teal — oklch(0.729 0.124 195)\n '#a5538c', // mulberry — oklch(0.560 0.129 340)\n '#65b1f6', // sky — oklch(0.740 0.126 248)\n] as const;\n\n/**\n * Measured OKLCH hue (deg) of each {@link CATEGORICAL_PALETTE} slot, in slot\n * order. Exported so the adjacency test can assert the ordering rule without\n * re-deriving OKLCH from the hexes.\n */\nexport const CATEGORICAL_HUES = [215.2, 70.0, 262.1, 349.7, 154.8, 300.2] as const;\n\n/** Measured OKLCH lightness of each {@link CATEGORICAL_PALETTE} slot. */\nexport const CATEGORICAL_LIGHTNESS = [0.715, 0.75, 0.559, 0.55, 0.64, 0.679] as const;\n\nexport type CategoricalPalette = typeof CATEGORICAL_PALETTE;\n\n// ---------------------------------------------------------------------------\n// Sequential\n// ---------------------------------------------------------------------------\n\n/** Sequential palette definition: an array of color stops from light to dark. */\nexport interface SequentialPalette {\n readonly name: string;\n readonly stops: readonly string[];\n}\n\nexport const SEQUENTIAL_BLUE: SequentialPalette = {\n name: 'blue',\n stops: ['#deebf7', '#c6dbef', '#9ecae1', '#6baed6', '#3182bd', '#08519c'],\n} as const;\n\nexport const SEQUENTIAL_GREEN: SequentialPalette = {\n name: 'green',\n stops: ['#e5f5e0', '#c7e9c0', '#a1d99b', '#74c476', '#31a354', '#006d2c'],\n} as const;\n\nexport const SEQUENTIAL_ORANGE: SequentialPalette = {\n name: 'orange',\n stops: ['#fee6ce', '#fdd0a2', '#fdae6b', '#fd8d3c', '#e6550d', '#a63603'],\n} as const;\n\nexport const SEQUENTIAL_PURPLE: SequentialPalette = {\n name: 'purple',\n stops: ['#efedf5', '#dadaeb', '#bcbddc', '#9e9ac8', '#756bb1', '#54278f'],\n} as const;\n\nexport const SEQUENTIAL_TEAL: SequentialPalette = {\n name: 'teal',\n stops: ['#06b6d4', '#05a3be', '#0490a8', '#037d92', '#026a7c', '#015766', '#004450'],\n} as const;\n\n/** All sequential palettes keyed by name. */\nexport const SEQUENTIAL_PALETTES: Record<string, string[]> = {\n blue: [...SEQUENTIAL_BLUE.stops],\n green: [...SEQUENTIAL_GREEN.stops],\n orange: [...SEQUENTIAL_ORANGE.stops],\n purple: [...SEQUENTIAL_PURPLE.stops],\n teal: [...SEQUENTIAL_TEAL.stops],\n};\n\n// ---------------------------------------------------------------------------\n// Diverging\n// ---------------------------------------------------------------------------\n\n/** Diverging palette definition: an array of color stops with a neutral midpoint. */\nexport interface DivergingPalette {\n readonly name: string;\n readonly stops: readonly string[];\n}\n\nexport const DIVERGING_RED_BLUE: DivergingPalette = {\n name: 'redBlue',\n stops: [\n '#b2182b', // strong red\n '#d6604d', // medium red\n '#f4a582', // light red\n '#f7f7f7', // neutral\n '#92c5de', // light blue\n '#4393c3', // medium blue\n '#2166ac', // strong blue\n ],\n} as const;\n\nexport const DIVERGING_BROWN_TEAL: DivergingPalette = {\n name: 'brownTeal',\n stops: [\n '#8c510a', // strong brown\n '#bf812d', // medium brown\n '#dfc27d', // light brown\n '#f6e8c3', // neutral\n '#80cdc1', // light teal\n '#35978f', // medium teal\n '#01665e', // strong teal\n ],\n} as const;\n\n/** All diverging palettes keyed by name. */\nexport const DIVERGING_PALETTES: Record<string, string[]> = {\n redBlue: [...DIVERGING_RED_BLUE.stops],\n brownTeal: [...DIVERGING_BROWN_TEAL.stops],\n};\n\n// ---------------------------------------------------------------------------\n// Scheme name resolution (spec-level `scale: { scheme }` support)\n// ---------------------------------------------------------------------------\n\n/**\n * Vega-Lite scheme-name aliases mapped onto the openchart palettes.\n * Keys are lowercase. Only schemes with a faithful local equivalent are\n * aliased; unknown names fail validation with the supported list rather\n * than silently substituting an unrelated ramp.\n */\nconst VL_SCHEME_ALIASES: Record<string, string> = {\n blues: 'blue',\n greens: 'green',\n oranges: 'orange',\n purples: 'purple',\n teals: 'teal',\n redblue: 'redBlue',\n brownteal: 'brownTeal',\n // ColorBrewer BrBG, which brownTeal mirrors\n brownbluegreen: 'brownTeal',\n category10: 'categorical',\n tableau10: 'categorical',\n};\n\n/**\n * Resolve a scheme name to its palette stops. Accepts openchart palette names\n * (blue, green, orange, purple, teal, redBlue, brownTeal, categorical) and\n * common Vega-Lite aliases (blues, greens, ..., redblue, category10).\n * Case-insensitive. Returns undefined for unknown names.\n */\nexport function resolveSchemeName(name: string): string[] | undefined {\n const canonical =\n SEQUENTIAL_PALETTES[name] || DIVERGING_PALETTES[name]\n ? name\n : (VL_SCHEME_ALIASES[name.toLowerCase()] ?? name.toLowerCase());\n if (canonical === 'categorical') return [...CATEGORICAL_PALETTE];\n return SEQUENTIAL_PALETTES[canonical] ?? DIVERGING_PALETTES[canonical];\n}\n\n/** Scheme names accepted by `scale: { scheme }`, for validation messages. */\nexport const SUPPORTED_SCHEME_NAMES: readonly string[] = [\n ...Object.keys(SEQUENTIAL_PALETTES),\n ...Object.keys(DIVERGING_PALETTES),\n 'categorical',\n ...Object.keys(VL_SCHEME_ALIASES),\n];\n","/**\n * Dark mode theme adaptation.\n *\n * Preserves hue of colors while adjusting lightness and saturation\n * to maintain the same relative contrast ratios on a dark background.\n */\n\nimport { hsl, rgb } from 'd3-color';\nimport { contrastRatio, isOpaqueColor } from '../colors/contrast';\nimport { deriveNeutralRamp } from '../colors/neutral';\nimport {\n ACHROMATIC_RAMP,\n CATEGORICAL_FILL_PALETTE_DARK,\n CATEGORICAL_PALETTE_DARK,\n} from '../colors/palettes';\nimport type { ResolvedTheme } from '../types/theme';\nimport { DEFAULT_THEME } from './defaults';\n\n// ---------------------------------------------------------------------------\n// Dark mode background\n// ---------------------------------------------------------------------------\n\n/** Default dark mode background color (zinc-based canvas). */\nconst DARK_BG = ACHROMATIC_RAMP.bg;\n/** Default dark mode text color. */\nconst DARK_TEXT = ACHROMATIC_RAMP.fg;\n\n// ---------------------------------------------------------------------------\n// Color adaptation\n// ---------------------------------------------------------------------------\n\n/**\n * Adapt a single color for dark mode.\n *\n * Preserves the hue and adjusts lightness/saturation so the adapted\n * color has the same contrast ratio against darkBg as the original\n * had against lightBg.\n */\nexport function adaptColorForDarkMode(color: string, lightBg: string, darkBg: string): string {\n // Adapter only handles hex/rgb-style inputs. Raw oklch() and other\n // CSS Color 4 strings parse unreliably through d3-color in happy-dom,\n // so guard early instead of silently falling back to a default.\n if (rgb(color) == null) {\n if (typeof console !== 'undefined' && console.warn) {\n console.warn(\n `[openchart] adaptColorForDarkMode: unparseable color \"${color}\", returning unchanged. Use precomputed sRGB hex.`,\n );\n }\n return color;\n }\n const originalRatio = contrastRatio(color, lightBg);\n const c = hsl(color);\n if (c == null || Number.isNaN(c.h)) {\n // Achromatic or invalid color: just adjust lightness\n const r = rgb(color);\n if (r == null) return color;\n const darkBgLum = _luminanceFromHex(darkBg);\n const isLight = darkBgLum < 0.5;\n if (isLight) return color;\n // Invert the lightness\n const inverted = hsl(color);\n if (inverted == null) return color;\n inverted.l = 1 - inverted.l;\n return inverted.formatHex();\n }\n\n // Binary search for lightness that gives equivalent contrast on dark bg\n let lo = 0.0;\n let hi = 1.0;\n let bestColor = color;\n let bestDiff = Infinity;\n\n for (let i = 0; i < 20; i++) {\n const mid = (lo + hi) / 2;\n const candidate = hsl(c.h, c.s, mid);\n const hex = candidate.formatHex();\n const ratio = contrastRatio(hex, darkBg);\n const diff = Math.abs(ratio - originalRatio);\n\n if (diff < bestDiff) {\n bestDiff = diff;\n bestColor = hex;\n }\n\n if (ratio < originalRatio) {\n // Need more contrast = more lightness on dark bg\n lo = mid;\n } else {\n hi = mid;\n }\n }\n\n return bestColor;\n}\n\n/** Quick luminance estimation from a hex color. */\nfunction _luminanceFromHex(color: string): number {\n const c = rgb(color);\n if (c == null) return 0;\n return (0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b) / 255;\n}\n\n// ---------------------------------------------------------------------------\n// Full theme adaptation\n// ---------------------------------------------------------------------------\n\n/**\n * Adapt an entire resolved theme for dark mode.\n *\n * Swaps background/text, adapts categorical and annotation colors,\n * adjusts gridline and axis colors for the dark background.\n */\nexport function adaptTheme(theme: ResolvedTheme): ResolvedTheme {\n const pairs = theme._tokenPairs;\n const inputBg = theme.colors.background;\n // A non-opaque background ('transparent', 'none', alpha-zero rgba) preserves\n // the background token but must still adopt dark text/gridline/axis colors —\n // the container is dark, not the chart canvas.\n const isTransparent = !isOpaqueColor(inputBg);\n const alreadyDark = isTransparent || _luminanceFromHex(inputBg) < 0.2;\n\n // Preserve user-supplied background (including transparent) but always\n // apply dark-mode text/axis/gridline colors so labels are readable on the\n // dark host surface.\n const darkBg = pairs?.['colors.background']\n ? pairs['colors.background'].dark\n : alreadyDark\n ? inputBg\n : DARK_BG;\n const darkMuted = ACHROMATIC_RAMP.fgMuted;\n\n // Per the \"sensible defaults, full override\" convention (see spec-grammar),\n // dark-mode adaptation only swaps in dark defaults for colors the spec\n // left at their light-mode default. An explicit override is preserved.\n // When a TokenValue pair has an explicit dark value, use it directly.\n const light = DEFAULT_THEME.colors;\n const lightChrome = DEFAULT_THEME.chrome;\n const overridden = <T>(current: T, lightDefault: T, darkDefault: T): T =>\n current === lightDefault ? darkDefault : current;\n\n const darkText = pairs?.['colors.text']\n ? pairs['colors.text'].dark\n : overridden(theme.colors.text, light.text, DARK_TEXT);\n const darkGridline = pairs?.['colors.gridline']\n ? pairs['colors.gridline'].dark\n : overridden(theme.colors.gridline, light.gridline, 'rgba(255,255,255,0.06)');\n const darkHairline = pairs?.['colors.hairline']\n ? pairs['colors.hairline'].dark\n : overridden(theme.colors.hairline, light.hairline, 'rgba(255,255,255,0.14)');\n // axis is also tick-label fill — needs WCAG AA contrast on dark bg.\n // Zinc-400 (`#a1a1aa`) hits ~6:1 against #09090b.\n const darkAxis = pairs?.['colors.axis']\n ? pairs['colors.axis'].dark\n : overridden(theme.colors.axis, light.axis, '#a1a1aa');\n\n // The categorical palette has purpose-built dark variants (L raised ~0.10,\n // chroma trimmed) rather than reusing the light hexes or running them\n // through contrast-equivalence, which dulls cyan into teal. A palette the\n // spec overrode passes through untouched, same rule as every other color.\n const categorical = overridden(theme.colors.categorical, light.categorical, [\n ...CATEGORICAL_PALETTE_DARK,\n ]);\n const categoricalFill = overridden(theme.colors.categoricalFill, light.categoricalFill, [\n ...CATEGORICAL_FILL_PALETTE_DARK,\n ]);\n\n return {\n ...theme,\n isDark: true,\n colors: {\n ...theme.colors,\n background: darkBg,\n text: darkText,\n gridline: darkGridline,\n axis: darkAxis,\n annotationFill: pairs?.['colors.annotationFill']\n ? pairs['colors.annotationFill'].dark\n : overridden(theme.colors.annotationFill, light.annotationFill, 'rgba(255,255,255,0.06)'),\n annotationText: pairs?.['colors.annotationText']\n ? pairs['colors.annotationText'].dark\n : overridden(theme.colors.annotationText, light.annotationText, darkMuted),\n categorical,\n categoricalFill,\n hairline: darkHairline,\n positive: pairs?.['colors.positive']\n ? pairs['colors.positive'].dark\n : overridden(theme.colors.positive, light.positive, '#4ade80'),\n negative: pairs?.['colors.negative']\n ? pairs['colors.negative'].dark\n : overridden(theme.colors.negative, light.negative, '#f87171'),\n neutral: deriveNeutralRamp(darkText, darkBg, true),\n },\n chrome: {\n // Eyebrow keeps its accent tint (cyan in both modes) unless the user\n // gave an explicit dark value; the other chrome elements desaturate\n // to a muted gray on the dark canvas. Each color only adapts if the\n // spec left it at the light default, so explicit chrome color\n // overrides survive dark-mode adaptation. When a TokenValue pair has\n // an explicit dark value, use it directly.\n eyebrow: pairs?.['chrome.eyebrow.color']\n ? { ...theme.chrome.eyebrow, color: pairs['chrome.eyebrow.color'].dark }\n : theme.chrome.eyebrow,\n title: {\n ...theme.chrome.title,\n color: pairs?.['chrome.title.color']\n ? pairs['chrome.title.color'].dark\n : overridden(theme.chrome.title.color, lightChrome.title.color, darkText),\n },\n subtitle: {\n ...theme.chrome.subtitle,\n color: pairs?.['chrome.subtitle.color']\n ? pairs['chrome.subtitle.color'].dark\n : overridden(theme.chrome.subtitle.color, lightChrome.subtitle.color, darkMuted),\n },\n source: {\n ...theme.chrome.source,\n color: pairs?.['chrome.source.color']\n ? pairs['chrome.source.color'].dark\n : overridden(theme.chrome.source.color, lightChrome.source.color, darkMuted),\n },\n byline: {\n ...theme.chrome.byline,\n color: pairs?.['chrome.byline.color']\n ? pairs['chrome.byline.color'].dark\n : overridden(theme.chrome.byline.color, lightChrome.byline.color, darkMuted),\n },\n footer: {\n ...theme.chrome.footer,\n color: pairs?.['chrome.footer.color']\n ? pairs['chrome.footer.color'].dark\n : overridden(theme.chrome.footer.color, lightChrome.footer.color, darkMuted),\n },\n },\n // The editorial rule is a brand device: it keeps its authored color unless\n // the theme gave an explicit dark half of the pair.\n rule:\n theme.rule && pairs?.['rule.color']\n ? { ...theme.rule, color: pairs['rule.color'].dark }\n : theme.rule,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Light mode line stroke darkening\n// ---------------------------------------------------------------------------\n\n/**\n * Returns a darker variant of a color for use as a foreground stroke on\n * light backgrounds, where mid-lightness palette colors (e.g. cyan-500\n * `#06b6d4`) lack contrast against white. Drops HSL lightness by ~12%\n * absolute (clamped to >= 0) while preserving hue and saturation, which\n * reproduces the cyan-500 → cyan-600 step the cyan accent originally\n * needed and works for any other palette accent the user picks.\n *\n * Returns the input unchanged when:\n * - the color is already dark enough (L <= ~0.40) — further darkening\n * just muddies the stroke without adding contrast\n * - the color isn't parseable\n * - the color is achromatic (NaN hue) — HSL lightness on grays drifts\n * toward black instead of staying neutral\n *\n * Exposed via the `--oc-accent-strong` CSS token.\n */\nexport function adaptForLightLineStroke(color: string): string {\n if (rgb(color) == null) return color;\n const c = hsl(color);\n if (c == null) return color;\n // Achromatic check: NaN hue OR low saturation. d3-color reports a valid\n // hue for grays whose RGB channels aren't perfectly equal (e.g. zinc\n // `#a1a1aa` has s≈0.05), so saturation is the more reliable signal.\n // Threshold 0.10 catches near-grays without tripping on real palette\n // colors (palette saturation is ≥ ~0.5).\n if (Number.isNaN(c.h) || c.s < 0.1) return color;\n if (c.l <= 0.4) return color;\n c.l = Math.max(0, c.l - 0.12);\n return c.formatHex();\n}\n","/**\n * Text measurement for environments without a DOM.\n *\n * Uses per-character advance-width tables measured from Inter (the default\n * chart font) for ~2-3% accuracy on Latin text at weights 400/700.\n * Other fonts set via theme.fonts.family still get a better-than-flat-ratio\n * approximation but no accuracy guarantee.\n *\n * Tables generated by scripts/generate-char-widths.mjs — regenerate, don't\n * hand-edit.\n */\n\nimport type { MeasureTextFn } from '../types/layout';\n\n// ---------------------------------------------------------------------------\n// Per-character advance widths for Inter, normalized to fontSize = 1.\n// Generated by scripts/generate-char-widths.mjs from Chromium canvas metrics.\n// ---------------------------------------------------------------------------\n\nconst CHAR_WIDTHS_400: Record<string, number> = {\n ' ': 0.278,\n '!': 0.278,\n '\"': 0.355,\n '#': 0.556,\n $: 0.556,\n '%': 0.889,\n '&': 0.667,\n \"'\": 0.191,\n '(': 0.333,\n ')': 0.333,\n '*': 0.389,\n '+': 0.584,\n ',': 0.278,\n '-': 0.333,\n '.': 0.278,\n '/': 0.278,\n '0': 0.556,\n '1': 0.556,\n '2': 0.556,\n '3': 0.556,\n '4': 0.556,\n '5': 0.556,\n '6': 0.556,\n '7': 0.556,\n '8': 0.556,\n '9': 0.556,\n ':': 0.278,\n ';': 0.278,\n '<': 0.584,\n '=': 0.584,\n '>': 0.584,\n '?': 0.556,\n '@': 1.015,\n A: 0.667,\n B: 0.667,\n C: 0.722,\n D: 0.722,\n E: 0.667,\n F: 0.611,\n G: 0.778,\n H: 0.722,\n I: 0.278,\n J: 0.5,\n K: 0.667,\n L: 0.556,\n M: 0.833,\n N: 0.722,\n O: 0.778,\n P: 0.667,\n Q: 0.778,\n R: 0.722,\n S: 0.667,\n T: 0.611,\n U: 0.722,\n V: 0.667,\n W: 0.944,\n X: 0.667,\n Y: 0.667,\n Z: 0.611,\n '[': 0.278,\n '\\\\': 0.278,\n ']': 0.278,\n '^': 0.469,\n _: 0.556,\n '`': 0.333,\n a: 0.556,\n b: 0.556,\n c: 0.5,\n d: 0.556,\n e: 0.556,\n f: 0.278,\n g: 0.556,\n h: 0.556,\n i: 0.222,\n j: 0.222,\n k: 0.5,\n l: 0.222,\n m: 0.833,\n n: 0.556,\n o: 0.556,\n p: 0.556,\n q: 0.556,\n r: 0.333,\n s: 0.5,\n t: 0.278,\n u: 0.556,\n v: 0.5,\n w: 0.722,\n x: 0.5,\n y: 0.5,\n z: 0.5,\n '{': 0.334,\n '|': 0.26,\n '}': 0.334,\n '~': 0.584,\n '₹': 0.52,\n '₩': 1.022,\n '¥': 0.556,\n '¥': 1,\n '€': 0.744,\n '£': 0.556,\n '¢': 0.556,\n '§': 0.556,\n '©': 0.737,\n '®': 0.737,\n '™': 1,\n '°': 0.4,\n '–': 0.556,\n '—': 1,\n '‘': 0.222,\n '’': 0.222,\n '“': 0.333,\n '”': 0.333,\n '…': 1,\n '±': 0.549,\n '×': 0.584,\n '÷': 0.549,\n};\n\nconst AVG_CHAR_WIDTH_400 = 0.53;\n\nconst CHAR_WIDTHS_700: Record<string, number> = {\n ' ': 0.278,\n '!': 0.333,\n '\"': 0.474,\n '#': 0.556,\n $: 0.556,\n '%': 0.889,\n '&': 0.722,\n \"'\": 0.238,\n '(': 0.333,\n ')': 0.333,\n '*': 0.389,\n '+': 0.584,\n ',': 0.278,\n '-': 0.333,\n '.': 0.278,\n '/': 0.278,\n '0': 0.556,\n '1': 0.556,\n '2': 0.556,\n '3': 0.556,\n '4': 0.556,\n '5': 0.556,\n '6': 0.556,\n '7': 0.556,\n '8': 0.556,\n '9': 0.556,\n ':': 0.333,\n ';': 0.333,\n '<': 0.584,\n '=': 0.584,\n '>': 0.584,\n '?': 0.611,\n '@': 0.975,\n A: 0.722,\n B: 0.722,\n C: 0.722,\n D: 0.722,\n E: 0.667,\n F: 0.611,\n G: 0.778,\n H: 0.722,\n I: 0.278,\n J: 0.556,\n K: 0.722,\n L: 0.611,\n M: 0.833,\n N: 0.722,\n O: 0.778,\n P: 0.667,\n Q: 0.778,\n R: 0.722,\n S: 0.667,\n T: 0.611,\n U: 0.722,\n V: 0.667,\n W: 0.944,\n X: 0.667,\n Y: 0.667,\n Z: 0.611,\n '[': 0.333,\n '\\\\': 0.278,\n ']': 0.333,\n '^': 0.584,\n _: 0.556,\n '`': 0.333,\n a: 0.556,\n b: 0.611,\n c: 0.556,\n d: 0.611,\n e: 0.556,\n f: 0.333,\n g: 0.611,\n h: 0.611,\n i: 0.278,\n j: 0.278,\n k: 0.556,\n l: 0.278,\n m: 0.889,\n n: 0.611,\n o: 0.611,\n p: 0.611,\n q: 0.611,\n r: 0.389,\n s: 0.556,\n t: 0.333,\n u: 0.611,\n v: 0.556,\n w: 0.778,\n x: 0.556,\n y: 0.556,\n z: 0.5,\n '{': 0.389,\n '|': 0.28,\n '}': 0.389,\n '~': 0.584,\n '₹': 0.553,\n '₩': 1.024,\n '¥': 0.556,\n '¥': 1,\n '€': 0.744,\n '£': 0.556,\n '¢': 0.556,\n '§': 0.556,\n '©': 0.737,\n '®': 0.737,\n '™': 1,\n '°': 0.4,\n '–': 0.556,\n '—': 1,\n '‘': 0.278,\n '’': 0.278,\n '“': 0.5,\n '”': 0.5,\n '…': 1,\n '±': 0.549,\n '×': 0.584,\n '÷': 0.549,\n};\n\nconst AVG_CHAR_WIDTH_700 = 0.554;\n\n// Weights < 650 scale the CHAR_WIDTHS_400 table; weights >= 650 scale\n// CHAR_WIDTHS_700. The factor is 1.0 at each table's native weight.\nconst WEIGHT_ADJUSTMENT: Record<number, number> = {\n 100: 0.9,\n 200: 0.92,\n 300: 0.95,\n 400: 1.0,\n 500: 1.02,\n 600: 1.05,\n 700: 1.0, // uses CHAR_WIDTHS_700 directly\n 800: 1.1,\n 900: 1.12,\n};\n\ninterface ResolvedTable {\n table: Record<string, number>;\n avg: number;\n scale: number;\n}\n\n// Precomputed per-weight results so resolveTable never allocates. Covers the\n// explicit WEIGHT_ADJUSTMENT entries plus a default (scale 1.0) fallback for\n// any other weight, bucketed the same way resolveTable's branch always was.\nconst RESOLVED_TABLE_400_DEFAULT: ResolvedTable = {\n table: CHAR_WIDTHS_400,\n avg: AVG_CHAR_WIDTH_400,\n scale: 1.0,\n};\nconst RESOLVED_TABLE_700_DEFAULT: ResolvedTable = {\n table: CHAR_WIDTHS_700,\n avg: AVG_CHAR_WIDTH_700,\n scale: 1.0,\n};\n\nconst RESOLVED_TABLE_CACHE = new Map<number, ResolvedTable>();\nfor (const [weightStr, scale] of Object.entries(WEIGHT_ADJUSTMENT)) {\n const weight = Number(weightStr);\n RESOLVED_TABLE_CACHE.set(\n weight,\n weight >= 650\n ? { table: CHAR_WIDTHS_700, avg: AVG_CHAR_WIDTH_700, scale }\n : { table: CHAR_WIDTHS_400, avg: AVG_CHAR_WIDTH_400, scale },\n );\n}\n\nfunction resolveTable(fontWeight: number): ResolvedTable {\n const cached = RESOLVED_TABLE_CACHE.get(fontWeight);\n if (cached) return cached;\n return fontWeight >= 650 ? RESOLVED_TABLE_700_DEFAULT : RESOLVED_TABLE_400_DEFAULT;\n}\n\nfunction charWidthRatio(ch: string, table: Record<string, number>, avg: number): number {\n const direct = table[ch];\n if (direct !== undefined) return direct;\n const code = ch.codePointAt(0) ?? 0;\n // Combining marks (diacritics) have zero advance width\n if ((code >= 0x0300 && code <= 0x036f) || (code >= 0x1ab0 && code <= 0x1aff)) return 0;\n // CJK ideographs, kana, hangul, and fullwidth forms are full-em\n if (\n (code >= 0x1100 && code <= 0x11ff) ||\n (code >= 0x2e80 && code <= 0x9fff) ||\n (code >= 0xac00 && code <= 0xd7af) ||\n (code >= 0xf900 && code <= 0xfaff) ||\n (code >= 0xff00 && code <= 0xff60)\n )\n return 1.0;\n return avg;\n}\n\n/**\n * Estimate the average character width for a given font size and weight.\n * Used by both text width estimation and word-wrap line counting.\n */\nexport function estimateCharWidth(fontSize: number, fontWeight = 400): number {\n const { avg, scale } = resolveTable(fontWeight);\n return fontSize * avg * scale;\n}\n\nfunction measureLine(\n line: string,\n table: Record<string, number>,\n avg: number,\n scale: number,\n fontSize: number,\n): number {\n let sum = 0;\n for (const ch of line) {\n sum += charWidthRatio(ch, table, avg);\n }\n return sum * fontSize * scale;\n}\n\n/**\n * Estimate the rendered width of a text string.\n *\n * Uses per-character advance widths measured from Inter (the default chart\n * font). Accurate to ~2-3% for Latin text at weights 400/700; other weights\n * are interpolated from the nearest table.\n */\nexport function estimateTextWidth(text: string, fontSize: number, fontWeight = 400): number {\n const { table, avg, scale } = resolveTable(fontWeight);\n\n if (text.includes('\\n')) {\n let maxWidth = 0;\n for (const line of text.split('\\n')) {\n const w = measureLine(line, table, avg, scale, fontSize);\n if (w > maxWidth) maxWidth = w;\n }\n return maxWidth;\n }\n return measureLine(text, table, avg, scale, fontSize);\n}\n\n/**\n * Width reserved for the \"OpenData\" brand watermark in the bottom-right corner.\n */\nexport const BRAND_RESERVE_WIDTH = 130;\n\n/** Font size of the brand watermark (px). Shared between layout and renderer. */\nexport const BRAND_FONT_SIZE = 12;\n\n/** Minimum chart width to render the brand watermark (px). */\nexport const BRAND_MIN_WIDTH = 120;\n\n/**\n * Width threshold below which layout tightens spacing (legend gaps, chrome\n * padding, label gaps) to maximize data area on small screens.\n */\nexport const COMPACT_WIDTH = 420;\n\n/**\n * Estimate the rendered height of a text block.\n */\nexport function estimateTextHeight(fontSize: number, lineCount = 1, lineHeight = 1.3): number {\n return fontSize * lineHeight * lineCount;\n}\n\n/**\n * Distance from the top of a text box to its alphabetic baseline.\n *\n * Layout stores text positions as top-edge coordinates; renderers convert to\n * baseline coordinates with this ratio instead of `dominant-baseline: hanging`.\n * WebKit computes the hanging baseline from different font metrics than Blink\n * and never inherits dominant-baseline into tspans (WebKit bugs 139258 /\n * 297455), which shifted whole titles above the container on iOS Safari.\n * The 0.8 ratio matches Blink's rendered hanging position for typical UI\n * fonts (hanging sits at ~0.8 x ascent, Inter ascent ~= 0.97em) and the\n * Highcharts fontMetrics convention.\n */\nexport function textAscent(fontSize: number): number {\n return fontSize * 0.8;\n}\n\n// ---------------------------------------------------------------------------\n// Measurer resolver — bridges heuristic ↔ injected canvas measurer\n// ---------------------------------------------------------------------------\n\nexport const heuristicMeasure: MeasureTextFn = (text, fontSize, fontWeight) => ({\n width: estimateTextWidth(text, fontSize, fontWeight),\n height: estimateTextHeight(fontSize),\n});\n\nexport function resolveMeasurer(measureText?: MeasureTextFn): MeasureTextFn {\n return measureText ?? heuristicMeasure;\n}\n","/**\n * Responsive breakpoints and layout strategies.\n *\n * Width breakpoints:\n * - compact: < 400px (mobile, small embeds)\n * - medium: 400-700px (tablet, sidebars)\n * - full: > 700px (desktop, full-width)\n *\n * Height classes:\n * - minimal: < 100px (sparklines, chrome-less thumbnails)\n * - cramped: 100-199px (dashboard widgets with a compact title)\n * - short: 200-350px (embedded panels, short containers)\n * - normal: > 350px (standard containers)\n */\n\n// ---------------------------------------------------------------------------\n// Width breakpoint type and detection\n// ---------------------------------------------------------------------------\n\n/** Responsive breakpoint based on container width. */\nexport type Breakpoint = 'compact' | 'medium' | 'full';\n\n/** Breakpoint thresholds in pixels. */\nexport const BREAKPOINT_COMPACT_MAX = 400;\nexport const BREAKPOINT_MEDIUM_MAX = 700;\n\n/**\n * Determine the breakpoint for a given container width.\n */\nexport function getBreakpoint(width: number): Breakpoint {\n if (width < BREAKPOINT_COMPACT_MAX) return 'compact';\n if (width <= BREAKPOINT_MEDIUM_MAX) return 'medium';\n return 'full';\n}\n\n// ---------------------------------------------------------------------------\n// Height class type and detection\n// ---------------------------------------------------------------------------\n\n/** Height classification based on container height. */\nexport type HeightClass = 'minimal' | 'cramped' | 'short' | 'normal';\n\n/** Height class thresholds in pixels. */\nexport const HEIGHT_MINIMAL_MAX = 100;\nexport const HEIGHT_CRAMPED_MAX = 200;\nexport const HEIGHT_SHORT_MAX = 350;\n\n/**\n * Determine the height class for a given container height.\n */\nexport function getHeightClass(height: number): HeightClass {\n if (height < HEIGHT_MINIMAL_MAX) return 'minimal';\n if (height < HEIGHT_CRAMPED_MAX) return 'cramped';\n if (height <= HEIGHT_SHORT_MAX) return 'short';\n return 'normal';\n}\n\n// ---------------------------------------------------------------------------\n// Layout strategy\n// ---------------------------------------------------------------------------\n\n/** Label display mode at a given breakpoint. */\nexport type LabelMode = 'all' | 'important' | 'none';\n\n/**\n * Legend position at a given breakpoint. `'top-left'` is a map-only overlay\n * position: the legend floats inside the map area's top-left corner (with its\n * own backdrop) instead of reserving a block of figure height. Charts treat\n * it as the default position.\n */\nexport type LegendPosition = 'top' | 'top-left' | 'right' | 'bottom' | 'bottom-right' | 'inline';\n\n/** Annotation position strategy. */\nexport type AnnotationPosition = 'inline' | 'tooltip-only';\n\n/** Axis label density (controls tick count reduction). */\nexport type AxisLabelDensity = 'full' | 'reduced' | 'minimal';\n\n/** Chrome display mode based on available height. */\nexport type ChromeMode = 'full' | 'compact' | 'hidden';\n\n/**\n * Layout strategy defining how the visualization adapts to available space.\n * Returned by getLayoutStrategy() based on width breakpoint and height class.\n */\nexport interface LayoutStrategy {\n /** How data labels are displayed. */\n labelMode: LabelMode;\n /** Where the legend is positioned. */\n legendPosition: LegendPosition;\n /** How annotations are displayed. */\n annotationPosition: AnnotationPosition;\n /** Axis tick density. */\n axisLabelDensity: AxisLabelDensity;\n /** Chrome display mode: full, compact (title only), or hidden. */\n chromeMode: ChromeMode;\n /** Max fraction of container height for legend (0-1). -1 means unlimited. */\n legendMaxHeight: number;\n}\n\n/**\n * Get the base layout strategy for a width breakpoint.\n */\nfunction getWidthStrategy(breakpoint: Breakpoint): LayoutStrategy {\n switch (breakpoint) {\n case 'compact':\n return {\n labelMode: 'none',\n legendPosition: 'top',\n annotationPosition: 'tooltip-only',\n axisLabelDensity: 'minimal',\n chromeMode: 'full',\n legendMaxHeight: -1,\n };\n case 'medium':\n return {\n labelMode: 'important',\n legendPosition: 'top',\n annotationPosition: 'inline',\n axisLabelDensity: 'reduced',\n chromeMode: 'full',\n legendMaxHeight: -1,\n };\n case 'full':\n return {\n labelMode: 'all',\n legendPosition: 'top',\n annotationPosition: 'inline',\n axisLabelDensity: 'full',\n chromeMode: 'full',\n legendMaxHeight: -1,\n };\n }\n}\n\n/**\n * Apply height constraints to a width-based strategy.\n * Short containers compress chrome and cap legend height.\n * Minimal containers hide chrome and labels entirely. Cramped containers\n * (100-199px) keep the same reduced layout but show a compact title: a\n * titled dashboard tile in that range is unusable without its label.\n */\nfunction applyHeightConstraints(\n strategy: LayoutStrategy,\n heightClass: HeightClass,\n): LayoutStrategy {\n if (heightClass === 'normal') return strategy;\n\n if (heightClass === 'minimal' || heightClass === 'cramped') {\n return {\n ...strategy,\n chromeMode: heightClass === 'minimal' ? 'hidden' : 'compact',\n legendMaxHeight: 0,\n labelMode: 'none',\n annotationPosition: 'tooltip-only',\n };\n }\n\n // short\n return {\n ...strategy,\n chromeMode: 'compact',\n legendMaxHeight: 0.15,\n };\n}\n\n/**\n * Get the layout strategy for a given breakpoint and height class.\n *\n * Compact: minimal chrome, no inline labels, legend on top, reduced axes.\n * Medium: moderate labels, legend on top, reduced axes.\n * Full: all labels, legend on top, full axes.\n *\n * Legend position is 'top' at every breakpoint: a top legend reads as a key to\n * the chart before the eye reaches the plot, and it keeps the plotting area\n * full-width instead of surrendering a right-hand column. `legend.position`\n * still overrides it per spec.\n *\n * Height constraints further reduce chrome and legend when container is short.\n */\nexport function getLayoutStrategy(\n breakpoint: Breakpoint,\n heightClass: HeightClass = 'normal',\n): LayoutStrategy {\n const base = getWidthStrategy(breakpoint);\n return applyHeightConstraints(base, heightClass);\n}\n\n// ---------------------------------------------------------------------------\n// Chrome economy\n// ---------------------------------------------------------------------------\n\n/**\n * Height below which gridlines are dropped. A 140px dashboard tile has room\n * for the shape of the series and nothing else; five hairlines behind it are\n * decoration, not structure.\n */\nexport const HEIGHT_NO_GRID_MAX = 150;\n\n/** Tick-label budget for the x-axis at the compact breakpoint. */\nexport const COMPACT_MAX_X_TICKS = 3;\n\n/**\n * How much chart chrome a container can afford.\n *\n * Small embeds get the same treatment an editorial designer would give them:\n * drop the grid first, then the axes, and keep only the tick labels that\n * anchor the reader (first, last, and one in between). Every drop is a\n * default — an explicit `axis.grid` / `encoding.*.axis` on the spec wins.\n */\nexport interface ChromeEconomy {\n /** Draw gridlines by default? False under `HEIGHT_NO_GRID_MAX`. */\n gridlines: boolean;\n /** Draw axes by default? False under `HEIGHT_CRAMPED_MAX`. */\n axes: boolean;\n /** Cap on drawn x-axis tick labels, or undefined for no cap. */\n maxXTicks?: number;\n}\n\n/**\n * Resolve the chrome economy for a container size.\n *\n * @param width - Container width in pixels.\n * @param height - Container height in pixels.\n */\nexport function resolveChromeEconomy(width: number, height: number): ChromeEconomy {\n const economy: ChromeEconomy = {\n gridlines: height >= HEIGHT_NO_GRID_MAX,\n axes: height >= HEIGHT_CRAMPED_MAX,\n };\n if (getBreakpoint(width) === 'compact') economy.maxXTicks = COMPACT_MAX_X_TICKS;\n return economy;\n}\n","/**\n * Pixel-level responsive metrics for chart layout.\n *\n * These are the numeric tuning values that correspond to the semantic decisions\n * in LayoutStrategy. Centralizing them here ensures the engine and renderer\n * always agree on the same thresholds and offsets without duplicating raw numbers.\n *\n * LayoutStrategy is semantic (what to show), this module is metric (how much space).\n */\n\nimport { estimateTextWidth } from '../layout/text-measure';\nimport { BREAKPOINT_COMPACT_MAX } from './breakpoints';\n\n// ---------------------------------------------------------------------------\n// Y-axis title offsets\n// ---------------------------------------------------------------------------\n\n/** Distance from the chart edge to the rotated y-axis title center (compact viewports). */\nexport const AXIS_TITLE_OFFSET_COMPACT = 35;\n\n/** Distance from the chart edge to the rotated y-axis title center (standard viewports). */\nexport const AXIS_TITLE_OFFSET_DEFAULT = 45;\n\n/** Returns the y-axis title offset appropriate for the given container width. */\nexport function getAxisTitleOffset(width: number): number {\n return width < BREAKPOINT_COMPACT_MAX ? AXIS_TITLE_OFFSET_COMPACT : AXIS_TITLE_OFFSET_DEFAULT;\n}\n\n// ---------------------------------------------------------------------------\n// Horizontal padding\n// ---------------------------------------------------------------------------\n\n/**\n * On compact viewports axis titles and tick labels tolerate closer container edges\n * than chrome text (title, subtitle). Reduce horizontal padding to reclaim space.\n */\nexport const HPAD_COMPACT_FRACTION = 0.5;\n\n/** Minimum horizontal padding regardless of scaling. */\nexport const HPAD_COMPACT_MIN = 4;\n\n// ---------------------------------------------------------------------------\n// Tick label offset\n// ---------------------------------------------------------------------------\n\n/**\n * Horizontal gap between the chart edge and the near edge of y-axis tick labels.\n * The engine uses this to reserve right-axis margin; the renderer uses it to position labels.\n * Both must agree on this value — do not change one without the other.\n */\nexport const TICK_LABEL_OFFSET = 6;\n\n// ---------------------------------------------------------------------------\n// Axis title trailing padding\n// ---------------------------------------------------------------------------\n\n/**\n * Extra padding beyond half-glyph height added to the rotated axis title margin on\n * standard (non-compact) viewports. Omitted on compact viewports to save space.\n */\nexport const AXIS_TITLE_TRAILING_PAD = 4;\n\n// ---------------------------------------------------------------------------\n// Axis title gap\n// ---------------------------------------------------------------------------\n\n/**\n * Visible breathing room between the widest tick label's far edge and the\n * rotated y-axis title's near edge.\n *\n * The title is rotated -90deg and anchored at its center, so its glyph box\n * extends half the title font size toward the tick labels. To keep the gap\n * constant regardless of title font size, callers add that half-glyph on top\n * of this value (see axisTitleOffset() below). At the old fixed gap of 14 the\n * clearance silently shrank as the font grew (14 - 11 = 3px at body size 21),\n * which let large-font titles collide with the tick labels.\n *\n * Used in both the engine (margin reservation) and the renderer\n * (axes.ts title placement) via axisTitleOffset(). Both must agree on this value.\n */\nexport const AXIS_TITLE_GAP = 3;\n\n/**\n * Computes the distance from the chart edge to the rotated y-axis title's center.\n *\n * The title center must sit far enough left that, after accounting for the\n * widest tick label and the title's own half-glyph height, AXIS_TITLE_GAP of\n * visible clearance remains. Falls back to the viewport-minimum offset when the\n * dynamic value would be smaller (short tick labels on wide containers).\n *\n * When `inline` is true the y-tick labels render inside the chart area (above\n * their gridlines) rather than in a left gutter, so the title only needs to\n * clear the chart edge — no tick-label width and no viewport-minimum floor,\n * both of which would otherwise leave a dead gap between the title and the plot.\n *\n * Shared by the engine (margin reservation) and the renderer (title placement)\n * so the reserved space always matches where the title is drawn.\n */\nexport function axisTitleOffset(\n maxTickLabelWidth: number,\n titleFontSize: number,\n width: number,\n inline = false,\n): number {\n const halfTitleGlyph = Math.ceil(titleFontSize / 2);\n if (inline) {\n // No gutter tick labels to clear: title edge + gap + half-glyph only.\n return TICK_LABEL_OFFSET + AXIS_TITLE_GAP + halfTitleGlyph;\n }\n const dynamic = TICK_LABEL_OFFSET + maxTickLabelWidth + AXIS_TITLE_GAP + halfTitleGlyph;\n return Math.max(dynamic, getAxisTitleOffset(width));\n}\n\n// ---------------------------------------------------------------------------\n// Narrow viewport threshold (between compact and medium)\n// ---------------------------------------------------------------------------\n\n/**\n * Width below which \"narrow\" adjustments apply: extra iOS Safari top padding and\n * tighter category label gaps. Sits between compact (< 400) and medium (400–700).\n * Not a semantic breakpoint — a layout heuristic for narrow-but-not-compact containers.\n */\nexport const NARROW_VIEWPORT_MAX = 500;\n\n// ---------------------------------------------------------------------------\n// Top padding (iOS Safari address bar / notch clearance)\n// ---------------------------------------------------------------------------\n\n/** Extra top padding added below NARROW_VIEWPORT_MAX to clear iOS Safari browser chrome. */\nexport const TOP_PAD_EXTRA_NARROW = 10;\n\n/** @deprecated Use NARROW_VIEWPORT_MAX instead. */\nexport const TOP_PAD_NARROW_MAX = NARROW_VIEWPORT_MAX;\n\n// ---------------------------------------------------------------------------\n// Category label gaps (left-axis bar/dot charts)\n// ---------------------------------------------------------------------------\n\n/** Gap between category label text and chart edge on narrow viewports (< NARROW_VIEWPORT_MAX). */\nexport const LABEL_GAP_COMPACT = 8;\n\n/** Gap between category label text and chart edge on standard viewports. */\nexport const LABEL_GAP_DEFAULT = 12;\n\n/** @deprecated Use NARROW_VIEWPORT_MAX instead. */\nexport const LABEL_GAP_NARROW_MAX = NARROW_VIEWPORT_MAX;\n\n// ---------------------------------------------------------------------------\n// Max left-axis label fraction\n// ---------------------------------------------------------------------------\n\n/**\n * Width threshold for the medium left-label fraction cap.\n * Sits between medium (400-700) and full (> 700) to prevent wide category labels\n * from consuming too much horizontal space at mid-range widths.\n */\nexport const MAX_LEFT_LABEL_FRACTION_MEDIUM_MAX = 600;\n\n/** Max fraction of container width reservable for left category labels (compact). */\nexport const MAX_LEFT_LABEL_FRACTION_COMPACT = 0.45;\n\n/** Max fraction of container width reservable for left category labels (mid-range). */\nexport const MAX_LEFT_LABEL_FRACTION_MEDIUM = 0.55;\n\n/** Max fraction of container width reservable for left category labels (standard). */\nexport const MAX_LEFT_LABEL_FRACTION_DEFAULT = 1;\n\n// ---------------------------------------------------------------------------\n// X-axis extent (height below chart area)\n// ---------------------------------------------------------------------------\n\nexport const X_AXIS_BAND_HEIGHT = 26;\nexport const X_AXIS_TITLE_BAND = 22;\nexport const X_AXIS_TITLE_BAND_ROTATED = 20;\n\n/**\n * Ceiling on the vertical extent reserved for rotated x tick labels. The angle\n * ladder (engine layout/axes/rotation) avoids -90° for labels longer than this.\n *\n * The cap bounds the RESERVATION, so a label whose rotated projection exceeds it\n * must be truncated to fit — see `truncateRotatedLabel`. Reserving a capped band\n * while drawing the full string is what let long ticks spill past the axis and\n * collide with the source line.\n */\nexport const X_AXIS_ROTATED_EXTENT_CAP = 120;\n\n/**\n * Line-height multiplier for a tick label's text ribbon. Shared by the\n * rotated-extent reservation below and the engine's rotation collision test\n * (layout/axes/rotation) so the reserved space and the fit threshold can't\n * drift apart.\n */\nexport const TICK_LINE_HEIGHT_FACTOR = 1.2;\n\n/**\n * Gap between the axis line and the top of the rotated label band. Mirrors the\n * tick-mark-to-label offset used for flat labels; keeps the reservation matched\n * to the drawn footprint across Blink and WebKit, whose rotated-glyph metrics\n * differ by a couple of pixels. Shared by the extent reservation and the\n * truncation budget so they stay in lockstep.\n */\nconst AXIS_TO_LABEL_GAP = 3;\n\nexport interface XAxisExtentInput {\n labels: string[];\n tickAngle?: number;\n hasTitle: boolean;\n tickFontSize: number;\n tickFontWeight: number;\n xAxisHeight?: number;\n measure?: (text: string, fontSize: number, fontWeight: number) => number;\n}\n\nexport function computeXAxisExtentFromLabels(input: XAxisExtentInput): number {\n const measure = input.measure ?? estimateTextWidth;\n const baseHeight = input.xAxisHeight ?? X_AXIS_BAND_HEIGHT;\n\n if (input.tickAngle && Math.abs(input.tickAngle) > 10) {\n const angleRad = Math.abs(input.tickAngle) * (Math.PI / 180);\n let maxLabelWidth = 40;\n for (const label of input.labels) {\n const w = measure(label, input.tickFontSize, input.tickFontWeight);\n if (w > maxLabelWidth) maxLabelWidth = w;\n }\n // Vertical extent of a rotated label = the projection of its bounding box\n // onto the vertical axis: textWidth*|sin θ| (the tilted length) plus\n // lineHeight*|cos θ| (the label's own height, which still contributes\n // unless the text is fully vertical). Measuring only the sin term\n // under-reserved space, letting rotated ticks spill into the source line.\n const lineHeight = input.tickFontSize * TICK_LINE_HEIGHT_FACTOR;\n const rotatedHeight = Math.min(\n maxLabelWidth * Math.sin(angleRad) + lineHeight * Math.cos(angleRad) + AXIS_TO_LABEL_GAP,\n X_AXIS_ROTATED_EXTENT_CAP,\n );\n return input.hasTitle ? rotatedHeight + X_AXIS_TITLE_BAND_ROTATED : rotatedHeight;\n }\n\n const tickExtent = 4 + input.tickFontSize + Math.max(11, Math.ceil(input.tickFontSize * 0.7));\n const flatHeight = Math.max(baseHeight, tickExtent);\n return input.hasTitle ? flatHeight + X_AXIS_TITLE_BAND : flatHeight;\n}\n\n/**\n * The widest a rotated tick label may be before its vertical projection exceeds\n * `X_AXIS_ROTATED_EXTENT_CAP`.\n *\n * Inverts the rotated-extent formula in `computeXAxisExtentFromLabels`:\n *\n * extent = width*sin(θ) + lineHeight*cos(θ) + gap\n * width = (cap - lineHeight*cos(θ) - gap) / sin(θ)\n *\n * Deriving it from the same terms (rather than hardcoding a second number) keeps\n * the truncation budget and the space reservation locked together — if one\n * changes, the other follows.\n */\nexport function maxRotatedLabelWidth(tickAngle: number, tickFontSize: number): number {\n const angleRad = Math.abs(tickAngle) * (Math.PI / 180);\n const sin = Math.sin(angleRad);\n // A flat (or near-flat) label has no vertical projection to bound.\n if (sin <= 0.001) return Number.POSITIVE_INFINITY;\n const lineHeight = tickFontSize * TICK_LINE_HEIGHT_FACTOR;\n const usable = X_AXIS_ROTATED_EXTENT_CAP - lineHeight * Math.cos(angleRad) - AXIS_TO_LABEL_GAP;\n return Math.max(0, usable / sin);\n}\n\n/**\n * Truncate a label with an ellipsis so it fits inside an explicit pixel budget.\n * Returns the label unchanged when it already fits.\n *\n * Binary-search on character count against the real measure function, so it\n * respects proportional fonts rather than assuming a fixed character width.\n */\nexport function truncateToWidth(\n label: string,\n budget: number,\n fontSize: number,\n fontWeight: number,\n measure: (text: string, fontSize: number, fontWeight?: number) => number = estimateTextWidth,\n): string {\n if (!Number.isFinite(budget)) return label;\n if (measure(label, fontSize, fontWeight) <= budget) return label;\n\n const ELLIPSIS = '…';\n // Nothing sensible fits: keep a single character + ellipsis rather than\n // returning an empty label.\n let lo = 0;\n let hi = label.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n const candidate = label.slice(0, mid) + ELLIPSIS;\n if (measure(candidate, fontSize, fontWeight) <= budget) {\n lo = mid;\n } else {\n hi = mid - 1;\n }\n }\n return lo > 0 ? label.slice(0, lo).trimEnd() + ELLIPSIS : ELLIPSIS;\n}\n\n/**\n * Truncate a rotated tick label with an ellipsis so it fits inside the capped\n * reservation. Returns the label unchanged when it already fits.\n *\n * Delegates to `truncateToWidth` with the budget derived from the rotation\n * geometry, so the binary-search stays in one place.\n */\nexport function truncateRotatedLabel(\n label: string,\n tickAngle: number,\n tickFontSize: number,\n tickFontWeight: number,\n measure: (text: string, fontSize: number, fontWeight?: number) => number = estimateTextWidth,\n): string {\n const budget = maxRotatedLabelWidth(tickAngle, tickFontSize);\n return truncateToWidth(label, budget, tickFontSize, tickFontWeight, measure);\n}\n","/**\n * Default theme definition.\n *\n * Editorial design system with a cyan-led six-hue categorical palette, Inter\n * Variable typography (weights 400/500/600/700), and zinc-based achromatic\n * surfaces. Light is the default surface; dark adaptation lives in\n * dark-mode.ts.\n *\n * resolveTheme() deep-merges user overrides onto this base.\n */\n\nimport {\n CATEGORICAL_FILL_PALETTE,\n CATEGORICAL_PALETTE,\n DIVERGING_PALETTES,\n SEQUENTIAL_PALETTES,\n} from '../colors/palettes';\nimport { X_AXIS_BAND_HEIGHT } from '../responsive/metrics';\nimport type { Theme } from '../types/theme';\n\n/**\n * The default theme. All fields are required and fully specified.\n */\nexport const DEFAULT_THEME: Theme = {\n colors: {\n categorical: [...CATEGORICAL_PALETTE],\n categoricalFill: [...CATEGORICAL_FILL_PALETTE],\n sequential: SEQUENTIAL_PALETTES,\n diverging: DIVERGING_PALETTES,\n background: 'transparent',\n text: '#09090b',\n gridline: 'rgba(0,0,0,0.08)',\n // Structural hairline: axis lines, separators. Deliberately heavier than\n // the gridline and lighter than any ink, so structure reads as structure.\n hairline: 'rgba(0,0,0,0.14)',\n // Used for axis lines/ticks AND axis tick label fill. Must clear WCAG AA\n // contrast (4.5:1) on white because tick labels are rendered with this\n // color. Zinc-500 hits ~5.7:1.\n axis: '#71717a',\n annotationFill: 'rgba(0,0,0,0.04)',\n annotationText: '#71717a',\n positive: '#15803d',\n negative: '#dc2626',\n },\n fonts: {\n family:\n '\"Inter Variable\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif',\n mono: '\"JetBrains Mono\", \"Fira Code\", \"Cascadia Code\", monospace',\n sizes: {\n title: 26,\n subtitle: 14,\n body: 13,\n small: 11,\n axisTick: 11,\n metricLabel: 11,\n metricValue: 22,\n },\n // One ladder everywhere: 400 regular / 500 medium (eyebrow, labels, UI,\n // values) / 600 semibold (title, series names, annotation lede) / 700 bold.\n weights: {\n normal: 400,\n medium: 500,\n semibold: 600,\n bold: 700,\n },\n },\n spacing: {\n padding: 20,\n chromeGap: 6,\n chromeToChart: 8,\n chartToFooter: 8,\n axisMargin: 6,\n xAxisHeight: X_AXIS_BAND_HEIGHT,\n // Gap (px) between the x-axis line and the TOP of the tick labels. The\n // renderer treats this as a literal top gap and then shifts the label down\n // by textAscent() to land it on the alphabetic baseline (rather than\n // dominant-baseline:hanging, which WebKit positions from different metrics),\n // so the gap holds regardless of font size.\n xAxisLabelPadding: 4,\n },\n /** Containers and tooltips. Marks carry their own 2px radius constant. */\n borderRadius: 8,\n chrome: {\n eyebrow: {\n fontSize: 11,\n fontWeight: 500,\n color: '#06b6d4',\n lineHeight: 1.4,\n },\n title: {\n fontSize: 26,\n fontWeight: 600,\n color: '#09090b',\n lineHeight: 1.15,\n },\n subtitle: {\n fontSize: 14,\n fontWeight: 400,\n color: '#71717a',\n lineHeight: 1.45,\n },\n source: {\n fontSize: 11,\n fontWeight: 400,\n color: '#71717a',\n lineHeight: 1.4,\n },\n byline: {\n fontSize: 11,\n fontWeight: 400,\n color: '#71717a',\n lineHeight: 1.4,\n },\n footer: {\n fontSize: 11,\n fontWeight: 400,\n color: '#71717a',\n lineHeight: 1.4,\n },\n },\n /** No editorial rule by default; the `broadsheet` preset sets one. */\n rule: null,\n seriesStrategy: 'palette',\n};\n","/**\n * Named theme presets built on the design token system.\n *\n * Each preset is a ThemeConfig object that can be passed to createChart()\n * or the React/Svelte/Vue Chart component. The engine deep-merges them\n * onto DEFAULT_THEME, so only the deltas are specified here.\n */\n\nimport { CATEGORICAL_FILL_PALETTE_DARK, CATEGORICAL_PALETTE_DARK } from '../colors/palettes';\nimport type { ThemeConfig } from '../types/spec';\n\n/** Editorial preset: the current default look. Exported for explicit selection. */\nexport const editorial: ThemeConfig = {};\n\n/** Essay preset: serif display titles, warmer background, generous spacing. */\nexport const essay: ThemeConfig = {\n colors: {\n background: { light: '#faf8f5', dark: '#1a1816' },\n text: { light: '#1c1917', dark: '#e7e5e4' },\n gridline: { light: 'rgba(0,0,0,0.08)', dark: 'rgba(255,255,255,0.06)' },\n axis: { light: '#78716c', dark: '#a8a29e' },\n },\n fonts: {\n family: '\"Source Serif 4\", \"Source Serif Pro\", Georgia, \"Times New Roman\", serif',\n sizes: { title: 28, subtitle: 15, body: 14, small: 12 },\n weights: { normal: 400, medium: 500, semibold: 600, bold: 700 },\n },\n spacing: {\n padding: 24,\n chromeGap: 6,\n chromeToChart: 12,\n chartToFooter: 12,\n },\n chrome: {\n title: { fontWeight: 600, lineHeight: 1.2 },\n subtitle: { fontWeight: 400, lineHeight: 1.5 },\n },\n borderRadius: 0,\n};\n\n/** Wire preset: dense, mono numerals, tighter chrome. Dashboard/agency feel. */\nexport const wire: ThemeConfig = {\n colors: {\n background: { light: '#ffffff', dark: '#0a0a0a' },\n text: { light: '#171717', dark: '#ededed' },\n gridline: { light: 'rgba(0,0,0,0.06)', dark: 'rgba(255,255,255,0.06)' },\n axis: { light: '#737373', dark: '#a3a3a3' },\n },\n fonts: {\n family: '\"JetBrains Mono\", \"Fira Code\", \"Cascadia Code\", monospace',\n sizes: { title: 16, subtitle: 12, body: 11, small: 10, axisTick: 10 },\n weights: { normal: 400, medium: 500, semibold: 600, bold: 700 },\n },\n spacing: {\n padding: 12,\n chromeGap: 2,\n chromeToChart: 4,\n chartToFooter: 4,\n },\n chrome: {\n title: { fontWeight: 600, lineHeight: 1.25 },\n subtitle: { fontWeight: 400, lineHeight: 1.3 },\n },\n borderRadius: 2,\n};\n\n/**\n * Broadsheet preset: newspaper editorial. Warm paper, a red masthead rule and\n * eyebrow, a bold 24px title, and a quiet palette built around one loud hue.\n *\n * Palette (measured OKLCH; checked with the repo's Brettel CVD simulator —\n * deuteranopia and protanopia both pass, nearest simulated pair 48.7 against\n * a threshold of 30):\n * 1 #1d5f8a oklch(0.466 0.095 242) ink blue\n * 2 #e3120b oklch(0.580 0.232 29) house red — the one loud hue\n * 3 #7fb0d3 oklch(0.735 0.073 240) pale blue\n * 4 #e0a63b oklch(0.762 0.137 79) ochre\n * 5 #3a8a6e oklch(0.576 0.090 167) green\n * 6 #8c8680 oklch(0.623 0.011 68) warm gray\n * Fills are the same hues one step lighter (L +0.06, chroma -12%).\n */\nexport const broadsheet: ThemeConfig = {\n colors: {\n categorical: ['#1d5f8a', '#e3120b', '#7fb0d3', '#e0a63b', '#3a8a6e', '#8c8680'],\n categoricalFill: ['#3b7097', '#ee493a', '#97c2e2', '#efbb64', '#579a81', '#9e9893'],\n background: { light: '#fffdf9', dark: '#171513' },\n text: { light: '#1a1714', dark: '#f4f1ea' },\n gridline: { light: 'rgba(26,23,20,0.08)', dark: 'rgba(244,241,234,0.07)' },\n // Heavier than the default 0.14: newsprint rules are drawn to be seen.\n hairline: { light: 'rgba(26,23,20,0.28)', dark: 'rgba(244,241,234,0.28)' },\n axis: { light: '#6b645c', dark: '#a8a099' },\n annotationFill: { light: 'rgba(26,23,20,0.05)', dark: 'rgba(244,241,234,0.07)' },\n annotationText: { light: '#6b645c', dark: '#a8a099' },\n },\n fonts: {\n sizes: { title: 24, subtitle: 14, body: 13, small: 11 },\n },\n spacing: {\n padding: 20,\n chromeGap: 8,\n chromeToChart: 16,\n chartToFooter: 12,\n },\n chrome: {\n eyebrow: { color: { light: '#e3120b', dark: '#f4463f' }, fontSize: 11, fontWeight: 700 },\n title: { fontSize: 24, fontWeight: 700, lineHeight: 1.18 },\n subtitle: { fontWeight: 400, lineHeight: 1.45 },\n },\n /** The masthead device: a short red bar above the eyebrow. */\n rule: { color: { light: '#e3120b', dark: '#f4463f' }, width: 40, thickness: 3 },\n borderRadius: 0,\n};\n\n/**\n * Terminal preset: a dense dark product surface. Dark in BOTH modes — the\n * resolver detects the dark background and adapts chrome text without\n * `darkMode` being set — so it carries the dark palette variants explicitly\n * rather than waiting for adaptTheme().\n *\n * `seriesStrategy: 'accent-neutral'` keeps one cyan accent against neutral\n * siblings, the register a monitoring UI wants.\n */\nexport const terminal: ThemeConfig = {\n colors: {\n categorical: [...CATEGORICAL_PALETTE_DARK],\n categoricalFill: [...CATEGORICAL_FILL_PALETTE_DARK],\n background: { light: '#0b0f14', dark: '#0b0f14' },\n text: { light: '#e6edf3', dark: '#e6edf3' },\n gridline: { light: 'rgba(230,237,243,0.06)', dark: 'rgba(230,237,243,0.06)' },\n hairline: { light: 'rgba(230,237,243,0.14)', dark: 'rgba(230,237,243,0.14)' },\n // 6.25:1 on #0b0f14 — tick labels are drawn in this color too.\n axis: { light: '#8b949e', dark: '#8b949e' },\n annotationFill: { light: 'rgba(230,237,243,0.06)', dark: 'rgba(230,237,243,0.06)' },\n annotationText: { light: '#8b949e', dark: '#8b949e' },\n positive: { light: '#4ade80', dark: '#4ade80' },\n negative: { light: '#f87171', dark: '#f87171' },\n },\n fonts: {\n family:\n '\"Inter Variable\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif',\n mono: '\"JetBrains Mono\", \"Fira Code\", \"Cascadia Code\", monospace',\n sizes: { title: 15, subtitle: 12, body: 12, small: 10, axisTick: 10 },\n },\n spacing: {\n padding: 12,\n chromeGap: 3,\n chromeToChart: 6,\n chartToFooter: 6,\n },\n chrome: {\n eyebrow: { color: { light: '#22d3ee', dark: '#22d3ee' }, fontSize: 10, fontWeight: 500 },\n title: { fontSize: 15, fontWeight: 600, lineHeight: 1.3 },\n subtitle: { fontWeight: 400, lineHeight: 1.4 },\n },\n borderRadius: 4,\n seriesStrategy: 'accent-neutral',\n};\n","/**\n * Theme resolver: deep-merges user overrides onto default theme.\n *\n * Produces a ResolvedTheme where every property is guaranteed to have\n * a value. The engine uses ResolvedTheme internally so it never needs\n * null checks on theme properties.\n *\n * Auto-detects dark backgrounds and adapts chrome text colors so\n * custom themes with dark canvases are readable without explicitly\n * enabling darkMode.\n */\n\nimport { isOpaqueColor } from '../colors/contrast';\nimport { deriveNeutralRamp } from '../colors/neutral';\nimport type { ChromeThemeOverride, ThemeConfig } from '../types/spec';\nimport type { ChromeDefaults, ResolvedTheme, Theme, TokenValue } from '../types/theme';\nimport { DEFAULT_THEME } from './defaults';\n\n/**\n * Deep merge source into target, creating a new object.\n * Only merges plain objects; arrays and primitives replace directly.\n */\n// biome-ignore lint/suspicious/noExplicitAny: recursive object merge requires dynamic typing\nfunction deepMerge(target: any, source: any): any {\n const result = { ...target };\n\n for (const key of Object.keys(source)) {\n const sourceVal = source[key];\n const targetVal = target[key];\n\n if (\n sourceVal !== undefined &&\n sourceVal !== null &&\n typeof sourceVal === 'object' &&\n !Array.isArray(sourceVal) &&\n typeof targetVal === 'object' &&\n targetVal !== null &&\n !Array.isArray(targetVal)\n ) {\n result[key] = deepMerge(targetVal, sourceVal);\n } else if (sourceVal !== undefined) {\n result[key] = sourceVal;\n }\n }\n\n return result;\n}\n\n/** Extract the light-mode string from a TokenValue. */\nfunction resolveTokenLight(v: TokenValue): string {\n return typeof v === 'string' ? v : v.light;\n}\n\n/**\n * Convert a user-facing ThemeConfig (partial) into the full Theme shape\n * that deepMerge can work with.\n */\nfunction themeConfigToPartial(config: ThemeConfig): Partial<Theme> {\n const partial: Partial<Theme> = {};\n\n if (config.colors) {\n const colors: Partial<Theme['colors']> = {};\n // Shorthand: flat string[] is treated as categorical palette\n if (Array.isArray(config.colors)) {\n colors.categorical = config.colors;\n } else {\n if (config.colors.categorical) colors.categorical = config.colors.categorical;\n if (config.colors.categoricalFill) colors.categoricalFill = config.colors.categoricalFill;\n if (config.colors.sequential) colors.sequential = config.colors.sequential;\n if (config.colors.diverging) colors.diverging = config.colors.diverging;\n if (config.colors.background) colors.background = resolveTokenLight(config.colors.background);\n if (config.colors.text) colors.text = resolveTokenLight(config.colors.text);\n if (config.colors.gridline) colors.gridline = resolveTokenLight(config.colors.gridline);\n if (config.colors.hairline) colors.hairline = resolveTokenLight(config.colors.hairline);\n if (config.colors.axis) colors.axis = resolveTokenLight(config.colors.axis);\n if (config.colors.annotationFill)\n colors.annotationFill = resolveTokenLight(config.colors.annotationFill);\n if (config.colors.annotationText)\n colors.annotationText = resolveTokenLight(config.colors.annotationText);\n if (config.colors.positive) colors.positive = resolveTokenLight(config.colors.positive);\n if (config.colors.negative) colors.negative = resolveTokenLight(config.colors.negative);\n }\n // A user who sets only `categorical` gets fills from the same array. The\n // two palettes must never drift apart behind the author's back: a custom\n // six-hue palette shouldn't leave bars on the built-in fills.\n if (colors.categorical && !colors.categoricalFill) {\n colors.categoricalFill = colors.categorical;\n }\n partial.colors = colors as Theme['colors'];\n }\n\n if (config.fonts) {\n const fonts: Partial<Theme['fonts']> = {};\n if (config.fonts.family) fonts.family = config.fonts.family;\n if (config.fonts.mono) fonts.mono = config.fonts.mono;\n if (config.fonts.sizes) fonts.sizes = config.fonts.sizes as Theme['fonts']['sizes'];\n if (config.fonts.weights) fonts.weights = config.fonts.weights as Theme['fonts']['weights'];\n partial.fonts = fonts as Theme['fonts'];\n }\n\n if (config.spacing) {\n const spacing: Partial<Theme['spacing']> = {};\n if (config.spacing.padding !== undefined) spacing.padding = config.spacing.padding;\n if (config.spacing.chromeGap !== undefined) spacing.chromeGap = config.spacing.chromeGap;\n if (config.spacing.chromeToChart !== undefined)\n spacing.chromeToChart = config.spacing.chromeToChart;\n if (config.spacing.chartToFooter !== undefined)\n spacing.chartToFooter = config.spacing.chartToFooter;\n if (config.spacing.axisMargin !== undefined) spacing.axisMargin = config.spacing.axisMargin;\n if (config.spacing.xAxisHeight !== undefined) spacing.xAxisHeight = config.spacing.xAxisHeight;\n if (config.spacing.xAxisLabelPadding !== undefined)\n spacing.xAxisLabelPadding = config.spacing.xAxisLabelPadding;\n partial.spacing = spacing as Theme['spacing'];\n }\n\n if (config.borderRadius !== undefined) {\n partial.borderRadius = config.borderRadius;\n }\n\n if (config.chrome) {\n const chrome: Record<string, Partial<ChromeDefaults>> = {};\n for (const [element, value] of Object.entries(config.chrome)) {\n if (value === undefined) continue;\n if (typeof value === 'string') {\n chrome[element] = { color: value };\n } else {\n const override: Partial<ChromeDefaults> = {};\n const obj = value as ChromeThemeOverride;\n if (obj.color !== undefined) override.color = resolveTokenLight(obj.color);\n if (obj.fontSize !== undefined) override.fontSize = obj.fontSize;\n if (obj.fontWeight !== undefined) override.fontWeight = obj.fontWeight;\n if (obj.lineHeight !== undefined) override.lineHeight = obj.lineHeight;\n chrome[element] = override;\n }\n }\n partial.chrome = chrome as unknown as Theme['chrome'];\n }\n\n if (config.rule) {\n partial.rule = {\n color: resolveTokenLight(config.rule.color),\n width: config.rule.width,\n thickness: config.rule.thickness,\n };\n }\n\n if (config.seriesStrategy) {\n partial.seriesStrategy = config.seriesStrategy;\n }\n\n return partial;\n}\n\n/**\n * Collect TokenValue pairs from the user's ThemeConfig so adaptTheme()\n * can use explicit dark values instead of algorithmic adaptation.\n */\nfunction collectTokenPairs(\n config: ThemeConfig,\n): Record<string, { light: string; dark: string }> | undefined {\n const pairs: Record<string, { light: string; dark: string }> = {};\n if (config.colors && !Array.isArray(config.colors)) {\n const colorFields = [\n 'background',\n 'text',\n 'gridline',\n 'hairline',\n 'axis',\n 'annotationFill',\n 'annotationText',\n 'positive',\n 'negative',\n ] as const;\n for (const field of colorFields) {\n const v = config.colors[field];\n if (v && typeof v === 'object' && 'light' in v) {\n pairs[`colors.${field}`] = v;\n }\n }\n }\n if (config.rule?.color && typeof config.rule.color === 'object' && 'light' in config.rule.color) {\n pairs['rule.color'] = config.rule.color;\n }\n if (config.chrome) {\n for (const [element, value] of Object.entries(config.chrome)) {\n if (\n value &&\n typeof value === 'object' &&\n 'color' in value &&\n value.color &&\n typeof value.color === 'object' &&\n 'light' in value.color\n ) {\n pairs[`chrome.${element}.color`] = value.color;\n }\n }\n }\n return Object.keys(pairs).length > 0 ? pairs : undefined;\n}\n\n/**\n * Parse a hex color to sRGB relative luminance (WCAG 2.1).\n * Returns 0 for invalid/unparseable colors.\n */\nfunction relativeLuminance(hex: string): number {\n const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());\n if (!m) return 0;\n const r = parseInt(m[1].slice(0, 2), 16) / 255;\n const g = parseInt(m[1].slice(2, 4), 16) / 255;\n const b = parseInt(m[1].slice(4, 6), 16) / 255;\n const toLinear = (c: number) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);\n return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);\n}\n\n/** Returns true if the hex color is perceptually dark (luminance < 0.2). */\nfunction isDarkBackground(hex: string): boolean {\n // A non-opaque background ('transparent', 'none', alpha-zero rgba) has no\n // intrinsic luminance — don't treat it as dark so light-mode chrome text\n // defaults don't get inverted.\n if (!isOpaqueColor(hex)) return false;\n return relativeLuminance(hex) < 0.2;\n}\n\n/**\n * Adapt chrome text colors for a dark background.\n * Only overrides values that still match the light-mode defaults,\n * so explicit user overrides are preserved.\n */\nfunction adaptChromeForDarkBg(theme: Theme, textColor: string): Theme {\n const light = DEFAULT_THEME.chrome;\n return {\n ...theme,\n chrome: {\n // Eyebrow keeps its accent tint regardless of surface mode.\n eyebrow: theme.chrome.eyebrow,\n title: {\n ...theme.chrome.title,\n color:\n theme.chrome.title.color === light.title.color ? textColor : theme.chrome.title.color,\n },\n subtitle: {\n ...theme.chrome.subtitle,\n color:\n theme.chrome.subtitle.color === light.subtitle.color\n ? adjustOpacity(textColor, 0.7)\n : theme.chrome.subtitle.color,\n },\n source: {\n ...theme.chrome.source,\n color:\n theme.chrome.source.color === light.source.color\n ? adjustOpacity(textColor, 0.5)\n : theme.chrome.source.color,\n },\n byline: {\n ...theme.chrome.byline,\n color:\n theme.chrome.byline.color === light.byline.color\n ? adjustOpacity(textColor, 0.5)\n : theme.chrome.byline.color,\n },\n footer: {\n ...theme.chrome.footer,\n color:\n theme.chrome.footer.color === light.footer.color\n ? adjustOpacity(textColor, 0.5)\n : theme.chrome.footer.color,\n },\n },\n };\n}\n\n/** Blend a hex color toward white/black by a factor to simulate opacity. */\nfunction adjustOpacity(hex: string, opacity: number): string {\n const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());\n if (!m) return hex;\n const r = parseInt(m[1].slice(0, 2), 16);\n const g = parseInt(m[1].slice(2, 4), 16);\n const b = parseInt(m[1].slice(4, 6), 16);\n // Blend toward mid-gray for muted secondary text\n const mix = (c: number) => Math.round(c * opacity + 128 * (1 - opacity));\n const toHex = (n: number) => n.toString(16).padStart(2, '0');\n return `#${toHex(mix(r))}${toHex(mix(g))}${toHex(mix(b))}`;\n}\n\n/**\n * Resolve a theme by deep-merging user overrides onto a base theme.\n *\n * Auto-detects dark backgrounds: if the resolved background color is\n * perceptually dark and chrome text colors are still the light-mode\n * defaults, they're adapted for readability.\n *\n * @param userTheme - Optional partial theme overrides from the spec.\n * @param base - Base theme to merge onto. Defaults to DEFAULT_THEME.\n * @returns A ResolvedTheme with all properties guaranteed.\n */\nexport function resolveTheme(userTheme?: ThemeConfig, base: Theme = DEFAULT_THEME): ResolvedTheme {\n let merged: Theme = userTheme ? deepMerge(base, themeConfigToPartial(userTheme)) : { ...base };\n\n // Propagate fonts.sizes overrides onto the chrome element fontSizes.\n // The chrome defaults hard-code fontSize independently from fonts.sizes,\n // so we sync them here after merging so a single sizes override applies everywhere.\n if (userTheme?.fonts?.sizes) {\n const s = userTheme.fonts.sizes;\n merged = {\n ...merged,\n chrome: {\n ...merged.chrome,\n ...(s.title !== undefined && {\n title: { ...merged.chrome.title, fontSize: s.title },\n }),\n ...(s.subtitle !== undefined && {\n subtitle: { ...merged.chrome.subtitle, fontSize: s.subtitle },\n }),\n ...(s.small !== undefined && {\n source: { ...merged.chrome.source, fontSize: s.small },\n byline: { ...merged.chrome.byline, fontSize: s.small },\n footer: { ...merged.chrome.footer, fontSize: s.small },\n }),\n },\n };\n }\n\n // Auto-adapt chrome for dark backgrounds\n const dark = isDarkBackground(merged.colors.background);\n if (dark) {\n merged = adaptChromeForDarkBg(merged, merged.colors.text);\n }\n\n return {\n ...merged,\n colors: {\n ...merged.colors,\n neutral: deriveNeutralRamp(merged.colors.text, merged.colors.background, dark),\n },\n isDark: dark,\n _tokenPairs: userTheme ? collectTokenPairs(userTheme) : undefined,\n } as ResolvedTheme;\n}\n","/**\n * Chrome layout computation.\n *\n * Takes a Chrome spec + resolved theme and produces a ResolvedChrome\n * with computed text positions, styles, and total chrome heights.\n *\n * Supports three chrome modes:\n * - full: all chrome elements rendered at normal size\n * - compact: title only, no subtitle/source/byline/footer\n * - hidden: no chrome at all (maximizes chart area)\n *\n * Font sizes scale down continuously at narrow widths to keep\n * chrome proportional to the container.\n */\n\nimport type { ChromeMode } from '../responsive/breakpoints';\nimport type {\n MeasureTextFn,\n ResolvedChrome,\n ResolvedChromeElement,\n ResolvedChromeRule,\n TextStyle,\n} from '../types/layout';\nimport type { Chrome, ChromeText } from '../types/spec';\nimport type { ChromeDefaults, ResolvedTheme } from '../types/theme';\nimport {\n BRAND_FONT_SIZE,\n BRAND_MIN_WIDTH,\n BRAND_RESERVE_WIDTH,\n COMPACT_WIDTH,\n estimateCharWidth,\n estimateTextHeight,\n} from './text-measure';\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Normalize a chrome field to text + optional style and offset overrides. */\nfunction normalizeChromeText(\n value: string | ChromeText | undefined,\n): { text: string; style?: ChromeText['style']; offset?: ChromeText['offset'] } | null {\n if (value === undefined) return null;\n if (typeof value === 'string') return { text: value };\n return { text: value.text, style: value.style, offset: value.offset };\n}\n\n/**\n * Scale a font size based on container width. Only applies to default sizes\n * (not user overrides). Scales from 100% at >= 500px down to 72% at <= 250px.\n */\nfunction scaleFontSize(baseFontSize: number, width: number): number {\n if (width >= 500) return baseFontSize;\n if (width <= 250) return Math.max(Math.round(baseFontSize * 0.72), 10);\n const t = (width - 250) / 250;\n return Math.max(Math.round(baseFontSize * (0.72 + t * 0.28)), 10);\n}\n\n/**\n * Extra scale applied to the default compact title font size in the 100-199px\n * cramped height range (see `computeChrome`'s `crampedTitle` param).\n */\nconst CRAMPED_TITLE_SCALE = 0.65;\n\n/** Build a TextStyle from chrome defaults + optional overrides, with width-based scaling. */\nfunction buildTextStyle(\n defaults: ChromeDefaults,\n fontFamily: string,\n textColor: string,\n width: number,\n overrides?: ChromeText['style'],\n extraScale = 1,\n): TextStyle {\n const hasExplicitSize = overrides?.fontSize !== undefined;\n const baseFontSize = overrides?.fontSize ?? defaults.fontSize;\n const fontSize = hasExplicitSize\n ? baseFontSize\n : Math.max(Math.round(scaleFontSize(baseFontSize, width) * extraScale), 10);\n\n // No dominantBaseline: chrome y positions are top-edge coordinates and\n // renderers convert to the alphabetic baseline via textAscent(). WebKit\n // renders `hanging` from different font metrics than Blink and never\n // inherits it into tspans (bugs 139258/297455), which shifted wrapped\n // titles above the container on iOS Safari.\n return {\n fontFamily: overrides?.fontFamily ?? fontFamily,\n fontSize,\n fontWeight: overrides?.fontWeight ?? defaults.fontWeight,\n fill: overrides?.color ?? textColor ?? defaults.color,\n lineHeight: defaults.lineHeight,\n textAnchor: 'start',\n };\n}\n\n/**\n * Estimate how many lines text will wrap to, given a max width.\n * Uses character-count word-wrapping that matches the SVG renderer's\n * wrapText behavior (word-boundary breaks, same charWidth heuristic).\n * Returns at least 1.\n */\nfunction estimateLineCount(\n text: string,\n style: TextStyle,\n maxWidth: number,\n measureText?: MeasureTextFn,\n maxLines?: number,\n): number {\n if (maxWidth <= 0) return 1;\n\n // Split on explicit newlines first, then estimate wrapping per segment\n const segments = text.split('\\n');\n if (segments.length > 1) {\n const count = segments.reduce((total, segment) => {\n return (\n total +\n (segment.length === 0 ? 1 : estimateLineCount(segment, style, maxWidth, measureText))\n );\n }, 0);\n return maxLines != null ? Math.min(count, maxLines) : count;\n }\n\n // Use real text measurement when available, fall back to heuristic\n if (measureText) {\n const textWidth = measureText(text, style.fontSize, style.fontWeight).width;\n if (textWidth <= maxWidth) return 1;\n\n const words = text.split(' ');\n let lines = 1;\n let current = '';\n\n for (const word of words) {\n const candidate = current ? `${current} ${word}` : word;\n const candidateWidth = measureText(candidate, style.fontSize, style.fontWeight).width;\n if (candidateWidth > maxWidth && current) {\n lines++;\n current = word;\n } else {\n current = candidate;\n }\n }\n\n return maxLines != null ? Math.min(lines, maxLines) : lines;\n }\n\n const charWidth = estimateCharWidth(style.fontSize, style.fontWeight);\n const maxChars = Math.floor(maxWidth / charWidth);\n\n if (text.length <= maxChars) return 1;\n\n const words = text.split(' ');\n let lines = 1;\n let current = '';\n\n for (const word of words) {\n const candidate = current ? `${current} ${word}` : word;\n if (candidate.length > maxChars && current) {\n lines++;\n current = word;\n } else {\n current = candidate;\n }\n }\n\n return maxLines != null ? Math.min(lines, maxLines) : lines;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Line-height multiplier for the auto-thinning footnote list.\n *\n * Exported because the chrome renderer steps each footnote line down by it\n * while the band height below is computed from it -- the two have to agree, or\n * the last line drifts outside the band that was reserved for it.\n */\nexport const FOOTNOTE_LINE_HEIGHT = 1.3;\n/** Gap below the footnote list, separating it from the source row. */\nconst FOOTNOTE_BAND_GAP = 4;\n\n/**\n * Vertical space a footnote list of `count` lines occupies below the plot.\n *\n * Single source of truth, deliberately: the engine reserves this band in the\n * bottom margin, the chrome renderer shifts the source row down by it, and the\n * brand watermark shifts by it too. When those three disagree the footnotes\n * land on top of the watermark, which is exactly the bug this centralizes away.\n */\nexport function footnoteBandHeight(count: number, theme: ResolvedTheme): number {\n if (count <= 0) return 0;\n return count * theme.fonts.sizes.small * FOOTNOTE_LINE_HEIGHT + FOOTNOTE_BAND_GAP;\n}\n\n/**\n * Compute resolved chrome layout from a Chrome spec and resolved theme.\n *\n * Produces positioned text elements and total chrome heights (top and bottom).\n * Top chrome: title, subtitle. Bottom chrome: source, byline, footer.\n *\n * @param chrome - The Chrome spec from the user's VizSpec.\n * @param theme - The fully resolved theme.\n * @param width - Total available width in pixels.\n * @param measureText - Optional real text measurement function from the adapter.\n * @param chromeMode - Chrome display mode: full, compact (title only), or hidden.\n * @param padding - Override padding (for scaled padding from dimensions).\n * @param watermark - Whether the brand watermark renders (affects bottom space).\n * @param bottomLegendHeight - Reserved height for a bottom-positioned legend\n * (legend bounds height + gap). When > 0, source/byline/footer y positions\n * are shifted down by this amount so the chrome stacks BELOW the legend\n * rather than colliding with it. The returned `bottomHeight` includes this\n * reservation, so callers should not double-reserve it in margin math.\n * @param crampedTitle - Shrinks the default (non-explicit) title font size by\n * `CRAMPED_TITLE_SCALE` on top of the width-based scaling. Set by callers for the\n * 100-199px cramped height range: at that height a full-size compact title\n * can push `chrome.topHeight` past the min-chart-height guardrail, which\n * would strip the title back out via the hidden-chrome fallback.\n */\nexport function computeChrome(\n chrome: Chrome | undefined,\n theme: ResolvedTheme,\n width: number,\n measureText?: MeasureTextFn,\n chromeMode: ChromeMode = 'full',\n padding?: number,\n watermark: boolean = true,\n bottomLegendHeight: number = 0,\n crampedTitle: boolean = false,\n): ResolvedChrome {\n if (!chrome || chromeMode === 'hidden') {\n // `watermark` is still true here in two cases: the user explicitly opted\n // in at a cramped height (the compiler suppresses the default there), or\n // the chromeEatsPlot/min-dims fallback hid chrome at a normal height where\n // the default watermark was never suppressed. Reserve the brand band for\n // both so the brand doesn't paint over the plot's bottom edge.\n if (watermark && width >= BRAND_MIN_WIDTH) {\n const hiddenPad = padding ?? theme.spacing.padding;\n const brandHeight = estimateTextHeight(BRAND_FONT_SIZE, 1);\n return {\n topHeight: 0,\n bottomHeight: theme.spacing.chartToFooter + brandHeight + hiddenPad + bottomLegendHeight,\n };\n }\n if (bottomLegendHeight > 0) {\n // No brand band, but a bottom legend was reserved upstream. Surface the\n // reservation through bottomHeight so margin math stays additive, same\n // as the compact branch below.\n return { topHeight: 0, bottomHeight: bottomLegendHeight };\n }\n return { topHeight: 0, bottomHeight: 0 };\n }\n\n const pad = padding ?? theme.spacing.padding;\n const chromeGap = theme.spacing.chromeGap;\n // Subtract a small buffer beyond padding so the heuristic char-width\n // estimate triggers word-wrap before the real font overflows the container.\n const WRAP_SAFETY_BUFFER = 5;\n const maxWidth = width - pad * 2 - WRAP_SAFETY_BUFFER;\n const fontFamily = theme.fonts.family;\n\n // Track vertical cursor for top elements\n let topY = pad;\n const topElements: Partial<Pick<ResolvedChrome, 'eyebrow' | 'title' | 'subtitle'>> = {};\n\n // Eyebrow (hidden in compact mode — same rule as subtitle, keeps the\n // title alone at narrow viewports).\n const eyebrowNorm = chromeMode === 'compact' ? null : normalizeChromeText(chrome.eyebrow);\n const titleNormPeek = normalizeChromeText(chrome.title);\n const subtitleNormPeek = chromeMode === 'compact' ? null : normalizeChromeText(chrome.subtitle);\n\n // Editorial rule (theme.rule): a short colored bar above the block. Only\n // drawn when there is top chrome for it to sit above; it reserves its own\n // thickness plus one chromeGap, exactly like a text element.\n let ruleElement: ResolvedChromeRule | undefined;\n if (theme.rule && (eyebrowNorm || titleNormPeek || subtitleNormPeek)) {\n ruleElement = {\n x: pad,\n y: topY,\n width: theme.rule.width,\n thickness: theme.rule.thickness,\n color: theme.rule.color,\n };\n topY += theme.rule.thickness + chromeGap;\n }\n\n if (eyebrowNorm) {\n const style = buildTextStyle(\n theme.chrome.eyebrow,\n fontFamily,\n theme.chrome.eyebrow.color,\n width,\n eyebrowNorm.style,\n );\n const maxLines = eyebrowNorm.style?.maxLines;\n const lineCount = estimateLineCount(eyebrowNorm.text, style, maxWidth, measureText, maxLines);\n const element: ResolvedChromeElement = {\n text: eyebrowNorm.text,\n x: pad + (eyebrowNorm.offset?.dx ?? 0),\n y: topY + (eyebrowNorm.offset?.dy ?? 0),\n maxWidth,\n ...(maxLines != null ? { maxLines } : {}),\n style,\n };\n topElements.eyebrow = element;\n topY += estimateTextHeight(style.fontSize, lineCount, style.lineHeight) + chromeGap;\n }\n\n // Title\n const titleNorm = titleNormPeek;\n if (titleNorm) {\n const style = buildTextStyle(\n theme.chrome.title,\n fontFamily,\n theme.chrome.title.color,\n width,\n titleNorm.style,\n crampedTitle ? CRAMPED_TITLE_SCALE : 1,\n );\n const maxLines = titleNorm.style?.maxLines;\n const lineCount = estimateLineCount(titleNorm.text, style, maxWidth, measureText, maxLines);\n const element: ResolvedChromeElement = {\n text: titleNorm.text,\n x: pad + (titleNorm.offset?.dx ?? 0),\n y: topY + (titleNorm.offset?.dy ?? 0),\n maxWidth,\n ...(maxLines != null ? { maxLines } : {}),\n style,\n };\n topElements.title = element;\n topY += estimateTextHeight(style.fontSize, lineCount, style.lineHeight) + chromeGap;\n }\n\n // Subtitle (hidden in compact mode)\n const subtitleNorm = subtitleNormPeek;\n if (subtitleNorm) {\n const style = buildTextStyle(\n theme.chrome.subtitle,\n fontFamily,\n theme.chrome.subtitle.color,\n width,\n subtitleNorm.style,\n );\n const maxLines = subtitleNorm.style?.maxLines;\n const lineCount = estimateLineCount(subtitleNorm.text, style, maxWidth, measureText, maxLines);\n const element: ResolvedChromeElement = {\n text: subtitleNorm.text,\n x: pad + (subtitleNorm.offset?.dx ?? 0),\n y: topY + (subtitleNorm.offset?.dy ?? 0),\n maxWidth,\n ...(maxLines != null ? { maxLines } : {}),\n style,\n };\n topElements.subtitle = element;\n topY += estimateTextHeight(style.fontSize, lineCount, style.lineHeight) + chromeGap;\n }\n\n // Add chromeToChart gap if there are any top elements. Tighten on narrow\n // viewports so the subtitle doesn't float far above a legend or chart area.\n const hasTopChrome = eyebrowNorm || titleNorm || subtitleNorm;\n const chromeToChart =\n width < COMPACT_WIDTH ? Math.min(theme.spacing.chromeToChart, 2) : theme.spacing.chromeToChart;\n const topHeight = hasTopChrome ? topY - pad + chromeToChart - chromeGap : 0;\n\n // Bottom chrome text hidden in compact mode, but brand watermark still\n // renders for wide-enough charts. Reserve space so it doesn't overflow.\n if (chromeMode === 'compact') {\n let compactBottom = 0;\n if (watermark && width >= BRAND_MIN_WIDTH) {\n const brandHeight = estimateTextHeight(BRAND_FONT_SIZE, 1);\n compactBottom = theme.spacing.chartToFooter + brandHeight + pad + bottomLegendHeight;\n } else if (bottomLegendHeight > 0) {\n // No bottom chrome content but a bottom legend was reserved upstream.\n // Surface the reservation through bottomHeight so margin math stays\n // additive and consistent with full mode.\n compactBottom = bottomLegendHeight;\n }\n return {\n topHeight,\n bottomHeight: compactBottom,\n ...(ruleElement ? { rule: ruleElement } : {}),\n ...topElements,\n };\n }\n\n // Bottom elements: source, byline, footer, and an optional custom brand\n // (`chrome.brand`). When a custom brand is supplied it suppresses the default\n // OpenData watermark and renders right-anchored on the same baseline.\n const brandNorm = normalizeChromeText(chrome.brand);\n const showWatermark = watermark && !brandNorm;\n // Reserve space on the right for the brand watermark or custom brand so the\n // left-anchored bottom chrome items don't overlap.\n const shouldReserveBrandWidth = (showWatermark || !!brandNorm) && width >= BRAND_MIN_WIDTH;\n const bottomMaxWidth = maxWidth - (shouldReserveBrandWidth ? BRAND_RESERVE_WIDTH : 0);\n const bottomElements: Partial<Pick<ResolvedChrome, 'source' | 'byline' | 'footer'>> = {};\n let bottomHeight = 0;\n\n const bottomItems: Array<{\n key: 'source' | 'byline' | 'footer';\n norm: { text: string; style?: ChromeText['style']; offset?: ChromeText['offset'] };\n defaults: ChromeDefaults;\n }> = [];\n\n const sourceNorm = normalizeChromeText(chrome.source);\n if (sourceNorm) {\n bottomItems.push({\n key: 'source',\n norm: sourceNorm,\n defaults: theme.chrome.source,\n });\n }\n\n const bylineNorm = normalizeChromeText(chrome.byline);\n if (bylineNorm) {\n bottomItems.push({\n key: 'byline',\n norm: bylineNorm,\n defaults: theme.chrome.byline,\n });\n }\n\n const footerNorm = normalizeChromeText(chrome.footer);\n if (footerNorm) {\n bottomItems.push({\n key: 'footer',\n norm: footerNorm,\n defaults: theme.chrome.footer,\n });\n }\n\n if (bottomItems.length > 0) {\n bottomHeight += theme.spacing.chartToFooter;\n // Push bottom chrome below the bottom legend (if reserved). Stored y values\n // include this offset so renderers don't need to know about the legend band.\n bottomHeight += bottomLegendHeight;\n\n for (const item of bottomItems) {\n const style = buildTextStyle(\n item.defaults,\n fontFamily,\n item.defaults.color,\n width,\n item.norm.style,\n );\n const maxLines = item.norm.style?.maxLines;\n const lineCount = estimateLineCount(\n item.norm.text,\n style,\n bottomMaxWidth,\n measureText,\n maxLines,\n );\n const height = estimateTextHeight(style.fontSize, lineCount, style.lineHeight);\n\n // y positions will be computed relative to the bottom of the\n // chart area by the engine. We store offsets from bottom start.\n bottomElements[item.key] = {\n text: item.norm.text,\n x: pad + (item.norm.offset?.dx ?? 0),\n y: bottomHeight + (item.norm.offset?.dy ?? 0), // offset from where bottom chrome starts\n maxWidth: bottomMaxWidth,\n ...(maxLines != null ? { maxLines } : {}),\n style,\n };\n\n bottomHeight += height + chromeGap;\n }\n\n // Remove trailing gap\n bottomHeight -= chromeGap;\n\n // Ensure bottom height accommodates the brand watermark, which renders\n // at the same Y as the first bottom chrome item but is taller (20px font\n // vs 12px source). Without this, the brand overflows the SVG viewBox.\n if (showWatermark && width >= BRAND_MIN_WIDTH) {\n const brandHeight = estimateTextHeight(BRAND_FONT_SIZE, 1);\n // firstItemY is chartToFooter (the Y offset of the first bottom item).\n // The brand needs brandHeight below that point; bottom chrome content\n // needs (bottomHeight - chartToFooter). Take the max.\n const contentBelowFirstItem = bottomHeight - theme.spacing.chartToFooter;\n if (brandHeight > contentBelowFirstItem) {\n bottomHeight += brandHeight - contentBelowFirstItem;\n }\n }\n\n // Add bottom padding\n bottomHeight += pad;\n } else if (showWatermark && width >= BRAND_MIN_WIDTH) {\n // No bottom chrome items, but brand watermark still renders.\n // Reserve space: chartToFooter gap + brand text height + padding,\n // plus the bottom-legend reservation so the watermark sits below it.\n const brandHeight = estimateTextHeight(BRAND_FONT_SIZE, 1);\n bottomHeight = theme.spacing.chartToFooter + brandHeight + pad + bottomLegendHeight;\n } else if (bottomLegendHeight > 0) {\n // No bottom chrome content and no watermark, but a bottom legend was\n // reserved upstream. Surface the reservation so callers don't need to\n // re-add it on top of bottomHeight.\n bottomHeight = bottomLegendHeight;\n }\n\n // Custom brand is right-anchored on the same row as the first bottom chrome\n // item. Compute its style/position once we know whether bottom chrome exists.\n let brandElement: ResolvedChromeElement | undefined;\n if (brandNorm && width >= BRAND_MIN_WIDTH) {\n const brandStyle = buildTextStyle(\n theme.chrome.footer,\n fontFamily,\n theme.chrome.footer.color,\n width,\n brandNorm.style,\n );\n brandStyle.textAnchor = 'end';\n // Y matches the first bottom item (chartToFooter offset). When there are\n // no bottom items, the brand becomes the sole footer element and we need\n // to allocate the same baseline.\n const brandY =\n bottomItems.length > 0 ? theme.spacing.chartToFooter : theme.spacing.chartToFooter;\n brandElement = {\n text: brandNorm.text,\n x: width - pad + (brandNorm.offset?.dx ?? 0),\n y: brandY + (brandNorm.offset?.dy ?? 0),\n maxWidth: BRAND_RESERVE_WIDTH,\n style: brandStyle,\n };\n // Ensure bottomHeight reserves space when brand is the only footer item.\n if (bottomItems.length === 0) {\n const brandHeight = estimateTextHeight(brandStyle.fontSize, 1, brandStyle.lineHeight);\n bottomHeight = theme.spacing.chartToFooter + brandHeight + pad;\n }\n }\n\n return {\n topHeight,\n bottomHeight,\n ...(ruleElement ? { rule: ruleElement } : {}),\n ...topElements,\n ...bottomElements,\n ...(brandElement ? { brand: brandElement } : {}),\n };\n}\n","/**\n * Word-wrap a text string into lines that fit within a max width.\n *\n * Shared by the SVG chart renderer and the sankey renderer. When a\n * `measureText` callback is provided, uses real DOM measurement for\n * accurate wrapping. Otherwise falls back to a character-width\n * heuristic driven by `estimateCharWidth` from text-measure.ts.\n *\n * Non-browser callers (SSR, tests) omit the measureText argument to\n * use the heuristic fallback. Do not change the signature without\n * re-verifying visual baselines for every caller.\n */\n\nimport type { MeasureTextFn } from '../types/layout';\nimport { estimateCharWidth } from './text-measure';\n\n/**\n * Break text into lines that fit within maxWidth using word wrapping.\n *\n * Splits on explicit newlines first, then word-wraps each segment.\n * Preserves empty segments so consecutive newlines produce blank lines.\n *\n * @param text - The text to wrap. May contain `\\n` for forced breaks.\n * @param fontSize - Font size in pixels.\n * @param fontWeight - Font weight (100-900).\n * @param maxWidth - Maximum line width in pixels. Non-positive values return `[text]` unchanged.\n * @param measureText - Optional real text measurer. When omitted, uses a character-width heuristic.\n */\nexport function wrapText(\n text: string,\n fontSize: number,\n fontWeight: number,\n maxWidth: number,\n measureText?: MeasureTextFn,\n): string[] {\n if (maxWidth <= 0) return [text];\n\n // Split on explicit newlines first\n const segments = text.split('\\n');\n if (segments.length > 1) {\n return segments.flatMap((segment) =>\n segment.length === 0 ? [''] : wrapText(segment, fontSize, fontWeight, maxWidth, measureText),\n );\n }\n\n // Use real text measurement when available\n if (measureText) {\n const textWidth = measureText(text, fontSize, fontWeight).width;\n if (textWidth <= maxWidth) return [text];\n\n const words = text.split(' ');\n const lines: string[] = [];\n let current = '';\n\n for (const word of words) {\n const candidate = current ? `${current} ${word}` : word;\n const candidateWidth = measureText(candidate, fontSize, fontWeight).width;\n if (candidateWidth > maxWidth && current) {\n lines.push(current);\n current = word;\n } else {\n current = candidate;\n }\n }\n if (current) lines.push(current);\n\n return lines;\n }\n\n // Heuristic: estimate character width from font size and weight.\n // Reuses the same ratio and weight adjustment constants as text-measure.\n const charWidth = estimateCharWidth(fontSize, fontWeight);\n const maxChars = Math.floor(maxWidth / charWidth);\n\n if (text.length <= maxChars) return [text];\n\n const words = text.split(' ');\n const lines: string[] = [];\n let current = '';\n\n for (const word of words) {\n const candidate = current ? `${current} ${word}` : word;\n if (candidate.length > maxChars && current) {\n lines.push(current);\n current = word;\n } else {\n current = candidate;\n }\n }\n if (current) lines.push(current);\n\n return lines;\n}\n","/**\n * Label collision detection and resolution.\n *\n * Greedy algorithm: sort by priority, place in order, try offset\n * positions for conflicts, demote to tooltip-only if no position works.\n */\n\nimport { estimateTextWidth } from '../layout/text-measure';\nimport type { Rect, ResolvedLabel, TextStyle } from '../types/layout';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Priority levels for label placement. Data labels win over annotations which win over axes. */\nexport type LabelPriority = 'data' | 'annotation' | 'axis';\n\n/** Priority sort order (lower = higher priority). */\nconst PRIORITY_ORDER: Record<LabelPriority, number> = {\n data: 0,\n annotation: 1,\n axis: 2,\n};\n\n/**\n * A label candidate for collision resolution.\n * The collision engine decides its final position and visibility.\n */\nexport interface LabelCandidate {\n /** The label text. */\n text: string;\n /** Preferred anchor position (before collision resolution). */\n anchorX: number;\n /** Preferred anchor position (before collision resolution). */\n anchorY: number;\n /** Estimated width of the label text. */\n width: number;\n /** Estimated height of the label text. */\n height: number;\n /** Label priority for collision resolution. */\n priority: LabelPriority;\n /** Text style to apply. */\n style: TextStyle;\n /**\n * Optional caller-assigned source index. resolveCollisions may reorder its\n * output (it sorts by priority), so callers that zip the result back against\n * a parallel array should carry an index here and read it off ResolvedLabel\n * rather than relying on positional order.\n */\n index?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Collision detection\n// ---------------------------------------------------------------------------\n\n/**\n * Detect AABB (axis-aligned bounding box) overlap between two rectangles.\n */\nexport function detectCollision(a: Rect, b: Rect): boolean {\n return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;\n}\n\n/** Compute the area of intersection between two axis-aligned rectangles. Returns 0 if disjoint. */\nexport function overlapArea(a: Rect, b: Rect): number {\n const w = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);\n const h = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);\n return w > 0 && h > 0 ? w * h : 0;\n}\n\n// ---------------------------------------------------------------------------\n// Offset strategies\n// ---------------------------------------------------------------------------\n\n/** An offset position to try when resolving a label collision. */\nexport interface OffsetStrategy {\n dx: number;\n dy: number;\n}\n\n/** Offsets to try when a label collides with an existing placement. */\nexport const OFFSET_STRATEGIES: readonly OffsetStrategy[] = [\n { dx: 0, dy: 0 }, // original position\n { dx: 0, dy: -1.2 }, // above (factor of height)\n { dx: 0, dy: 1.2 }, // below\n { dx: 1.1, dy: 0 }, // right\n { dx: -1.1, dy: 0 }, // left\n { dx: 1.1, dy: -1.2 }, // upper-right\n { dx: -1.1, dy: -1.2 }, // upper-left\n];\n\n/**\n * Extended offset strategies with additional vertical spread for dense\n * multi-series endpoints (e.g. 5+ line series converging at similar y-values).\n */\nexport const EXTENDED_OFFSET_STRATEGIES: readonly OffsetStrategy[] = [\n ...OFFSET_STRATEGIES,\n { dx: 0, dy: -2.4 }, // further above\n { dx: 0, dy: 2.4 }, // further below\n { dx: 0, dy: -3.6 }, // even further above\n { dx: 0, dy: 3.6 }, // even further below\n { dx: 1.1, dy: -2.4 }, // upper-right far\n { dx: 1.1, dy: 2.4 }, // lower-right far\n];\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve label collisions using a greedy placement algorithm.\n *\n * Sorts labels by priority (data > annotation > axis), then places\n * each label at its preferred position. If it collides with an already-placed\n * label, tries offset positions. If no position works, the label is\n * demoted to tooltip-only (visible: false).\n *\n * @param labels - Array of label candidates to position.\n * @param strategies - Optional offset strategies to use (defaults to OFFSET_STRATEGIES).\n * @returns Array of resolved labels with computed positions and visibility.\n */\nexport function resolveCollisions(\n labels: LabelCandidate[],\n strategies: readonly OffsetStrategy[] = OFFSET_STRATEGIES,\n): ResolvedLabel[] {\n // Sort by priority (highest first)\n const sorted = [...labels].sort(\n (a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority],\n );\n\n const placed: Rect[] = [];\n const results: ResolvedLabel[] = [];\n\n for (const label of sorted) {\n let bestRect: Rect | null = null;\n let bestX = label.anchorX;\n let bestY = label.anchorY;\n\n // Try each offset strategy\n for (const offset of strategies) {\n const candidateX = label.anchorX + offset.dx * label.width;\n const candidateY = label.anchorY + offset.dy * label.height;\n const candidateRect: Rect = {\n x: candidateX,\n y: candidateY,\n width: label.width,\n height: label.height,\n };\n\n const hasCollision = placed.some((p) => detectCollision(candidateRect, p));\n\n if (!hasCollision) {\n bestRect = candidateRect;\n bestX = candidateX;\n bestY = candidateY;\n break;\n }\n }\n\n if (bestRect) {\n placed.push(bestRect);\n const nudgeDx = bestX - label.anchorX;\n const nudgeDy = bestY - label.anchorY;\n const nudgeDist = Math.sqrt(nudgeDx * nudgeDx + nudgeDy * nudgeDy);\n // Only show a connector when the label is nudged far enough that the\n // reader can't tell which data point it belongs to. Short nudges between\n // adjacent series labels produce visual noise without aiding comprehension.\n const MIN_CONNECTOR_DISTANCE = 20;\n const needsConnector = nudgeDist >= MIN_CONNECTOR_DISTANCE;\n\n results.push({\n text: label.text,\n x: bestX,\n y: bestY,\n style: label.style,\n visible: true,\n // Only carry the index through when the caller set one, so labels that\n // don't need it keep an unchanged output shape.\n ...(label.index !== undefined ? { index: label.index } : {}),\n connector: needsConnector\n ? {\n from: { x: bestX, y: bestY },\n to: { x: label.anchorX, y: label.anchorY },\n stroke: label.style.fill,\n style: 'straight' as const,\n arrow: false,\n }\n : undefined,\n });\n } else {\n // No position works, demote to tooltip-only\n results.push({\n text: label.text,\n x: label.anchorX,\n y: label.anchorY,\n style: label.style,\n visible: false,\n ...(label.index !== undefined ? { index: label.index } : {}),\n });\n }\n }\n\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Label bounds estimation\n// ---------------------------------------------------------------------------\n\n/**\n * Compute the bounding rect of a resolved label from its position and text.\n * Uses heuristic text measurement so it works without DOM access.\n *\n * label.y is the SVG text anchor point, whose meaning depends on the label's\n * dominant-baseline. The rect's top edge is derived accordingly so the box\n * covers the actual glyphs (obstacles were sitting ~0.8*fontSize below the\n * text for alphabetic-baseline labels, e.g. column value labels):\n * - 'central': y is the vertical center, so top = y - height/2.\n * - 'hanging'/'text-after-edge': y is the top edge already.\n * - default (alphabetic): y is the baseline, so top = y - fontSize*0.8\n * (matches textAscent, the top-to-baseline distance renderers apply).\n */\nexport function computeLabelBounds(label: ResolvedLabel): Rect {\n const fontSize = label.style.fontSize;\n const fontWeight = label.style.fontWeight;\n const width = estimateTextWidth(label.text, fontSize, fontWeight);\n const height = fontSize * (label.style.lineHeight ?? 1.2);\n\n // Adjust x based on text anchor\n let x = label.x;\n if (label.style.textAnchor === 'middle') {\n x = label.x - width / 2;\n } else if (label.style.textAnchor === 'end') {\n x = label.x - width;\n }\n\n // Map the anchor y to the box top based on dominant-baseline.\n const baseline = label.style.dominantBaseline;\n let y: number;\n if (baseline === 'central') {\n y = label.y - height / 2;\n } else if (baseline === 'hanging' || baseline === 'text-after-edge') {\n y = label.y;\n } else {\n y = label.y - fontSize * 0.8;\n }\n\n return { x, y, width, height };\n}\n","/**\n * Locale-aware number and date formatting utilities.\n *\n * Uses d3-format and d3-time-format for formatting,\n * with convenience wrappers for common patterns.\n */\n\nimport { format as d3Format } from 'd3-format';\nimport { timeFormat, utcFormat } from 'd3-time-format';\n\n// ---------------------------------------------------------------------------\n// Cached d3-format instances\n// ---------------------------------------------------------------------------\n\n// Module-level caching assumes the d3-format/d3-time-format default locale;\n// consumer `formatDefaultLocale()` calls are unsupported (verified: no\n// callers of formatDefaultLocale exist in this codebase).\nconst FORMAT_INTEGER = d3Format(',');\nconst FORMAT_TINY = d3Format('.2~r');\nconst FORMAT_DECIMAL = d3Format(',.2f');\nconst FORMAT_ABBREV_MANTISSA = d3Format('.1~f');\nconst FORMAT_PERCENT_FRACTION = d3Format('.1~%');\nconst FORMAT_PERCENT_RAW = d3Format(',.1~f');\nconst FORMAT_CURRENCY_INTEGER = d3Format('$,');\nconst FORMAT_CURRENCY_DECIMAL = d3Format('$,.2f');\nconst FORMAT_STEP_FRACTION = [d3Format('.0~f'), d3Format('.1~f'), d3Format('.2~f')] as const;\n\n// ---------------------------------------------------------------------------\n// Number formatting\n// ---------------------------------------------------------------------------\n\n/**\n * Format a number with locale-appropriate separators.\n *\n * Uses d3-format under the hood. Default format: comma-separated\n * with auto-precision (e.g. 1500000 -> \"1,500,000\").\n *\n * @param value - The number to format.\n * @param locale - Locale string (currently unused, reserved for i18n).\n */\nexport function formatNumber(value: number, _locale?: string): string {\n if (!Number.isFinite(value)) return String(value);\n if (Number.isInteger(value)) {\n return FORMAT_INTEGER(value);\n }\n if (Math.abs(value) < 0.005) {\n return FORMAT_TINY(value);\n }\n return FORMAT_DECIMAL(value);\n}\n\n// ---------------------------------------------------------------------------\n// Number abbreviation\n// ---------------------------------------------------------------------------\n\nconst UNIT_TABLE: Array<{ threshold: number; suffix: string; divisor: number }> = [\n { threshold: 1_000_000_000_000, suffix: 'T', divisor: 1_000_000_000_000 },\n { threshold: 1_000_000_000, suffix: 'B', divisor: 1_000_000_000 },\n { threshold: 1_000_000, suffix: 'M', divisor: 1_000_000 },\n { threshold: 1_000, suffix: 'k', divisor: 1_000 },\n];\n\nfunction unitFor(abs: number): { suffix: string; divisor: number } {\n for (const entry of UNIT_TABLE) {\n if (abs >= entry.threshold) return entry;\n }\n return { suffix: '', divisor: 1 };\n}\n\nfunction fractionDigits(x: number): number {\n const a = Math.abs(x);\n for (let p = 0; p <= 3; p++) {\n const scaled = a * 10 ** p;\n if (Math.abs(scaled - Math.round(scaled)) < 1e-9) return p;\n }\n return 4;\n}\n\nexport function abbreviateNumber(value: number): string {\n if (!Number.isFinite(value)) return String(value);\n\n const absValue = Math.abs(value);\n const sign = value < 0 ? '-' : '';\n\n for (let i = 0; i < UNIT_TABLE.length; i++) {\n const { threshold, suffix, divisor } = UNIT_TABLE[i];\n if (absValue >= threshold) {\n const mantissa = FORMAT_ABBREV_MANTISSA(absValue / divisor);\n // Roll-up: if mantissa rounds to 1000, use the next unit up.\n // When i === 0 (T), there's no larger unit so we emit \"1000T\".\n if (mantissa === '1000' && i > 0) {\n return `${sign}1${UNIT_TABLE[i - 1].suffix}`;\n }\n return `${sign}${mantissa}${suffix}`;\n }\n }\n\n return formatNumber(value);\n}\n\n// ---------------------------------------------------------------------------\n// Ordinal formatting\n// ---------------------------------------------------------------------------\n\n/**\n * Format a number as an English ordinal: 1 -> \"1st\", 2 -> \"2nd\", 3 -> \"3rd\",\n * 11 -> \"11th\", 22 -> \"22nd\". Values are rounded to the nearest integer first;\n * intended for rank axes where the domain is integer positions.\n */\nexport function formatOrdinal(value: number): string {\n if (!Number.isFinite(value)) return String(value);\n const n = Math.round(value);\n const abs = Math.abs(n);\n const rem100 = abs % 100;\n const rem10 = abs % 10;\n let suffix: string;\n if (rem100 >= 11 && rem100 <= 13) suffix = 'th';\n else if (rem10 === 1) suffix = 'st';\n else if (rem10 === 2) suffix = 'nd';\n else if (rem10 === 3) suffix = 'rd';\n else suffix = 'th';\n return `${n}${suffix}`;\n}\n\n// ---------------------------------------------------------------------------\n// d3-format with suffix support\n// ---------------------------------------------------------------------------\n\n/**\n * Regex that matches a valid d3-format specifier (with optional ~ trim flag)\n * followed by a literal suffix. The first capture group is the d3 format part,\n * the second is the trailing literal suffix (e.g. \"T\", \"pp\", etc.).\n *\n * Examples:\n * - \"$,.2~fT\" -> [\"$,.2~f\", \"T\"]\n * - \".0f%\" -> [\".0f\", \"%\"] (% here is literal, not d3 percent type)\n * - \"$,.0f\" -> no match (valid d3 format, no suffix)\n */\nconst D3_FORMAT_SUFFIX_RE = /^(.*~?[efgsrdxXobcnp%])(.+)$/;\n\n/**\n * Build a number formatter from a d3-format string, with support for a\n * trailing literal suffix that d3-format itself would reject.\n *\n * Also accepts the OpenChart extension `'ordinal'`, which renders English\n * ordinals (\"1st\", \"2nd\", \"3rd\") for rank axes.\n *\n * Returns null if the format string is falsy or unparseable.\n */\nexport function buildD3Formatter(formatStr: string | undefined): ((v: number) => string) | null {\n if (!formatStr) return null;\n\n // OpenChart extension: rank-axis ordinals (\"1st\", \"2nd\", \"3rd\").\n if (formatStr === 'ordinal') return formatOrdinal;\n\n // OpenChart extension: 'compact' abbreviates (1.2k, 3.4M). Charts abbreviate\n // by default; on a table, where full precision is the default, this is how a\n // column opts in. Handled here so every resolution path picks it up.\n if (formatStr === 'compact') {\n return (v: number) => {\n if (!Number.isFinite(v)) return String(v);\n return Math.abs(v) >= 1000 ? abbreviateNumber(v) : formatNumber(v);\n };\n }\n\n try {\n const fmt = d3Format(formatStr);\n // Replace SI prefix \"G\" (giga) with \"B\" (billion) for financial readability\n if (formatStr.includes('s')) {\n return (v: number) => fmt(v).replace(/G$/, 'B');\n }\n return fmt;\n } catch {\n // If d3-format rejects it, try stripping a trailing literal suffix\n const m = formatStr.match(D3_FORMAT_SUFFIX_RE);\n if (m) {\n try {\n const fmt = d3Format(m[1]);\n const suffix = m[2];\n return (v: number) => fmt(v) + suffix;\n } catch {\n // Unparseable even after suffix stripping\n }\n }\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Number formatter type + semantic formatters\n// ---------------------------------------------------------------------------\n\nexport type NumberFormatter = (value: number) => string;\n\nexport function formatPercent(value: number, options?: { fraction?: boolean }): string {\n const fraction = options?.fraction ?? true;\n if (fraction) {\n return FORMAT_PERCENT_FRACTION(value);\n }\n return `${FORMAT_PERCENT_RAW(value)}%`;\n}\n\nexport function formatCurrency(value: number, options?: { compact?: boolean }): string {\n const compact = options?.compact ?? false;\n const abs = Math.abs(value);\n const sign = value < 0 ? '-' : '';\n\n if (compact) {\n if (abs >= 1000) {\n return `${sign}$${abbreviateNumber(abs)}`;\n }\n return `${sign}$${formatNumber(abs)}`;\n }\n if (Number.isInteger(value)) {\n return sign + FORMAT_CURRENCY_INTEGER(abs);\n }\n return sign + FORMAT_CURRENCY_DECIMAL(abs);\n}\n\n// ---------------------------------------------------------------------------\n// Years guard\n// ---------------------------------------------------------------------------\n\nexport function isYearLikeValues(values: readonly number[]): boolean {\n if (values.length === 0) return false;\n for (const v of values) {\n if (!Number.isFinite(v) || !Number.isInteger(v) || v < 1500 || v > 2500) return false;\n }\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// Field format context\n// ---------------------------------------------------------------------------\n\nexport interface FieldFormatContext {\n extent?: [number, number];\n allIntegers?: boolean;\n surface?: 'chart' | 'table';\n step?: number;\n stepReference?: number;\n}\n\nexport function computeFieldFormatContext(\n values: Iterable<unknown>,\n surface?: 'chart' | 'table',\n): FieldFormatContext {\n let min = Infinity;\n let max = -Infinity;\n let allIntegers = true;\n let hasFinite = false;\n\n for (const raw of values) {\n const v = typeof raw === 'number' ? raw : typeof raw === 'string' ? Number(raw) : NaN;\n if (!Number.isFinite(v)) continue;\n hasFinite = true;\n if (v < min) min = v;\n if (v > max) max = v;\n if (allIntegers && !Number.isInteger(v)) allIntegers = false;\n }\n\n const ctx: FieldFormatContext = {};\n if (hasFinite) {\n ctx.extent = [min, max];\n ctx.allIntegers = allIntegers;\n }\n if (surface) ctx.surface = surface;\n return ctx;\n}\n\nexport function isYearContext(ctx: FieldFormatContext | undefined): boolean {\n if (!ctx?.extent || !ctx.allIntegers) return false;\n const [min, max] = ctx.extent;\n return min >= 1500 && max <= 2500;\n}\n\n// ---------------------------------------------------------------------------\n// Keyword-aware formatter resolution\n// ---------------------------------------------------------------------------\n\nexport function resolveNumberFormatter(\n formatStr: string | undefined,\n ctx?: FieldFormatContext,\n): NumberFormatter | null {\n if (!formatStr) return null;\n\n if (formatStr === 'ordinal') return formatOrdinal;\n\n // OpenChart extension: 'compact' abbreviates (1.2k, 3.4M). Charts abbreviate\n // by default; on a table, where full precision is the default, this is how a\n // column opts in. Handled here so every resolution path picks it up.\n if (formatStr === 'compact') {\n return (v: number) => {\n if (!Number.isFinite(v)) return String(v);\n return Math.abs(v) >= 1000 ? abbreviateNumber(v) : formatNumber(v);\n };\n }\n\n if (formatStr === 'percent') {\n // Heuristic: if all values fit in [0,1] (maxAbs <= 1), treat them as\n // fractions (0.45 -> \"45%\"). Values > 1 are treated as pre-scaled\n // (45 -> \"45%\"). Datasets like [0, 0.5, 1.1] are ambiguous; the <= 1\n // threshold errs on the side of the most common convention (fractions).\n const fraction = ctx?.extent\n ? Math.max(Math.abs(ctx.extent[0]), Math.abs(ctx.extent[1])) <= 1\n : true;\n return (v: number) => formatPercent(v, { fraction });\n }\n\n if (formatStr === 'currency') {\n const isTable = ctx?.surface === 'table';\n if (!isTable && ctx?.step != null && (ctx.stepReference ?? 0) >= 1000) {\n const stepFmt = buildCompactStepFormatter(ctx.step);\n return (v: number) => {\n const sign = v < 0 ? '-' : '';\n return `${sign}$${stepFmt(Math.abs(v))}`;\n };\n }\n return (v: number) => formatCurrency(v, { compact: !isTable });\n }\n\n return buildD3Formatter(formatStr);\n}\n\n// ---------------------------------------------------------------------------\n// Default number formatter\n// ---------------------------------------------------------------------------\n\nexport function defaultNumberFormatter(ctx?: FieldFormatContext): NumberFormatter {\n if (isYearContext(ctx)) {\n return (v: number) => String(Math.round(v));\n }\n\n if (ctx?.surface === 'table') {\n return formatNumber;\n }\n\n if (ctx?.step != null && (ctx.stepReference ?? maxAbs(ctx.extent)) >= 1000) {\n return buildCompactStepFormatter(ctx.step);\n }\n\n return (v: number) => {\n if (!Number.isFinite(v)) return String(v);\n // When no context is provided (ARIA marks, standalone helpers) we\n // fall back to a per-value year heuristic: integers in [1500, 2500]\n // render bare. This trades false positives on non-year integers\n // (e.g. \"connections: 2024\") for avoiding \"2k\" on actual year axes.\n // Callers with field context should always pass it to get the\n // field-level isYearContext check instead.\n if (ctx?.extent === undefined && ctx?.allIntegers === undefined) {\n if (Number.isInteger(v) && v >= 1500 && v <= 2500) return String(v);\n }\n return Math.abs(v) >= 1000 ? abbreviateNumber(v) : formatNumber(v);\n };\n}\n\nfunction maxAbs(extent: [number, number] | undefined): number {\n if (!extent) return 0;\n return Math.max(Math.abs(extent[0]), Math.abs(extent[1]));\n}\n\n// ---------------------------------------------------------------------------\n// Step-aware compact tick formatter\n// ---------------------------------------------------------------------------\n\nexport function buildCompactStepFormatter(step: number): NumberFormatter {\n return (v: number) => {\n if (v === 0) return '0';\n if (!Number.isFinite(v)) return String(v);\n const abs = Math.abs(v);\n if (abs < 1000) return formatNumber(v);\n const { suffix, divisor } = unitFor(abs);\n const p = fractionDigits(step / divisor);\n if (p > 2) {\n return Number.isInteger(v) ? FORMAT_INTEGER(v) : FORMAT_DECIMAL(v);\n }\n const sign = v < 0 ? '-' : '';\n return sign + FORMAT_STEP_FRACTION[p](abs / divisor) + suffix;\n };\n}\n\n// ---------------------------------------------------------------------------\n// Date formatting\n// ---------------------------------------------------------------------------\n\n/** Granularity levels for date formatting. */\nexport type DateGranularity = 'year' | 'quarter' | 'month' | 'week' | 'day' | 'hour' | 'minute';\n\n/** Format strings for each granularity level. */\nconst GRANULARITY_FORMATS: Record<DateGranularity, string> = {\n year: '%Y',\n quarter: '', // Quarter is always special-cased in formatDate() below\n month: '%b %Y',\n week: '%b %d',\n day: '%b %d, %Y',\n hour: '%b %d %H:%M',\n minute: '%H:%M',\n};\n\n/**\n * Compact format strings used by axis ticks when full-format labels overlap.\n * Month is bare '%b': granularity inference runs per tick, so year-boundary\n * ticks already render as '2025' and the axis reads \"2025 · Feb · Mar\".\n * Year stays '%Y' — abbreviating to \"'25\" saves too little for the ambiguity.\n */\nconst GRANULARITY_FORMATS_COMPACT: Record<DateGranularity, string> = {\n year: '%Y',\n quarter: '', // special-cased below, like GRANULARITY_FORMATS\n month: '%b',\n week: '%b %d',\n day: '%b %d',\n hour: '%H:%M',\n minute: '%H:%M',\n};\n\n/**\n * Format a date value for display.\n *\n * @param value - Date object, ISO string, or timestamp number.\n * @param locale - Locale string (currently unused, reserved for i18n).\n * @param granularity - Time granularity for format selection.\n * @param useUtc - Whether to infer granularity and format using UTC methods.\n * Pass `false` when formatting ticks from a local-time scale (d3 scaleTime),\n * so that e.g. midnight local isn't misread as an intra-day UTC time.\n * Defaults to `true` for backward compatibility.\n * @param compact - Use the compact format table (axis ticks whose full-format\n * labels would overlap). Defaults to `false`.\n */\nexport function formatDate(\n value: Date | string | number,\n _locale?: string,\n granularity?: DateGranularity,\n useUtc: boolean = true,\n compact: boolean = false,\n): string {\n const date = value instanceof Date ? value : new Date(value);\n if (Number.isNaN(date.getTime())) return String(value);\n\n const gran = granularity ?? inferGranularity(date, useUtc);\n\n // Special handling for quarter (not a d3 format token)\n if (gran === 'quarter') {\n const q = useUtc\n ? Math.ceil((date.getUTCMonth() + 1) / 3)\n : Math.ceil((date.getMonth() + 1) / 3);\n const year = useUtc ? date.getUTCFullYear() : date.getFullYear();\n if (compact) {\n return `Q${q} '${String(year).slice(-2)}`;\n }\n return `Q${q} ${year}`;\n }\n\n const formatStr = (compact ? GRANULARITY_FORMATS_COMPACT : GRANULARITY_FORMATS)[gran];\n if (useUtc) {\n return getUtcFormatter(formatStr)(date);\n }\n return getTimeFormatter(formatStr)(date);\n}\n\n// Compiled d3-time-format formatters, keyed by format string. The key space\n// is bounded to the strings in GRANULARITY_FORMATS / GRANULARITY_FORMATS_COMPACT.\nconst TIME_FORMAT_CACHE = new Map<string, ReturnType<typeof timeFormat>>();\nconst UTC_FORMAT_CACHE = new Map<string, ReturnType<typeof utcFormat>>();\n\nfunction getTimeFormatter(formatStr: string): ReturnType<typeof timeFormat> {\n let fmt = TIME_FORMAT_CACHE.get(formatStr);\n if (!fmt) {\n fmt = timeFormat(formatStr);\n TIME_FORMAT_CACHE.set(formatStr, fmt);\n }\n return fmt;\n}\n\nfunction getUtcFormatter(formatStr: string): ReturnType<typeof utcFormat> {\n let fmt = UTC_FORMAT_CACHE.get(formatStr);\n if (!fmt) {\n fmt = utcFormat(formatStr);\n UTC_FORMAT_CACHE.set(formatStr, fmt);\n }\n return fmt;\n}\n\n/**\n * Build a formatter for temporal values using a d3-time-format string (e.g. \"%Y\", \"%b %Y\").\n * Returns a function that accepts a Date, string, or number and returns the formatted string.\n * Returns null if the format string is falsy.\n */\nexport function buildTemporalFormatter(\n formatStr: string | undefined,\n): ((value: Date | string | number) => string) | null {\n if (!formatStr) return null;\n const fmt = utcFormat(formatStr);\n return (value: Date | string | number) => {\n const date = value instanceof Date ? value : new Date(value);\n if (Number.isNaN(date.getTime())) return String(value);\n return fmt(date);\n };\n}\n\n/**\n * Infer the appropriate granularity from a date value.\n * If time components are all zero, assume day or higher.\n *\n * @param useUtc - When true, inspect UTC fields. When false, inspect local-time\n * fields. This must match the D3 scale type: scaleUtc -> true, scaleTime -> false.\n */\nfunction inferGranularity(date: Date, useUtc: boolean = true): DateGranularity {\n const hours = useUtc ? date.getUTCHours() : date.getHours();\n const minutes = useUtc ? date.getUTCMinutes() : date.getMinutes();\n const day = useUtc ? date.getUTCDate() : date.getDate();\n const month = useUtc ? date.getUTCMonth() : date.getMonth();\n\n if (hours !== 0 || minutes !== 0) {\n return minutes !== 0 ? 'minute' : 'hour';\n }\n if (day !== 1) return 'day';\n if (month !== 0) return 'month';\n return 'year';\n}\n","/**\n * Automatic alt text generation for chart accessibility.\n *\n * Produces human-readable descriptions of chart content for screen readers\n * and a tabular fallback for data access.\n */\n\nimport type { ChartSpec, DataRow } from '../types/spec';\nimport { MARK_DISPLAY_NAMES, resolveMarkDef, resolveMarkType } from '../types/spec';\n\n// ---------------------------------------------------------------------------\n// Alt text generation\n// ---------------------------------------------------------------------------\n\n/**\n * Generate alt text describing a chart's content.\n *\n * Produces a description like:\n * \"Line chart showing GDP Growth Rate from 2020 to 2024 with 2 series (US, UK)\"\n *\n * @param spec - The chart spec.\n * @param data - The data array.\n */\nexport function generateAltText(spec: ChartSpec, data: DataRow[]): string {\n const markType = resolveMarkType(spec.mark);\n const markDef = resolveMarkDef(spec.mark);\n let chartName = MARK_DISPLAY_NAMES[markType] ?? `${markType} chart`;\n\n // Special case: donut detection\n if (markType === 'arc' && markDef.innerRadius && markDef.innerRadius > 0) {\n chartName = 'Donut chart';\n }\n\n // Special case: histogram detection. A binned bar reaches here already\n // desugared to `bar`, so without this a distribution announces itself as\n // \"Bar chart\" and the reader loses what the chart is actually showing.\n // Matches the renderer's own dispatch condition, quantitative y included:\n // a nominal-y bar with x2 is a floating range bar, not a histogram.\n const isBinnedBar =\n markType === 'bar' &&\n !!spec.encoding.x2 &&\n spec.encoding.x?.type === 'quantitative' &&\n spec.encoding.y?.type === 'quantitative';\n if (isBinnedBar) {\n chartName = 'Histogram';\n }\n\n const parts: string[] = [chartName];\n\n // Add title context if present\n const title = spec.chrome?.title;\n if (title) {\n const titleText = typeof title === 'string' ? title : title.text;\n parts.push(`showing ${titleText}`);\n }\n\n // Waffle: name the unit framing. Per-category \"x of N units\" phrasing\n // lives on the mark aria labels; the chart-level text carries the total.\n if (markType === 'waffle') {\n const units = markDef.units ?? 100;\n parts.push(`dividing ${units} units`);\n }\n\n // Parliament: name the total seat count and the majority threshold, the two\n // numbers a reader most wants. Per-party seat counts live on the seat marks'\n // aria labels; this carries the overall framing.\n if (markType === 'parliament') {\n const valueField = spec.encoding.theta?.field ?? spec.encoding.y?.field;\n if (valueField) {\n const totalSeats = data.reduce((sum, d) => {\n const v = Number(d[valueField]);\n return sum + (Number.isFinite(v) && v > 0 ? v : 0);\n }, 0);\n if (totalSeats > 0) {\n const majority = Math.floor(totalSeats / 2) + 1;\n parts.push(`with ${totalSeats} seats, ${majority} needed for a majority`);\n }\n }\n }\n\n // Beeswarm: name the distribution fields (value axis + optional lane).\n // Skips the generic x-axis description below, which would either say\n // nothing (quantitative value axis) or repeat the lane count.\n if (markType === 'beeswarm') {\n const xIsValue = spec.encoding.x?.type === 'quantitative';\n const valueField = xIsValue ? spec.encoding.x?.field : spec.encoding.y?.field;\n const laneField = xIsValue ? spec.encoding.y?.field : spec.encoding.x?.field;\n if (valueField) {\n parts.push(\n `plotting the distribution of ${valueField}${laneField ? ` by ${laneField}` : ''}`,\n );\n }\n }\n\n // Histogram: name the field whose distribution is being shown. The x field\n // at this point is the bin-start output, so read the channel title the bin\n // sugar stamped with the original field name.\n if (isBinnedBar) {\n const sourceField = spec.encoding.x?.title ?? spec.encoding.x?.field;\n if (sourceField) {\n parts.push(`showing the distribution of ${sourceField}`);\n }\n }\n\n // Describe the data range\n if (spec.encoding.x && markType !== 'beeswarm' && data.length > 0) {\n const field = spec.encoding.x.field;\n const values = data.map((d) => d[field]).filter((v) => v != null);\n\n if (values.length > 0) {\n if (spec.encoding.x.type === 'temporal') {\n const dates = values.map((v) => (v instanceof Date ? v : new Date(String(v))));\n const validDates = dates.filter((d) => !Number.isNaN(d.getTime()));\n if (validDates.length >= 2) {\n validDates.sort((a, b) => a.getTime() - b.getTime());\n const first = validDates[0].getUTCFullYear();\n const last = validDates[validDates.length - 1].getUTCFullYear();\n if (first !== last) {\n parts.push(`from ${first} to ${last}`);\n }\n }\n } else if (spec.encoding.x.type === 'nominal' || spec.encoding.x.type === 'ordinal') {\n const uniqueValues = [...new Set(values.map(String))];\n parts.push(`across ${uniqueValues.length} categories`);\n }\n }\n }\n\n // Describe series if color encoding is present\n if (spec.encoding.color && 'field' in spec.encoding.color && data.length > 0) {\n const colorField = spec.encoding.color.field;\n const uniqueSeries = [...new Set(data.map((d) => String(d[colorField])).filter(Boolean))];\n if (uniqueSeries.length > 0) {\n parts.push(`with ${uniqueSeries.length} series (${uniqueSeries.join(', ')})`);\n }\n }\n\n // Add data point count\n parts.push(`(${data.length} data points)`);\n\n return parts.join(' ');\n}\n\n// ---------------------------------------------------------------------------\n// Data table fallback\n// ---------------------------------------------------------------------------\n\n/**\n * Maximum number of data rows in the screen-reader table. A 50k-point scatter\n * would otherwise put 50k hidden rows in the DOM.\n */\nexport const MAX_SR_TABLE_ROWS = 1000;\n\n/**\n * Generate a tabular data fallback for screen readers.\n *\n * Returns a 2D array where the first row is headers and subsequent\n * rows are data values. Only includes fields referenced in the encoding.\n *\n * Data rows are capped at MAX_SR_TABLE_ROWS (the header row does not count\n * against the cap). SCOPE: this caps the chart compile path only. Graph,\n * sankey, tilemap and map build their `dataTableFallback` independently and\n * stay uncapped.\n *\n * @param spec - The chart spec.\n * @param data - The data array.\n */\nexport function generateDataTable(spec: ChartSpec, data: DataRow[]): unknown[][] {\n // Collect fields from encoding\n const fields: string[] = [];\n const encoding = spec.encoding;\n\n if (encoding.x) fields.push(encoding.x.field);\n if (encoding.y) fields.push(encoding.y.field);\n if (encoding.color && 'field' in encoding.color) fields.push(encoding.color.field);\n if (encoding.size && 'field' in encoding.size) fields.push(encoding.size.field);\n\n // Deduplicate\n const uniqueFields = [...new Set(fields)];\n\n if (uniqueFields.length === 0) return [];\n\n // Header row\n const headers = uniqueFields;\n\n // Data rows\n const rows = data\n .slice(0, MAX_SR_TABLE_ROWS)\n .map((row) => uniqueFields.map((field) => row[field] ?? ''));\n\n return [headers, ...rows];\n}\n","/**\n * ARIA label generation for individual marks.\n *\n * Produces per-mark labels for screen reader navigation, enabling\n * users to explore data points individually.\n */\n\nimport { defaultNumberFormatter } from '../locale/format';\nimport type { Mark } from '../types/layout';\n\n/**\n * Generate ARIA labels for a set of marks.\n *\n * Returns a Map keyed by a mark identifier (index-based) with\n * descriptive labels like \"Data point: US GDP 42 in 2020-01\".\n *\n * @param marks - Array of mark objects from the compiled chart layout.\n */\nexport function generateAriaLabels(marks: Mark[]): Map<string, string> {\n const labels = new Map<string, string>();\n\n for (let i = 0; i < marks.length; i++) {\n const mark = marks[i];\n const key = `mark-${i}`;\n\n switch (mark.type) {\n case 'line': {\n const series = mark.seriesKey ?? 'Series';\n const pointCount = mark.points.length;\n labels.set(key, `Line series: ${series} with ${pointCount} points`);\n break;\n }\n\n case 'area': {\n const series = mark.seriesKey ?? 'Area';\n labels.set(key, `Area series: ${series}`);\n break;\n }\n\n case 'rect': {\n const dataEntries = Object.entries(mark.data).filter(([k]) => !k.startsWith('_'));\n const description = dataEntries.map(([k, v]) => `${k}: ${formatValue(v)}`).join(', ');\n labels.set(key, `Data point: ${description}`);\n break;\n }\n\n case 'arc': {\n const dataEntries = Object.entries(mark.data).filter(([k]) => !k.startsWith('_'));\n const description = dataEntries.map(([k, v]) => `${k}: ${formatValue(v)}`).join(', ');\n labels.set(key, `Slice: ${description}`);\n break;\n }\n\n case 'point': {\n const dataEntries = Object.entries(mark.data).filter(([k]) => !k.startsWith('_'));\n const description = dataEntries.map(([k, v]) => `${k}: ${formatValue(v)}`).join(', ');\n labels.set(key, `Data point: ${description}`);\n break;\n }\n }\n }\n\n return labels;\n}\n\nconst ariaNumberFormatter = defaultNumberFormatter();\n\nfunction formatValue(value: unknown): string {\n if (value == null) return 'N/A';\n if (value instanceof Date) return value.toISOString().slice(0, 10);\n if (typeof value === 'number') {\n return ariaNumberFormatter(value);\n }\n return String(value);\n}\n","/**\n * Spec construction helpers: typed builder functions for common chart types.\n *\n * These builders reduce boilerplate when creating specs programmatically.\n * They accept field names as strings (simple case) or full EncodingChannel\n * objects (when you need to customize type, aggregate, axis, or scale).\n *\n * Type inference: when a string field name is provided, the builder samples\n * data values to infer the encoding type (quantitative, temporal, nominal).\n */\n\nimport type {\n AnimationSpec,\n Annotation,\n ChartSpec,\n Chrome,\n DarkMode,\n DataRow,\n Encoding,\n EncodingChannel,\n FieldType,\n LegendConfig,\n MarkDef,\n MarkType,\n TableSpec,\n ThemeConfig,\n TileMapEncoding,\n TileMapPalette,\n TileMapSpec,\n} from '../types/spec';\nimport type { ColumnConfig } from '../types/table';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** A field reference: either a plain string (field name) or a full EncodingChannel. */\nexport type FieldRef = string | EncodingChannel;\n\n/** Common options shared by all chart builder functions. */\nexport interface ChartBuilderOptions {\n /** Color encoding: field name or full channel config for series differentiation. */\n color?: FieldRef;\n /** Size encoding: field name or full channel config. */\n size?: FieldRef;\n /** Editorial chrome (title, subtitle, source, etc.). */\n chrome?: Chrome;\n /** Data annotations. */\n annotations?: Annotation[];\n /** Whether the chart adapts to container width. Defaults to true. */\n responsive?: boolean;\n /** Theme configuration overrides. */\n theme?: ThemeConfig;\n /** Dark mode behavior. */\n darkMode?: DarkMode;\n}\n\n/** Options for the dataTable builder. */\nexport interface TableBuilderOptions {\n /** Column definitions. Auto-generated from data keys if omitted. */\n columns?: ColumnConfig[];\n /** Field to use as a unique row identifier. */\n rowKey?: string;\n /** Editorial chrome. */\n chrome?: Chrome;\n /** Theme configuration overrides. */\n theme?: ThemeConfig;\n /** Dark mode behavior. */\n darkMode?: DarkMode;\n /** Enable client-side search/filter. */\n search?: boolean;\n /** Pagination configuration. */\n pagination?: boolean | { pageSize: number };\n /** Stick the first column during horizontal scroll. */\n stickyFirstColumn?: boolean;\n /** Compact mode. */\n compact?: boolean;\n /** Whether the table adapts to container width. */\n responsive?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Type inference\n// ---------------------------------------------------------------------------\n\n/** ISO 8601 date patterns we recognize for temporal inference. */\nconst ISO_DATE_RE = /^\\d{4}(-\\d{2}(-\\d{2}(T\\d{2}(:\\d{2}(:\\d{2})?)?)?)?)?$/;\n\n/**\n * Infer the encoding field type from data values.\n *\n * Samples up to 20 values from the data array for the given field.\n * - If all sampled values are numbers (or null/undefined), returns \"quantitative\".\n * - If all sampled string values match ISO date patterns, returns \"temporal\".\n * - Otherwise returns \"nominal\".\n */\nexport function inferFieldType(data: DataRow[], field: string): FieldType {\n const sampleSize = Math.min(data.length, 20);\n let hasNumber = false;\n let hasDateString = false;\n let hasOtherString = false;\n\n for (let i = 0; i < sampleSize; i++) {\n const value = data[i][field];\n\n // Skip null/undefined\n if (value == null) continue;\n\n if (typeof value === 'number') {\n hasNumber = true;\n } else if (typeof value === 'string') {\n if (ISO_DATE_RE.test(value) && !Number.isNaN(Date.parse(value))) {\n hasDateString = true;\n } else {\n hasOtherString = true;\n }\n } else if (value instanceof Date) {\n hasDateString = true;\n } else {\n hasOtherString = true;\n }\n }\n\n // Numbers-only: quantitative\n if (hasNumber && !hasDateString && !hasOtherString) return 'quantitative';\n // Date strings/Date objects only: temporal\n if (hasDateString && !hasNumber && !hasOtherString) return 'temporal';\n // Everything else: nominal\n return 'nominal';\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve a FieldRef into a full EncodingChannel.\n * If the ref is a string, infer the type from data.\n */\nfunction resolveField(ref: FieldRef, data: DataRow[]): EncodingChannel {\n if (typeof ref === 'string') {\n return { field: ref, type: inferFieldType(data, ref) };\n }\n return ref;\n}\n\n/** Build the encoding object from resolved channels. */\nfunction buildEncoding(\n channels: { x?: EncodingChannel; y?: EncodingChannel },\n options?: ChartBuilderOptions,\n data?: DataRow[],\n): Encoding {\n const encoding: Encoding = { ...channels };\n if (options?.color && data) {\n encoding.color = resolveField(options.color, data);\n }\n if (options?.size && data) {\n encoding.size = resolveField(options.size, data);\n }\n return encoding;\n}\n\n/** Build a ChartSpec from the resolved pieces. */\nfunction buildChartSpec(\n mark: MarkType | MarkDef,\n data: DataRow[],\n encoding: Encoding,\n options?: ChartBuilderOptions,\n): ChartSpec {\n // Cast needed because the generic discriminated union can't be constructed\n // from a runtime MarkType variable — the caller always pairs mark+encoding correctly.\n const spec = { mark, data, encoding } as ChartSpec;\n if (options?.chrome) spec.chrome = options.chrome;\n if (options?.annotations) spec.annotations = options.annotations;\n if (options?.responsive !== undefined) spec.responsive = options.responsive;\n if (options?.theme) spec.theme = options.theme;\n if (options?.darkMode) spec.darkMode = options.darkMode;\n return spec;\n}\n\n// ---------------------------------------------------------------------------\n// Builder functions\n// ---------------------------------------------------------------------------\n\n/**\n * Create a line chart spec.\n *\n * @param data - Array of data rows.\n * @param x - X-axis field (typically temporal or ordinal).\n * @param y - Y-axis field (typically quantitative).\n * @param options - Optional color, size, chrome, annotations, theme, etc.\n */\nexport function lineChart(\n data: DataRow[],\n x: FieldRef,\n y: FieldRef,\n options?: ChartBuilderOptions,\n): ChartSpec {\n const xChannel = resolveField(x, data);\n const yChannel = resolveField(y, data);\n const encoding = buildEncoding({ x: xChannel, y: yChannel }, options, data);\n return buildChartSpec('line', data, encoding, options);\n}\n\n/**\n * Create a bar chart spec (horizontal bars).\n *\n * Convention: category goes on y-axis, value on x-axis.\n * The `category` param maps to y, `value` maps to x.\n *\n * @param data - Array of data rows.\n * @param category - Category field (y-axis, nominal/ordinal).\n * @param value - Value field (x-axis, quantitative).\n * @param options - Optional color, size, chrome, annotations, theme, etc.\n */\nexport function barChart(\n data: DataRow[],\n category: FieldRef,\n value: FieldRef,\n options?: ChartBuilderOptions,\n): ChartSpec {\n const categoryChannel = resolveField(category, data);\n const valueChannel = resolveField(value, data);\n\n // Bar charts: category on y, value on x\n const encoding = buildEncoding({ x: valueChannel, y: categoryChannel }, options, data);\n return buildChartSpec('bar', data, encoding, options);\n}\n\n/**\n * Create a column chart spec (vertical columns).\n *\n * @param data - Array of data rows.\n * @param x - X-axis field (category or temporal).\n * @param y - Y-axis field (quantitative value).\n * @param options - Optional color, size, chrome, annotations, theme, etc.\n */\nexport function columnChart(\n data: DataRow[],\n x: FieldRef,\n y: FieldRef,\n options?: ChartBuilderOptions,\n): ChartSpec {\n const xChannel = resolveField(x, data);\n const yChannel = resolveField(y, data);\n const encoding = buildEncoding({ x: xChannel, y: yChannel }, options, data);\n return buildChartSpec({ type: 'bar' } as MarkDef, data, encoding, options);\n}\n\n/**\n * Create a pie chart spec.\n *\n * Convention: category maps to the color channel, value maps to theta.\n * Pie charts have no x-axis.\n *\n * @param data - Array of data rows.\n * @param category - Category field (color channel, nominal).\n * @param value - Value field (theta channel, quantitative).\n * @param options - Optional chrome, annotations, theme, etc.\n * Note: color option is ignored since category is used for color.\n */\nexport function pieChart(\n data: DataRow[],\n category: FieldRef,\n value: FieldRef,\n options?: ChartBuilderOptions,\n): ChartSpec {\n const categoryChannel = resolveField(category, data);\n const valueChannel = resolveField(value, data);\n\n // Pie charts: value on theta (canonical in v8), category on color (no x-axis)\n const encoding: Encoding = {\n theta: valueChannel,\n color: categoryChannel,\n };\n if (options?.size && data) {\n encoding.size = resolveField(options.size, data);\n }\n\n return buildChartSpec('arc', data, encoding, options);\n}\n\n/**\n * Create an area chart spec.\n *\n * @param data - Array of data rows.\n * @param x - X-axis field (typically temporal or ordinal).\n * @param y - Y-axis field (typically quantitative).\n * @param options - Optional color, size, chrome, annotations, theme, etc.\n */\nexport function areaChart(\n data: DataRow[],\n x: FieldRef,\n y: FieldRef,\n options?: ChartBuilderOptions,\n): ChartSpec {\n const xChannel = resolveField(x, data);\n const yChannel = resolveField(y, data);\n const encoding = buildEncoding({ x: xChannel, y: yChannel }, options, data);\n return buildChartSpec('area', data, encoding, options);\n}\n\n/**\n * Create a donut chart spec.\n *\n * Convention: category maps to the color channel, value maps to theta.\n * Donut charts have no x-axis.\n *\n * @param data - Array of data rows.\n * @param category - Category field (color channel, nominal).\n * @param value - Value field (theta channel, quantitative).\n * @param options - Optional chrome, annotations, theme, etc.\n * Note: color option is ignored since category is used for color.\n */\nexport function donutChart(\n data: DataRow[],\n category: FieldRef,\n value: FieldRef,\n options?: ChartBuilderOptions,\n): ChartSpec {\n const categoryChannel = resolveField(category, data);\n const valueChannel = resolveField(value, data);\n\n const encoding: Encoding = {\n theta: valueChannel,\n color: categoryChannel,\n };\n if (options?.size && data) {\n encoding.size = resolveField(options.size, data);\n }\n\n return buildChartSpec({ type: 'arc', innerRadius: 40 } as MarkDef, data, encoding, options);\n}\n\n/**\n * Create a dot chart spec (strip plot / dot plot).\n *\n * @param data - Array of data rows.\n * @param x - X-axis field (quantitative or temporal).\n * @param y - Y-axis field (nominal/categorical grouping).\n * @param options - Optional color, size, chrome, annotations, theme, etc.\n */\nexport function dotChart(\n data: DataRow[],\n x: FieldRef,\n y: FieldRef,\n options?: ChartBuilderOptions,\n): ChartSpec {\n const xChannel = resolveField(x, data);\n const yChannel = resolveField(y, data);\n const encoding = buildEncoding({ x: xChannel, y: yChannel }, options, data);\n return buildChartSpec('circle', data, encoding, options);\n}\n\n/**\n * Create a scatter chart spec.\n *\n * @param data - Array of data rows.\n * @param x - X-axis field (quantitative).\n * @param y - Y-axis field (quantitative).\n * @param options - Optional color, size, chrome, annotations, theme, etc.\n */\nexport function scatterChart(\n data: DataRow[],\n x: FieldRef,\n y: FieldRef,\n options?: ChartBuilderOptions,\n): ChartSpec {\n const xChannel = resolveField(x, data);\n const yChannel = resolveField(y, data);\n const encoding = buildEncoding({ x: xChannel, y: yChannel }, options, data);\n return buildChartSpec('point', data, encoding, options);\n}\n\n/**\n * Create a data table spec.\n *\n * If no columns are specified, auto-generates column configs from the\n * keys of the first data row.\n *\n * @param data - Array of data rows.\n * @param options - Column definitions, chrome, pagination, search, etc.\n */\nexport function dataTable(data: DataRow[], options?: TableBuilderOptions): TableSpec {\n // Auto-generate columns from data keys if not provided\n let columns = options?.columns;\n if (!columns && data.length > 0) {\n columns = Object.keys(data[0]).map((key): ColumnConfig => {\n // Infer alignment from data type\n const fieldType = inferFieldType(data, key);\n const align = fieldType === 'quantitative' ? ('right' as const) : ('left' as const);\n\n return {\n key,\n label: key,\n align,\n };\n });\n }\n\n const spec: TableSpec = {\n type: 'table',\n data,\n columns: columns ?? [],\n };\n\n if (options?.rowKey) spec.rowKey = options.rowKey;\n if (options?.chrome) spec.chrome = options.chrome;\n if (options?.theme) spec.theme = options.theme;\n if (options?.darkMode) spec.darkMode = options.darkMode;\n if (options?.search !== undefined) spec.search = options.search;\n if (options?.pagination !== undefined) spec.pagination = options.pagination;\n if (options?.stickyFirstColumn !== undefined) spec.stickyFirstColumn = options.stickyFirstColumn;\n if (options?.compact !== undefined) spec.compact = options.compact;\n if (options?.responsive !== undefined) spec.responsive = options.responsive;\n\n return spec;\n}\n\n// ---------------------------------------------------------------------------\n// Tile map builder\n// ---------------------------------------------------------------------------\n\n/** Options for the tileMap convenience builder. */\nexport interface TileMapBuilderOptions {\n /** Encoding channels (required when data is DataRow[]). */\n encoding?: TileMapEncoding;\n /** Sequential color palette name (quantitative mode only). */\n palette?: TileMapPalette;\n /** Custom category-to-color mapping. Forces categorical mode. */\n colors?: Record<string, string>;\n /** Editorial chrome. */\n chrome?: Chrome;\n /** Legend display configuration. */\n legend?: LegendConfig;\n /** Theme overrides. */\n theme?: ThemeConfig;\n /** Dark mode setting. */\n darkMode?: DarkMode;\n /** Whether to show the watermark. */\n watermark?: boolean;\n /** Animation configuration. */\n animation?: AnimationSpec;\n /** d3-format string for value formatting. */\n valueFormat?: string;\n}\n\n/**\n * Build a TileMapSpec from a state-to-value record or tabular data.\n *\n * @example Record map (simplest):\n * ```ts\n * tileMap({ CA: 4.4, TX: 17.6, NY: 4.5 })\n * ```\n *\n * @example Tabular data with encoding:\n * ```ts\n * tileMap(data, { encoding: { state: { field: 'code' }, value: { field: 'rate' } } })\n * ```\n */\nexport function tileMap(\n data: Record<string, number | string | null> | DataRow[],\n options?: TileMapBuilderOptions,\n): TileMapSpec {\n return {\n type: 'tilemap' as const,\n data,\n ...(options?.encoding && { encoding: options.encoding }),\n ...(options?.palette && { palette: options.palette }),\n ...(options?.colors && { colors: options.colors }),\n ...(options?.chrome && { chrome: options.chrome }),\n ...(options?.legend && { legend: options.legend }),\n ...(options?.theme && { theme: options.theme }),\n ...(options?.darkMode && { darkMode: options.darkMode }),\n ...(options?.watermark !== undefined && { watermark: options.watermark }),\n ...(options?.animation && { animation: options.animation }),\n ...(options?.valueFormat && { valueFormat: options.valueFormat }),\n };\n}\n","/**\n * Deep merge for applying partial patches onto a spec-shaped object.\n *\n * Used by the scrollytelling story layer (`vanilla/story`) to apply\n * cumulative deep-partial spec patches onto a base spec. Arrays REPLACE\n * rather than merge element-by-element: predictable behavior for things\n * like `annotations` or `data`, where index-wise merging would silently\n * produce nonsense. Plain objects merge key-by-key, recursively.\n */\n// biome-ignore lint/suspicious/noExplicitAny: recursive object merge requires dynamic typing\nexport function deepMergeSpec(target: any, source: any): any {\n if (source === undefined) return target;\n if (source === null) return source;\n if (\n typeof source !== 'object' ||\n Array.isArray(source) ||\n typeof target !== 'object' ||\n target === null ||\n Array.isArray(target)\n ) {\n return source;\n }\n\n const result = { ...target };\n for (const key of Object.keys(source)) {\n result[key] = deepMergeSpec(target[key], source[key]);\n }\n return result;\n}\n"],"mappings":"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ACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,GAAG;AAAA,EACH,KAAK;AAAA,EACL,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,UAAK;AAAA,EACL,UAAK;AAAA,EACL,QAAK;AAAA,EACL,UAAK;AAAA,EACL,UAAK;AAAA,EACL,QAAK;AAAA,E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","names":["rgb","rgb","rgb","relativeLuminance","mix","words","lines","current","words","lines","current"]}
|