The definitive field-by-field reference for every type in VizSpec. Cross-referenced against the TypeScript source in @opendata-ai/openchart-core/src/types/.
All types are importable from @opendata-ai/openchart-core or from the convenience re-exports in @opendata-ai/openchart-react, @opendata-ai/openchart-vue, @opendata-ai/openchart-svelte, and @opendata-ai/openchart-engine.
- VizSpec (top-level union type)
- ChartSpec (line, area, bar, column, pie, donut, dot, scatter)
- Mark properties (fill, gradient, point, interpolate, opacity)
- LayerSpec (overlay multiple chart types)
- Encoding (x, y, color, size, detail channels)
- Faceting (facet, row, column channels; resolve)
- Transforms (filter, bin, aggregate, joinaggregate, calculate, timeUnit, fold, window, density)
- Annotations (refline, text, range)
- Labels (density, format, position)
- Chrome (title, subtitle, source, byline, footer)
- ThemeConfig (colors, fonts, spacing, editorial rule)
- DarkMode (auto, force, off)
- TableSpec (data tables: density, striping, sort, totals)
- ColumnConfig (column definitions and visual types)
- Event handlers (chart and table interaction callbacks)
- GraphSpec (network/relationship visualizations)
- SankeySpec (flow diagrams)
- GeoMapSpec (choropleth and symbol maps)
- TileMapSpec (US state tile grid maps)
- Spec builder functions (lineChart, barChart, etc.)
- Validation (validateSpec, error codes)
The top-level type is a discriminated union on the type field:
type VizSpec =
| ChartSpec
| LayerSpec
| TableSpec
| GraphSpec
| SankeySpec
| TileMapSpec
| GeoMapSpec
| BarListSpec;Use type to select which spec shape you're building:
- Chart types (
line,area,bar,column,pie,donut,dot,scatter) produce aChartSpec tableproduces aTableSpecgraphproduces aGraphSpecsankeyproduces aSankeySpectilemapproduces aTileMapSpecmapproduces aGeoMapSpecbarlistproduces aBarListSpec
Type guards are available: isChartSpec(spec), isTableSpec(spec), isGraphSpec(spec), isSankeySpec(spec), isTileMapSpec(spec), isGeoMapSpec(spec), isBarListSpec(spec).
The primary input for standard chart types. Source: core/src/types/spec.ts.
| Field | Type | Default | Description |
|---|---|---|---|
mark |
MarkType | MarkDef |
(required) | The mark: 'bar', 'line', 'area', 'point', 'circle', 'arc', 'text', 'rule', 'tick', 'rect', 'lollipop', 'beeswarm', 'histogram', 'density', 'range', 'waffle', 'calendar', 'parliament'. See Mark properties for the object form. |
data |
DataRow[] |
(required) | Array of data rows. Each row is a Record<string, unknown>. Must be non-empty. |
encoding |
Encoding |
(required) | Maps data fields to visual channels. See Encoding. |
chrome |
Chrome |
undefined |
Editorial text: title, subtitle, source, byline, footer. See Chrome. |
chromeLayout |
'subtract' | 'grow' |
note-band growth | How chrome text interacts with the container height budget. See Chrome layout. |
annotations |
Annotation[] |
undefined |
Text callouts, highlighted ranges, reference lines. See Annotations. |
labels |
LabelConfig |
undefined |
Data label display controls. See Labels. |
responsive |
boolean | ResponsiveConfig |
true |
Layout adaptations at compact sizes; mount autoResize controls ResizeObserver. |
theme |
ThemeConfig |
undefined |
Theme overrides. Deep-merged onto the default theme. See ThemeConfig. |
watermark |
boolean |
true |
Whether to show the tryOpenData.ai watermark. Mount/compile options take precedence over spec and active breakpoint values. Defaults to true; auto-hides when the container is under 200px tall unless explicitly set. |
animation |
AnimationSpec |
undefined |
Animation configuration. true enables entrance + update/exit animations. See Animation. |
description |
string |
undefined |
Alt text for the chart (Vega-Lite aligned). Sugar for a11y.description. Auto-generated when absent. See the accessibility guide. |
a11y |
A11yConfig |
undefined |
Accessibility overrides: description (custom alt text, wins over top-level description) and hidden (hide from assistive technology). |
type DataRow = Record<string, unknown>;A plain object with string keys. Values can be numbers, strings, dates, nulls, arrays (for sparklines), or booleans. The engine inspects values at runtime to validate encoding types.
MarkDef is a discriminated union: each object accepts only options for its mark type. TypeScript, JSON Schema, and runtime validation enforce the same contract before sugar expansion. Extend it with an intersection type rather than an interface.
The mark field on ChartSpec accepts either a string ('line') or an object with additional mark configuration. LayerSpec children are ChartSpecs, so they use mark too.
// String shorthand
{ mark: 'area', data: [...], encoding: {...} }
// Object form with mark properties
{
mark: { type: 'area', point: true, fill: { gradient: 'linear', ... } },
data: [...],
encoding: {...}
}| Field | Type | Default | Applies to | Description |
|---|---|---|---|---|
type |
MarkType |
(required) | all | Mark type: 'bar', 'line', 'area', 'point', 'arc', 'text', etc. |
point |
boolean | 'transparent' |
false |
line, area | Show point markers at data positions. true = filled circles. |
interpolate |
string |
'linear' |
line, area | Curve: 'linear', 'monotone', 'step', 'step-before', 'step-after', 'basis', 'cardinal', 'natural'. |
orient |
'horizontal' | 'vertical' |
auto | bar | Explicit orientation override. |
innerRadius |
number |
0 |
arc | Inner radius. >0 produces a donut. |
outerRadius |
number |
auto | arc | Outer radius. |
cornerRadius |
number | 'pill' |
0 |
bar, arc | Corner rounding in pixels. 'pill' sets rx to half the bar thickness. |
centerLabel |
string | { text, subtitle? } |
undefined |
arc | Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful with an inner radius. |
filled |
boolean |
true |
all | Whether the mark is filled vs stroked only. |
opacity |
number |
1 |
all | Overall mark opacity (0-1). |
fillOpacity |
number |
varies | histogram, density, area, bar | Fill opacity independent of stroke. Defaults to 0.55 on overlapping histogram groups and 0.4 on grouped density curves. On an area it replaces the auto gradient with a flat translucent fill. |
fill |
string | GradientDef |
theme color | all | Fill color or gradient. See Gradients. |
stroke |
string |
undefined |
all | Stroke color. |
strokeWidth |
number |
varies | all | Stroke width in pixels. |
tooltip |
boolean | null |
true |
all | Tooltip behavior. null disables tooltips. |
clip |
boolean |
false |
all | Clip marks to the chart area. |
fillPattern |
'auto' | 'none' |
'none' |
bar, area, arc | 'auto' layers a per-series SVG pattern (hatch, dots, crosshatch, vertical) over the series color. See the accessibility guide. |
style |
'dumbbell' | 'arrow' | 'bar' |
'dumbbell' |
range | Range mark visual: dot-connector-dot, line with arrowhead at the end, or a plain floating bar. |
colorByDirection |
boolean |
false |
range | Color range marks by direction of change: increases use the theme positive color, decreases negative. A field-based color encoding takes precedence. |
units |
number |
100 |
waffle | Total cells in the grid. Categories normalize to this via largest-remainder rounding. |
columns |
number |
10 |
waffle | Columns per grid. Rows derive from units / columns. |
other |
number | { threshold, label? } |
undefined |
waffle | Opt-in "Other" bucketing: categories whose share of the total falls below threshold (a fraction, so 0.05 is 5%) merge into one trailing category. Off by default -- bucketing is an editorial decision. Same shape as SankeySpec.other. |
weekStart |
'monday' | 'sunday' |
'monday' |
calendar | Weekday on the top row of each year band. |
cellRadius |
number |
2 |
calendar | Corner radius in pixels for day cells. |
shape |
'hemicycle' |
'hemicycle' |
parliament | Seat layout shape (currently the only shape). |
binCount |
number |
20 |
histogram | Number of bins. Desugars to x.bin.maxbins. |
normalize |
boolean |
false |
histogram | Plot each group's share of its own total instead of a raw count, so differently sized groups are comparable. Formats the y axis as a percentage. |
bandwidth |
number |
Silverman | density | Gaussian kernel bandwidth. Omitted uses Silverman's rule of thumb. |
cumulative |
boolean |
false |
density | Emit a cumulative distribution (CDF) instead of a density. |
steps |
number |
200 |
density | Evaluation points along the curve. |
seatRadius |
number | 'auto' |
'auto' |
parliament | Seat dot radius in pixels. 'auto' sizes dots to fill the rings for the seat count. |
majorityLine |
boolean |
true |
parliament | Draw the majority-threshold line and its "N to win" label. |
For pre-binned histograms, an explicit encoding.tooltip replaces the default
numeric range title as well as the tooltip rows. To describe pooled end bins,
include a nominal field such as { field: 'rangeLabel', type: 'nominal', title: 'Gift size' } with values like Below $1,000 and $100,000 or more. Numeric
x/x2 still position the bars; a finite display boundary need not be presented
as the actual upper bound. Without explicit tooltip fields, the bin-edge range
remains the default title.
The fill property accepts a GradientDef object for linear or radial gradients. Source: core/src/types/spec.ts.
// Linear gradient (top-to-bottom opacity fade)
fill: {
gradient: 'linear',
x1: 0, y1: 1, // start (bottom)
x2: 0, y2: 0, // end (top)
stops: [
{ offset: 0, color: '#38bdf8', opacity: 0 },
{ offset: 1, color: '#38bdf8', opacity: 1 },
],
}| Field | Type | Default | Description |
|---|---|---|---|
gradient |
'linear' | 'radial' |
(required) | Gradient type. |
stops |
GradientStop[] |
(required) | Color stops with offset (0-1), color, and optional opacity. |
x1, y1 |
number |
0, 0 |
Start point in [0,1] normalized space. |
x2, y2 |
number |
0, 1 |
End point. Default is top-to-bottom. |
Area chart fill behavior: Multi-series area charts default to stacked mode (stack: 'zero'), Vega-Lite aligned — a higher-opacity per-layer gradient (top stop ~0.65 opacity) since each layer sits on solid ground. Single-series area charts apply a default fillOpacity of 0.15. Opting out with stack: null (or false) switches to overlap mode: a translucent per-series gradient fill (top stop ~0.04 opacity, fading to 0) layered on a shared baseline, comparison-first rather than composition-first. The gradient stop opacity values multiply with the per-series default, so a stop at opacity: 1 with the overlap mode produces an effective opacity of ~0.04. Design gradient stops accordingly, or use a LayerSpec with separate line and area marks for full control.
Overlay multiple chart types on shared scales. Source: core/src/types/spec.ts.
| Field | Type | Default | Description |
|---|---|---|---|
layer |
(ChartSpec | LayerSpec)[] |
(required) | Array of child chart specs. Nesting is supported. |
data |
DataRow[] |
undefined |
Shared data inherited by children that don't define their own. |
encoding |
Encoding |
undefined |
Shared encoding inherited by children. Child channels override parent channels. |
transform |
Transform[] |
undefined |
Shared transforms. Parent transforms run before child transforms. |
chrome |
Chrome |
undefined |
Editorial text (title, subtitle, source, etc.) for the layered view. |
annotations |
Annotation[] |
undefined |
Annotations on the layered view. |
labels |
LabelConfig |
undefined |
Label display configuration. |
legend |
LegendConfig |
undefined |
Legend display configuration. |
responsive |
boolean | ResponsiveConfig |
undefined |
Same as ChartSpec.responsive. When omitted, the first leaf's value applies. |
theme |
ThemeConfig |
undefined |
Theme overrides. |
watermark |
boolean |
true |
Whether to show the watermark. Defaults to true; auto-hides when the container is under 200px tall unless explicitly set. |
resolve |
ResolveConfig |
undefined |
Resolution strategy for shared vs. independent scales/axes/legends. |
hiddenSeries |
string[] |
undefined |
Series names to hide from rendering. |
animation |
AnimationSpec |
undefined |
Animation configuration. true enables entrance + update/exit animations. See Animation. |
Scale behavior: All layers share scales by default. The engine unions data from all layers to compute a single scale domain, so marks from different layers are positioned on the same coordinate system.
Encoding inheritance: Parent encoding channels are merged into child layers. A child channel overrides the parent on the same key, so you can share x across layers while varying y and mark.
const spec = {
layer: [
{
mark: 'area',
data: revenueData,
encoding: {
x: { field: 'date', type: 'temporal' },
y: { field: 'revenue', type: 'quantitative' },
},
},
{
mark: 'line',
data: targetData,
encoding: {
x: { field: 'date', type: 'temporal' },
y: { field: 'target', type: 'quantitative' },
},
},
],
chrome: { title: 'Revenue vs target' },
};Maps data fields to visual channels. Source: core/src/types/spec.ts.
interface Encoding {
x?: EncodingChannel;
y?: EncodingChannel;
color?: EncodingChannel;
size?: EncodingChannel;
theta?: EncodingChannel; // arc/waffle/parliament quantitative value
detail?: EncodingChannel;
facet?: FacetChannel; // small multiples -- see Faceting
row?: FacetChannel; // vertical facet stack -- see Faceting
column?: FacetChannel; // horizontal facet row -- see Faceting
}Which channels are required depends on the chart type. See Encoding by chart type.
| Field | Type | Default | Description |
|---|---|---|---|
field |
string |
(required) | Data field name. Must match a key in the data rows. |
type |
FieldType |
(required) | How to interpret values: 'quantitative', 'temporal', 'nominal', 'ordinal'. |
aggregate |
AggregateOp |
undefined |
Aggregate applied before encoding: 'count', 'sum', 'mean', 'median', 'min', 'max'. |
axis |
AxisConfig |
undefined |
Axis configuration. Only relevant for x and y channels. |
scale |
ScaleConfig |
undefined |
Scale configuration (domain, type, nice, zero). |
stack |
boolean | 'zero' | 'normalize' | 'center' | null |
'zero' for bar/column/area (Vega-Lite aligned) |
Stacking behavior for quantitative channels. null/false disables stacking (grouped/dodged bars, or overlap gradients for area). 'normalize' for 100% stacked. 'center' for streamgraph. |
| Value | Meaning | Scale created |
|---|---|---|
quantitative |
Continuous numbers | Linear (or log) |
temporal |
Dates and times | Time |
nominal |
Unordered categories | Band/ordinal |
ordinal |
Ordered categories | Band/ordinal |
'count' | 'sum' | 'mean' | 'median' | 'min' | 'max'
Applied to the field values before encoding. Useful when your data has multiple rows per category and you want a single value per mark.
| Field | Type | Default | Description |
|---|---|---|---|
label |
string |
field name | Axis label text displayed along the axis. |
format |
string |
auto | d3-format string for tick labels, e.g. ",.0f" for comma-separated integers. Also accepts "ordinal" for rank ticks (1st, 2nd, 3rd). |
tickCount |
number |
auto | Override the number of ticks. Engine picks a sensible default if omitted. |
grid |
boolean |
true for y-axis |
Whether to show gridlines for this axis. |
| Field | Type | Default | Description |
|---|---|---|---|
domain |
[number, number] or string[] |
auto from data | Explicit domain override. |
type |
'linear' | 'log' | 'time' | 'band' | 'point' | 'ordinal' |
inferred from FieldType | Scale type override. |
nice |
boolean |
true |
Round domain to clean tick values. |
zero |
boolean |
true (quantitative) |
Whether the domain should include zero. |
range |
string[] (color channels) |
palette-derived | Explicit output values, e.g. exact colors. Wins over scheme. |
scheme |
string |
palette default | Named color ramp (Vega-Lite aligned): sequential blue/green/orange/purple/teal, diverging redBlue/brownTeal; common VL aliases (blues, category10, ...) accepted. |
scheme is only meaningful where the compile path reads it: chart color
channels and quantitative map color (maps accept the sequential names only).
Sankey, tilemap, bar list, graph, and categorical map channels never read it,
and validation rejects it there with the mechanism that family actually uses
(theme.colors.categorical for sankey, top-level palette for tilemap,
scale.range for graph and categorical maps). See the
v8 migration guide.
The engine validates encoding channels at runtime using MARK_ENCODING_RULES. Source: core/src/types/encoding.ts.
Legend: (req) = required, (opt) = optional, -- = not applicable
| Chart Type | x | y | color | size | detail | Notes |
|---|---|---|---|---|---|---|
line |
temporal, ordinal (req) | quantitative (req) | nominal, ordinal (opt) | quantitative (opt) | nominal (opt) | color splits into multi-series with auto legend |
area |
temporal, ordinal (req) | quantitative (req) | nominal, ordinal (opt) | quantitative (opt) | nominal (opt) | Same encoding as line, filled region |
bar |
quantitative (req) | nominal, ordinal (req) | nominal, ordinal (opt) | quantitative (opt) | nominal (opt) | Horizontal bars. x = values, y = categories |
column |
nominal, ordinal, temporal (req) | quantitative (req) | nominal, ordinal (opt) | quantitative (opt) | nominal (opt) | Vertical bars. x = categories, y = values |
pie |
-- (opt, unused) | -- (use theta) | nominal, ordinal (req) | quantitative (opt) | nominal (opt) | color = slice categories, theta = slice values |
donut |
-- (opt, unused) | -- (use theta) | nominal, ordinal (req) | quantitative (opt) | nominal (opt) | theta = slice values; same as pie with inner radius |
dot |
quantitative (req) | nominal, ordinal (req) | nominal, ordinal (opt) | quantitative (opt) | nominal (opt) | Dot plot. x = values, y = categories |
scatter |
quantitative (req) | quantitative (req) | nominal, ordinal (opt) | quantitative (opt) | nominal (opt) | Both axes quantitative. size creates bubble chart |
beeswarm |
quantitative or nominal (opt) | quantitative or nominal (opt) | nominal, ordinal, quantitative (opt) | quantitative (opt) | nominal (opt) | One position is the quantitative value axis; the other (nominal/ordinal) splits into grouped lanes |
range |
quantitative or nominal (req) | quantitative or nominal (req) | nominal, ordinal (opt) | -- | nominal (opt) | Dumbbell/arrow/range-bar. x+x2 (horizontal) or y+y2 (vertical); orientation-dependent second endpoint |
waffle |
-- (opt, unused) | -- (use theta) | nominal, ordinal (req) | -- | nominal (opt) | Unit grid. theta = share, color = category |
parliament |
-- (opt, unused) | -- (use theta) | nominal, ordinal (req) | -- | nominal (opt) | Hemicycle seats. theta = seat count, color = party |
histogram |
quantitative (req) | quantitative (opt) | nominal, ordinal (opt) | quantitative (opt) | nominal (opt) | Raw values on x; y defaults to the per-bin count. Color groups overlap rather than dodge |
density |
quantitative (req) | -- (computed) | nominal, ordinal (opt) | -- | nominal (opt) | KDE over raw x values; y is the estimated density |
calendar |
temporal (req) | -- (not allowed) | quantitative (req) | -- | nominal (opt) | Calendar heatmap. x = daily date, color = per-day value; owns its own geometry |
x channel: Horizontal position. Temporal/ordinal for time series (line, area), nominal/ordinal for categories (bar, column), quantitative for values (bar, dot, scatter).
y channel: Vertical position. Quantitative values for most types. Nominal/ordinal categories for bar and dot (horizontal layout).
theta channel: Quantitative part-to-whole values for arc, waffle, and parliament. Their former authored y alias is rejected; use theta.
color channel: Series differentiation. Assigns colors from the categorical palette. Required for pie/donut (defines slices). Optional for all others (creates multi-series with legend).
size channel: Mark size. Creates bubble charts when applied to scatter. Maps a quantitative field to the dot radius in pixels, area-proportionally (sqrt curve); the default range is [3, 30] and scale.range overrides it in radius px. A constant lives at mark.size, also a radius (default 5, typical 2-12). Note this differs from Vega-Lite, where point size is an area in px² — a VL size of 110 is mark.size: 6 here (r = √(area/π)); validation rejects radii above 50 with the conversion.
detail channel: Grouping without visual encoding. Splits data into groups (like color does) but doesn't assign different colors. Useful when you want separate lines per group but all the same color.
Small multiples: partition data into a grid of panels, one panel per unique value of a categorical field. Each panel compiles as a regular chart with a header showing its facet value. Source: core/src/types/spec.ts (FacetChannel), engine/src/compile.ts (compileFaceted).
Three encoding channels drive faceting. They are mutually exclusive: row and column together fail validation (cross-product faceting is not yet supported), as does combining either with facet.
| Channel | Layout | Default scale sharing |
|---|---|---|
encoding.facet |
Wrap grid (columns wide) |
x and y both shared |
encoding.row |
Vertical stack, one column | x shared, y independent per panel |
encoding.column |
Single row of side-by-side panels | y shared, x independent per panel |
Unlike positional channels, a facet channel carries no scale or axis config -- it produces a grid, not a visual encoding.
| Field | Type | Default | Description |
|---|---|---|---|
field |
string |
(required) | Data field to partition by. Each unique value produces one panel. Must exist in the data. |
type |
'nominal' | 'ordinal' |
(required) | Must be categorical. Other field types fail validation. |
columns |
number |
auto | Wrap grid width (facet channel only; row forces 1 column, column uses one panel per value). Must be a positive integer. Auto picks min(ceil(sqrt(n)), what fits at the 200px panel minimum). |
sort |
'ascending' | 'descending' | null |
'ascending' |
Panel order. null keeps data order (first appearance). |
The top-level resolve field on the chart spec overrides the per-direction scale defaults. Only meaningful on faceted specs.
interface ResolveConfig {
scale?: Partial<Record<'x' | 'y' | 'color' | 'size', 'shared' | 'independent'>>;
axis?: Partial<Record<'x' | 'y', 'shared' | 'independent'>>;
legend?: Partial<Record<'color' | 'size', 'shared' | 'independent'>>;
}The facet compile path currently honors resolve.scale.x and resolve.scale.y; the other keys exist on the type (Vega-Lite aligned) but are not yet consumed.
Shared scales compute one domain from the full dataset so panels are directly comparable. Independent scales fit each panel's own domain so each panel fills its frame. When a scale is shared, redundant tick labels are stripped: y-axis labels render only on the leftmost column, and row-faceted panels show x-axis labels only on the bottom panel.
- Panels have a 200px minimum width. The grid drops columns responsively until panels fit, down to a single column.
- Panels have a 100px minimum height. If the grid would fall below it, the figure grows taller (once) to fit.
- Chrome, legend, and annotations are figure-level. Annotations resolve into every panel and thin per panel; demoted labels pool into one figure-level footnote list.
const spec: ChartSpec = {
mark: "line",
data: gdpGrowthRows, // rows carry a country field
encoding: {
x: { field: "date", type: "temporal" },
y: { field: "gdp", type: "quantitative" },
facet: { field: "country", type: "nominal", columns: 3 },
},
resolve: { scale: { y: "independent" } },
chrome: { title: "GDP growth by country" },
};Note: Vega-Lite's top-level facet operator ({ facet, spec }) is not supported. Faceting is always expressed through the encoding channels above.
Editorial text elements. Source: core/src/types/spec.ts.
interface Chrome {
eyebrow?: string | ChromeText;
title?: string | ChromeText;
subtitle?: string | ChromeText;
source?: string | ChromeText;
byline?: string | ChromeText;
footer?: string | ChromeText;
brand?: string | ChromeText;
}Each field accepts either a plain string or a ChromeText object for style overrides.
| Field | Position | Default style |
|---|---|---|
eyebrow |
Above title | 11px, medium (500), uppercase, theme accent — leading accent dot |
title |
Top, above chart | 26px, semibold (600), #09090b |
subtitle |
Below title | 14px, normal (400), #71717a |
source |
Below chart area | 12px, normal (400), #71717a |
byline |
Below source | 12px, normal (400), #71717a |
footer |
Below byline | 12px, normal (400), #71717a |
brand |
Right-aligned on the source row | 12px, suppresses the default watermark |
KPI metric row is a separate top-level field on chart specs, not part of Chrome. Use metrics: Metric[] on the chart spec for a horizontal row of label+value cells rendered between the subtitle and the chart area. Each cell can carry a delta and a secondary value. See Metric in core/src/types/spec.ts for the full shape.
When you need to override the default chrome styling:
interface ChromeText {
text: string;
style?: ChromeTextStyle;
}
interface ChromeTextStyle {
fontSize?: number; // Pixels
fontWeight?: number; // 400 = normal, 600 = semibold, 700 = bold
fontFamily?: string; // CSS font family
color?: string; // CSS color string
maxLines?: number; // Cap on wrapped line count
}Example:
chrome: {
title: { text: 'Revenue by quarter', style: { fontSize: 28, fontWeight: 700 } },
subtitle: 'FY2024 results',
source: 'Source: Internal reporting',
}Only the style properties you provide are overridden. Omitted properties use the theme defaults.
Caps how many lines the text wraps to. Extra lines are dropped and the last kept line is truncated with an ellipsis. This bounds the chrome element's contribution to layout height at any container width, which is useful when a long title would otherwise wrap to many lines on a narrow viewport. It applies whether the font size comes from a theme default or an explicit fontSize override.
chrome: {
title: { text: 'A long headline that would wrap on a phone', style: { maxLines: 2 } },
}maxLines caps height only. A single unbreakable word wider than the available width still overflows horizontally, same as without it.
The top-level chromeLayout property controls how chrome text height interacts with the container height budget.
| Value | Behavior |
|---|---|
| omitted | Notes and the series summary grow the figure; title, subtitle, source, and other chrome subtract from the container height budget. |
'subtract' |
Keep the exact outer height; all chrome and notes subtract from it, shrinking the plot. |
'grow' |
The container height is treated as the plot budget. The rendered SVG grows taller by the measured chrome height, so the plot keeps its full budget (minus axis and padding overhead) no matter how many lines the title wraps to. |
Reach for 'grow' on fixed-height article or blog charts where a long title on a narrow phone must not compress the plot.
{
mark: 'line',
data: [...],
encoding: {...},
chromeLayout: 'grow',
chrome: { title: 'A long headline that may wrap to several lines on a phone' },
}chromeLayout: 'grow' supports faceted charts and layers; engine-added height is counted once. Read layout.dimensions.height for the final height and layout.chrome.grownHeight for the added height. The default note-growth policy is internal: 'notes' is not a public spec value. Bar list, Sankey, map, and tilemap also honor the public spec/compile policies.
transform is an ordered array run against the data before scales and layout. Each entry produces a new array that feeds the next. Source: engine/src/transforms/.
| Transform | Shape | What it does |
|---|---|---|
filter |
{ filter: Predicate } |
Keeps rows matching a predicate (field/op/value, or a relative time reference). |
bin |
{ bin: true | BinParams, field, as } |
Buckets a quantitative field. BinParams is { maxbins, step, nice, extent }. as takes a string for the bin start, or [start, end] for both edges. |
calculate |
{ calculate: { op, field, field2? | value? }, as } |
Arithmetic between two fields or a field and a constant. |
timeUnit |
{ timeUnit, field, as } |
Truncates a temporal field to a unit (year, month, week, day...). |
aggregate |
{ aggregate: [{ op, field, as }], groupby } |
Collapses each group to one row. Ops: count, sum, mean, median, min, max, variance, stdev, distinct, q1, q3. |
joinaggregate |
{ joinaggregate: [{ op, field, as }], groupby } |
Same ops, but keeps every row and writes the group's summary onto each. This is what expresses "this row's share of its group's total". |
fold |
{ fold: [fields], as } |
Wide to long: one row per (row, field) pair. |
window |
{ window: [{ op, field, as }], groupby, sort, frame } |
Running/ordered computations over a moving frame. |
density |
{ density, groupby?, bandwidth?, extent?, cumulative?, counts?, steps?, as? } |
Gaussian kernel density estimate. Replaces the rows with one row per evaluation point per group, defaulting to fields value and density. Groups share one extent so the curves are comparable. |
A histogram is a bin plus a count aggregate, which is what mark: 'histogram' desugars to:
transform: [
{ bin: { maxbins: 24 }, field: "amount", as: ["bin_amount", "bin_amount_end"] },
{ aggregate: [{ op: "count", field: "amount", as: "__count" }], groupby: ["bin_amount", "bin_amount_end"] },
];Carrying both bin edges matters: the end edge goes to x2, which is what puts the bars on a continuous axis instead of one equal-width slot per bin.
Data-positioned overlays. Source: core/src/types/spec.ts.
Use unique annotation id values for durable selection and editing. Notes preserve complete primary/subtitle text and rich spans; their numbers follow source text-annotation order before filtering, so gaps are intentional. Notes and markers support linked focus and click/tap emphasis.
Three annotation types are available, discriminated by the type field:
A callout label positioned at a data coordinate.
| Field | Type | Default | Description |
|---|---|---|---|
type |
'text' |
(required) | Discriminant. |
x |
string | number |
(required) | X-axis data value or position. |
y |
string | number |
(required) | Y-axis data value or position. |
text |
string |
(required) | The annotation text. Supports \n for line breaks and **bold** spans. |
subtitle |
string |
undefined |
Optional muted second-tone text rendered below text. Use for supporting context (methodology, source, etc.). Supports **bold** too. |
maxWidth |
number | null |
min(240, plotWidth - 2 * padding), at least 1px |
Positive display wrapping width in pixels; null disables wrapping. |
subtitleFontSize |
number |
round(fontSize * 12 / 13) |
Optional positive subtitle size in pixels. |
dot |
boolean | AnnotationDot |
see below | Open-ring marker on the data point. Appears by default on non-arrowed connectors. false for none, an object to restyle. |
fontSize |
number |
13 |
Font size override in pixels. |
priority |
number |
source order | Lower values get placement and compact-inline preference; ties retain source order. |
responsive |
boolean |
true |
false pins the annotation inline and exempts it from thinning/count limits. |
fontWeight |
number |
400 |
Font weight override. |
offset |
AnnotationOffset |
undefined |
Pixel offset from computed position. { dx?: number, dy?: number }. |
anchor |
AnnotationAnchor |
'auto' |
Label placement direction: 'top', 'bottom', 'left', 'right', 'auto'. |
connector |
boolean | ConnectorType | ConnectorConfig |
true |
'straight', 'curve', or 'drop-line'; { type, arrow?, bend? } for explicit control; false disables it. |
connectorOffset |
{ from?, to? } |
undefined |
Per-endpoint pixel offsets for the connector line. |
background |
string | true |
undefined |
Background plate behind the text, for readability over chart lines. true follows the theme surface. |
fill |
string |
theme annotationFill |
Fill color. |
stroke |
string |
theme annotationText |
Stroke color. Also overrides the connector and marker stroke. |
opacity |
number |
1 |
Opacity (0 to 1). |
zIndex |
number |
0 |
Render ordering. Higher values render on top. |
Curve connectors accept optional bend: -1..1; omitted or zero preserves the
existing automatic curve. Other connector types ignore bend with a warning.
An authored offset object pins placement even when both values are zero.
\n starts a new line. Each line renders as its own <tspan>. Multi-line blocks are left-aligned by default, or right-aligned when anchor: 'left' puts the block to the left of the point, so the block's facing edge lines up with the leader.
**bold** marks an inline bold span in text and in subtitle. Emphasis belongs on the key phrase, not the whole block:
{ type: 'text', x: '2022-06', y: 8.5, text: 'Inflation peaked at **8.5%**' }Only matched pairs turn bold. An unmatched ** (and an empty ****) renders literally, so specs with stray asterisks aren't mangled. Bold spans are measured at weight 700, so bounds, collision nudging, and connector exits all account for them.
Primary text and subtitles default to weight 400. Set fontWeight: 700 for a bold primary line or use **bold** for selective emphasis; adding a subtitle does not change the primary weight.
The connector leaves the text block (primary text plus subtitle, treated as one box) on the side facing the data point, with a 6px standoff gap, and stops short of the marker at the other end. A curved connector is shaped to the data point and then pulled back along its end tangent, so the tail still aims at the marker. Two voices:
- Emphasis. An explicitly arrowed connector (
{ type: 'curve', arrow: true }) is drawn in the label's text ink with a small open-V arrowhead, so the gesture reads as part of the sentence. - Quiet. A non-arrowed curve, straight leader, or
'drop-line'is a gray hairline. It points without competing with the words.
Connectors are dropped when they'd be noise rather than signal: a leader shorter than 8px, or one whose data point sits inside the text block. The marker still renders. A default annotation sits far enough off its point (28px) to draw a real leader without any offset authoring; you only lose the line by pulling the label back onto its own marker.
| Field | Type | Default | Description |
|---|---|---|---|
radius |
number |
4 |
Circle radius in pixels. |
fill |
string |
theme annotationMarkerFill (neutral surface by default) |
Interior fill (the "open ring"). |
stroke |
string |
the connector's stroke | Ring color. |
strokeWidth |
number |
1.5 |
Ring width in pixels. |
The marker sits on the data point itself, not on the pulled-back connector tip. You get one by default when a connector is enabled and carries no arrowhead, including drop-lines. An arrowed connector gets no marker unless you ask for one, since the arrowhead already marks the spot. dot: false is always bare; an explicit dot always wins. A default dot that lands on a line's endpoint marker takes the marker's fill and stroke, so the two draw as one ring.
A highlighted band or rectangle.
| Field | Type | Default | Description |
|---|---|---|---|
type |
'range' |
(required) | Discriminant. |
x1 |
string | number |
undefined |
Start of the range on x-axis. |
x2 |
string | number |
undefined |
End of the range on x-axis. |
y1 |
string | number |
undefined |
Start of the range on y-axis. |
y2 |
string | number |
undefined |
End of the range on y-axis. |
extendToEdges |
boolean |
true |
For ordinal band/point scales, whether the range extends from the data-point center to the band/step edge. Set false to anchor at data-point centers. No effect on linear/time scales. |
label |
string |
undefined |
Range label text. |
labelOffset |
AnnotationOffset |
undefined |
Pixel offset for the label. { dx?: number, dy?: number }. |
labelAnchor |
AnnotationAnchor |
'auto' |
Label placement direction. |
fontSize |
number |
theme small (12px) |
Font size override for the range label (px). |
fontWeight |
number |
500 |
Font weight override for the range label. |
fill |
string |
theme annotationRangeFill (#f0c040) |
Fill color. |
stroke |
string |
undefined |
Stroke color. |
opacity |
number |
1 |
Opacity (0 to 1). |
zIndex |
number |
0 |
Render ordering. |
Range behavior depends on which bounds are provided:
x1/x2only: vertical band (full chart height)y1/y2only: horizontal band (full chart width)- All four: rectangle
A horizontal or vertical reference line.
| Field | Type | Default | Description |
|---|---|---|---|
type |
'refline' |
(required) | Discriminant. |
x |
string | number |
undefined |
X-axis value for a vertical line. |
y |
string | number |
undefined |
Y-axis value for a horizontal line. |
label |
string |
undefined |
Line label text. |
style |
'solid' | 'dashed' | 'dotted' |
'solid' |
Line style. |
strokeWidth |
number |
1 |
Line width in pixels. |
labelOffset |
AnnotationOffset |
undefined |
Pixel offset for the label. |
labelAnchor |
AnnotationAnchor |
'auto' |
Label placement direction. |
labelPlacement |
'auto' | 'start' | 'end' | 'outside' |
'auto' unless labelAnchor is authored |
Screen-space end selection, automatic containment, or a reserved outside gutter/lane. labelAnchor selects the side; an authored offset pins the base. |
fontSize |
number |
theme small (12px) |
Font size override for the label (px). |
fontWeight |
number |
400 |
Font weight override for the label. |
fill |
string |
undefined |
Fill color. |
stroke |
string |
theme annotationRule |
Stroke/line color. |
opacity |
number |
1 |
Opacity (0 to 1). |
zIndex |
number |
0 |
Render ordering. |
Provide x for a vertical line or y for a horizontal line. Both can be set for a crosshair.
With labelPlacement: 'outside', horizontal lines reserve a right lane as wide as the widest wrapped label (capped at 160px or 25% of the width). Vertical lines share a lane above the plot: labels whose extents don't overlap pack into one row, and whole rows are admitted while the lane stays within 25% of the height. A label whose row doesn't fit falls back to automatic placement inside the plot with a warning. The lane shrinks before the title or subtitle does.
annotations: [
// Horizontal threshold line
{ type: "refline", y: 100, label: "Target", style: "dashed", stroke: "#e45" },
// Highlighted time range
{
type: "range",
x1: "2023-06-01",
x2: "2023-09-01",
label: "Q3",
opacity: 0.08,
},
// Text callout with fine-tuned positioning
{
type: "text",
x: "2023-07-15",
y: 142,
text: "All-time high",
anchor: "top",
offset: { dy: -8 },
connector: true,
},
];Data label display configuration. Source: core/src/types/spec.ts.
The semantic format percent displays nonzero magnitudes below 0.1 percent as
<0.1% (or >−0.1% for negative values), while exact zero remains 0%. This
applies to labels, tooltip fields, and table columns using the semantic keyword,
and to the exported formatPercent utility. Fraction-versus-pre-scaled detection
is unchanged. Explicit d3 formats retain their requested precision: use .4%
for four decimal places or .1% for ordinary rounding. Formatting uses the
library's existing default d3 locale; runtime locale replacement is unsupported.
interface LabelConfig {
density?: LabelDensity;
format?: string;
}| Field | Type | Default | Description |
|---|---|---|---|
density |
LabelDensity |
'auto' |
How many data labels to show. |
format |
string |
auto | d3-format override for label values, e.g. ",.0f". |
| Value | Behavior |
|---|---|
'all' |
Show every label. No collision detection. Can overlap. |
'auto' |
Show labels with collision detection. Overlapping labels are hidden and only appear in tooltips. |
'endpoints' |
Show only the first and last label per series. Good for line charts where the trend matters more than individual values. |
'none' |
Hide all labels. Rely on tooltips and the legend for value readout. |
Controls entrance animations and data-update transitions. Source: core/src/types/spec.ts.
// Enable all animation phases with defaults
{ animation: true }
// Entrance only (no update/exit transitions)
{ animation: { enter: true, update: false, exit: false } }
// Full control
{
animation: {
enter: { duration: 500, ease: 'smooth', stagger: true },
update: { duration: 500, ease: 'smooth' },
exit: { duration: 300, ease: 'smooth' },
annotationDelay: 200,
}
}boolean | AnimationConfig
true: enables entrance animation with sensible defaults. Whenanimation: true, all three phases (enter, update, exit) are enabled.false/ omitted: no animation.AnimationConfig: per-phase control.
| Field | Type | Default | Description |
|---|---|---|---|
enter |
AnimationPhaseConfig | boolean |
true |
Entrance animation when chart first renders. |
update |
UpdatePhaseConfig | boolean |
true |
Transition animation when data updates via .update(). |
exit |
AnimationPhaseConfig | boolean |
true |
Exit animation when marks are removed during updates. |
annotationDelay |
number |
200 |
Delay in ms before annotations animate in after marks. |
| Field | Type | Default | Description |
|---|---|---|---|
duration |
number |
500 |
Duration in ms. |
ease |
AnimationEase |
'smooth' |
Easing preset: 'smooth', 'snappy', 'linear'. |
stagger |
AnimationStagger | boolean |
true |
Stagger config for entrance. false = simultaneous. |
Everything in AnimationPhaseConfig, plus:
| Field | Type | Default | Description |
|---|---|---|---|
maxMarks |
number |
500 (SVG) / 20000 (canvas) |
Largest mark count that still tweens. Above it, updates swap instantly. |
maxMarks is update-only: enter and exit animate via CSS, which does not have
the same per-frame cost.
The default depends on the rendering surface. SVG mode writes attributes on one
DOM element per mark, so its cap is deliberately low. Canvas mark
mode writes into typed arrays and issues one batched fill per
color bucket, so its default is 20,000. An explicit maxMarks overrides both.
The count checked is max(previous, next), not the new layout alone. Exit ghosts
render on the destination surface, so a 4,000-point chart updating down to 400
still has ~3,600 departing marks to animate.
When animation.update is enabled and .update(newSpec) is called, the chart animates marks from their previous positions to the new layout instead of doing an instant swap. The engine matches marks across layouts using keys derived from data values.
Supported mark types: bar, line, area, point (scatter/dot), beeswarm.
What transitions:
- Rect marks (bars/columns): position + size tween
- Line/area marks: point-matched path morphing with enter/exit interpolation
- Point marks (scatter): cx/cy/r tween
- Beeswarm dots: same cx/cy/r tween as scatter. Set
encoding.keyto a stable entity field (state, country) so each dot tracks its entity across updates. Without it the fallback identity islane|value, which changes whenever the value does — duplicate values re-pair by row order and dots shuffle within the swarm instead of tracking. Keyed transitions are not supported on faceted beeswarms (panel marks share un-namespaced keys, matching scatter). - Axis ticks and gridlines: position slide for updated, fade for enter/exit
- Annotations, endpoint labels, mark labels: delayed crossfade (40% delay, 60% fade-in)
Fallback to instant swap when any of these conditions is true:
- Mark type changes between updates
- Encoding fields change (x/y/color/key/facet field names)
- Dimensions change (container resized)
- Mark count exceeds
animation.update.maxMarks(default 500). The cap exists because per-frame SVG attribute writes on thousands of elements drop frames on low-end devices. Raise it when the audience is desktop-heavy and you have measured the frame budget:animation: { update: { maxMarks: 5000 } }. prefers-reduced-motionis active- Entrance animation is still in flight
- Chart is a sparkline
Legend-hidden series: When series are hidden via legend toggle, they are removed from the data. On the next .update() call, those series re-enter with enter animations as new marks.
Scatter charts with thousands of points cost more in DOM elements than they do
in pixels. The renderer mount option moves point marks onto a <canvas>
layered under the chart SVG, which keeps a 50,000-point scatter interactive and
lets a high-cardinality keyed morph actually tween instead of snapping.
The renderer is not part of the spec. It is a host decision passed to
createChart() (or the renderer prop on the React/Vue/Svelte <Chart>
components), mirroring vega-embed's { renderer } embed option:
createChart(el, {
mark: 'point',
data: campuses, // 4,341 rows
encoding: {
x: { field: 'lowIncomePct', type: 'quantitative' },
y: { field: 'readingProficiency', type: 'quantitative' },
key: { field: 'campusId' },
},
}, { renderer: 'canvas' });| Value | Behavior |
|---|---|
'auto' |
Default. Canvas above 1,000 compiled points, SVG at or below. |
'canvas' |
Canvas at any point count. |
'svg' |
Never canvas. |
renderer applies to point marks only. On any other mark type, and on faceted,
layered or sparkline charts, it falls back to SVG; an explicit 'canvas' there
warns once per compile.
The canvas paints the figure background, the gridlines and the dots. The SVG
keeps axes, tick labels, the trendline, annotations, legend and chrome, and
still carries the chart's role and aria-label.
Everything below is a deliberate trade, not a limitation to work around:
- The trendline draws above the dots, even with
trendline.layer: 'below'. The canvas is a single layer under the whole SVG, so anything the SVG draws lands on top. - Gradient fills flatten to their first stop on screen. Exports render the true gradient, because they materialize a full SVG (see below).
- Overlapping dots can show a stroke over a neighbour's fill. The canvas
paints all fills in one pass and all strokes in a second, so a later dot's
stroke can sit over an earlier dot's fill. SVG paints each dot's fill and
stroke together. Sparse clouds are indistinguishable; tight clusters show it.
Lowering
mark.opacityfor dense scatters — worth doing regardless, to keep overplotting readable — makes it disappear. - Per-mark keyboard focus and edit-mode point selection are unavailable. There are no per-point elements to focus. Annotation, chrome and legend editing still work, and the screen-reader data table is unaffected.
- The screen-reader table caps at 1,000 rows with a caption naming the true total. Full data remains available via CSV export.
Tooltips, onMarkClick, onMarkHover and hover highlighting all work: pointer
events fall through to the canvas and hit-test against a spatial index.
Exports are full-fidelity regardless of what is on screen. Canvas-mode charts re-render to a complete SVG at export time, so gradients, gridlines and the background all come back as vector.
| Format | Behavior |
|---|---|
svg, svg-with-fonts |
Vector at or below 5,000 points -- byte-identical to exporting the SVG-mode twin. Above that, vector everything with the dot cloud inlined as one raster <image>, so gridlines and text stay crisp without shipping 50,000 <circle> elements. |
png, jpg, gif |
Always full vector before rasterizing, so output is pixel-identical to SVG mode. |
csv |
Unchanged. |
The key channel maps a field that uniquely identifies each datum across updates. It is not visually encoded. When omitted, the engine derives keys from the x-axis value (and color field for grouped charts).
Explicit keys are useful for scatter plots where multiple points can share the same x/y values:
{
mark: 'point',
data: points,
encoding: {
x: { field: 'longitude', type: 'quantitative' },
y: { field: 'latitude', type: 'quantitative' },
key: { field: 'stationId' },
},
animation: true,
}When .update() is called while a previous transition is still running, the new transition retargets from the current interpolated positions rather than snapping to the previous final state. This creates a smooth redirect effect for rapid updates or streaming data.
For line/area marks, mid-morph interruption uses a crossfade from the frozen intermediate path to the new final path, since re-matching interpolated point arrays is intractable.
Public authored theme overrides live in core/src/types/spec.ts. Every field is optional. Use ThemeConfig for input and ResolvedTheme for compiled output; the former public Theme alias is removed.
mergeThemeConfig() deep-merges authored layers before resolveTheme() supplies defaults. Component theme props override provider themes; mount/compile options override the spec. In a LayerSpec, chrome, axes and legend use the layer theme (falling back to the first leaf's); each leaf's marks use its theme merged over the layer theme. Nested objects merge, arrays and { light, dark } TokenValues replace atomically, and null replaces. colors: string[] is shorthand for colors.categorical.
import { broadsheet, mergeThemeConfig } from '@opendata-ai/openchart-core';
import type { ThemeConfig } from '@opendata-ai/openchart-core';
const theme: ThemeConfig = mergeThemeConfig(broadsheet, {
fonts: { sizes: { small: 12, axisTick: 12 } },
chrome: { title: { fontSize: 28 } },
rule: { width: 80 },
});Authored chrome.<element>.fontSize takes precedence over propagation from fonts.sizes, including across theme layers. Theme chrome values are objects such as { color: '#333' }; string color shorthand is removed. Spec chrome text shorthand (chrome: { title: 'A title' }) remains valid.
Single colors accept TokenValue (string | { light: string; dark: string }). Palette arrays contain color strings. These are light-mode defaults unless a pair is shown:
| Field | Default | Description |
|---|---|---|
colors.categorical |
Six-hue palette | Nominal-series stroke palette |
colors.categoricalFill |
Default fill palette; inherits an authored categorical palette when omitted | Quieter fill colors for area-filling marks |
colors.sequential / diverging |
Named palette maps | Numeric color scales |
colors.background |
transparent |
Visualization background; opaque surfaces derive from the neutral surface |
colors.text |
#09090b |
Main text ink |
colors.gridline |
rgba(0,0,0,0.08) |
Grid lines |
colors.hairline |
rgba(0,0,0,0.14) |
Structural separators |
colors.axis |
#71717a |
Axis lines and tick text |
colors.annotationLabel |
#333333 |
Primary callout ink |
colors.annotationMuted |
#6b7280 |
Subtitle and note ink |
colors.annotationLeader |
#8f8f8f |
Quiet connector ink |
colors.annotationRule |
#888888 / #9ca3af |
Reference line ink, light/dark |
colors.annotationRangeFill |
#f0c040 |
Range fill before opacity |
colors.annotationMarkerFill / annotationPlate |
Resolved neutral surface | Marker interior and label plate; explicit colors win |
Generic annotationFill and annotationText retain their other uses. See Theming tokens for the complete token and dark-mode mapping.
| Field | Default | Description |
|---|---|---|
fonts.family |
Inter Variable, Inter, system sans-serif | Primary font family |
fonts.mono |
JetBrains Mono, Fira Code, Cascadia Code, monospace | Tabular/monospace family |
fonts.sizes.title / subtitle |
26 / 14 | General title sizes |
fonts.sizes.body / small / axisTick |
13 / 12 / 12 | Body, small, and tick sizes |
fonts.sizes.metricLabel / metricValue |
11 / 22 | Metric row sizes |
fonts.weights.normal / medium / semibold / bold |
400 / 500 / 600 / 700 | Weight ladder |
chrome.eyebrow |
11px, 500, #06b6d4, 1.4 line-height |
Eyebrow style |
chrome.title |
26px, 600, #09090b, 1.15 line-height |
Title style |
chrome.subtitle |
14px, 400, #71717a, 1.45 line-height |
Subtitle style |
chrome.source / byline / footer |
12px, 400, #71717a, 1.4 line-height |
Attribution styles |
spacing.padding / chromeGap |
20 / 6 | Container padding / gap between chrome elements |
spacing.chromeToChart / chartToFooter |
16 / 12 | Gaps above and below the chart |
spacing.axisMargin |
6 | Axis margin |
borderRadius |
8 | Containers and tooltips |
rule draws a short colored bar above top chrome. Omission inherits a preset rule, or draws none on the default theme. rule: null removes an inherited rule; partial objects preserve unmodified fields.
| Field | Fallback when a rule is authored without a preset | Description |
|---|---|---|
rule.color |
Theme text ink | TokenValue color, including light/dark pairs |
rule.width |
40px | Rule length |
rule.thickness |
3px | Rule height; chrome reserves thickness plus chromeGap |
No top chrome means no rule.
The named presets are essay, wire, broadsheet, and terminal. Omit theme or pass {} for the default; editorial is removed. clarity is deprecated and contains only its background, axis, and eyebrow colors. legacyEditorial retains v8 typography for migration.
Only authored theme overrides and derived values are stamped inline. Omitted CSS tokens remain overridable by host styles, including --oc-border-radius; removing an override restores the host's previous inline value. Export captures live token values.
Controls dark mode rendering. Source: core/src/types/spec.ts.
type DarkMode = "auto" | "force" | "off";| Value | Behavior |
|---|---|
'auto' |
Checks window.matchMedia('(prefers-color-scheme: dark)'). Adapts to system preference. |
'force' |
Always render in dark mode. |
'off' |
Always render in light mode. This is the default. |
Dark mode resolution happens in the adapter layer (vanilla/React), not the engine. The adapter resolves the DarkMode union to a boolean and passes darkMode: true/false to the engine's CompileOptions. The engine then calls adaptTheme() to transform colors: swap background/text, adjust palette brightness, lighten gridlines.
You don't need to define a separate dark theme. The engine handles the color transformations automatically.
Input for data table visualizations. Source: core/src/types/spec.ts.
| Field | Type | Default | Description |
|---|---|---|---|
type |
'table' |
(required) | Discriminant. Always 'table'. |
data |
DataRow[] |
(required) | Array of data rows. Must be non-empty. |
columns |
ColumnConfig[] |
(required) | Column definitions. See ColumnConfig. |
rowKey |
string |
auto-generated | Field to use as a unique row identifier. |
chrome |
Chrome |
undefined |
Editorial text (title, subtitle, source, etc.). |
theme |
ThemeConfig |
undefined |
Theme overrides. |
search |
boolean |
false |
Enable client-side search/filter bar. |
pagination |
boolean | { pageSize: number } |
false |
Enable pagination. true uses default page size. Object sets explicit page size. |
stickyFirstColumn |
boolean |
false |
Freeze the first column during horizontal scroll. |
density |
'condensed' | 'regular' | 'relaxed' |
'regular' |
Row height: condensed 40px, regular 48px, relaxed 56px. An explicit value always wins over the responsive auto-condense. |
striped |
boolean |
false |
Zebra striping on body rows. Hairlines only by default. |
sort |
SortState |
first inline-bar column, descending | Initial sort applied before the user touches a header. { column: string, direction: 'asc' | 'desc' }. |
totalRow |
boolean | { label?: string } |
false |
Sticky footer summing every quantitative column over the filtered rows (the whole result set, not just the current page). true uses the label 'Total'. |
responsive |
boolean |
true |
Whether the table adapts to container width. |
watermark |
boolean |
true |
Whether to show the tryOpenData.ai watermark. Mount/compile options take precedence over spec and active breakpoint values. |
Configuration for a single table column. Source: core/src/types/table.ts.
| Field | Type | Default | Description |
|---|---|---|---|
key |
string |
(required) | Data field key. Must match a key in the data rows. |
label |
string |
same as key |
Display label for the column header. |
sortable |
boolean |
true |
Whether this column is sortable. |
align |
'left' | 'center' | 'right' |
'right' for numbers, 'left' otherwise |
Text alignment. |
width |
string |
auto-sized | CSS width like '200px' or '20%' (percent of the table width). An explicit width is fixed, not flex: it is honored as-is and never scaled up to fill the container, and the remaining space is shared by the auto-sized columns. When the explicit widths overflow the space left for them, they are scaled down proportionally (floored at 60px) and the engine emits a TABLE_WIDTH_OVERFLOW warning through onWarn; if the floor engages the table still overflows. |
format |
string |
none | d3-format or d3-time-format string (e.g. ',.0f', '%Y-%m-%d'), or a semantic keyword: 'percent', 'currency', 'ordinal', 'compact'. Tables show full precision by default; 'compact' opts a column into the chart-surface abbreviation (1.2k, 3.4M). |
type |
'quantitative' | 'nominal' | 'ordinal' | 'temporal' |
inferred from the data | Field type, Vega-Lite style. Drives alignment (quantitative right, everything else left) and which columns the total row sums. Number-valued keys that read as identifiers (id, zip, postalCode, year, fips, in either snake_case or camelCase) infer nominal. |
priority |
1 | 2 | 3 |
1 for the first column, 2 elsewhere |
Mobile priority for cards mode (below 400px): 1 renders first and spans the row, 2 renders as a label/value pair, 3 is hidden. |
total |
boolean |
true |
Whether this column contributes a sum to the totalRow footer. Only quantitative text/bar/heatmap columns are summable in the first place; false leaves the total cell blank (e.g. a column of rates or averages, where a sum is nonsense). |
Each column can have at most one visual feature. If multiple are set, precedence is: sparkline > bar > delta > heatmap > image > flag > categoryColors.
| Feature | Config field | Config type | Description |
|---|---|---|---|
| Heatmap | heatmap |
HeatmapColumnConfig |
Color cell background based on numeric value. |
| Inline bar | bar |
BarColumnConfig |
Proportional bar in the cell. |
| Delta chip | delta |
boolean | DeltaColumnConfig |
Signed, colored change chip (up 2.4%). |
| Sparkline | sparkline |
SparklineColumnConfig |
Mini line/bar chart from array data. |
| Image | image |
ImageColumnConfig |
Render cell value as an image. |
| Flag | flag |
boolean |
Render cell value as a country flag. |
| Category colors | categoryColors |
Record<string, string> |
Color-code cells by categorical value. |
| Field | Type | Default | Description |
|---|---|---|---|
palette |
string | string[] |
theme sequential | Palette name (e.g. 'blue', 'redBlue') or array of color stops. |
domain |
[number, number] |
auto from data | Explicit min/max for the color scale. |
colorByField |
string |
same column | Use a different field's values for coloring while displaying this column's values. |
| Field | Type | Default | Description |
|---|---|---|---|
maxValue |
number |
auto from data | Maximum value for the bar scale. |
color |
string |
first categorical color | Bar fill color. |
| Field | Type | Default | Description |
|---|---|---|---|
type |
'line' | 'bar' | 'column' |
'line' |
Sparkline chart type. |
valuesField |
string |
same column | Field containing the array of values to plot. |
color |
string |
first categorical color | Sparkline color. |
domain |
'shared' | 'row' | [number, number] |
'shared' |
Value domain each row is normalized against. 'shared' computes one extent across every row so heights are comparable down the column; 'row' normalizes each row against its own min/max (maximises shape, loses comparability); a tuple sets an explicit extent. |
| Field | Type | Default | Description |
|---|---|---|---|
invert |
boolean |
false |
Flip the color valence so negative values read as positive. Use for metrics where down is good (churn, latency, cost). |
| Field | Type | Default | Description |
|---|---|---|---|
width |
number |
24 |
Image width in pixels. |
height |
number |
24 |
Image height in pixels. |
rounded |
boolean |
false |
Apply border-radius for circular images. |
type CategoryColorsConfig = Record<string, string>;Maps category string values to CSS color strings. Only values with an explicit entry in the map get colored. Unmapped values are left unstyled by default.
To highlight specific values (e.g., 3 of 8 rows):
categoryColors: {
'Active': '#2a9d8f',
'Warning': '#f4a261',
'Critical': '#e76f51',
}
// Other values like 'Inactive', 'Pending', etc. remain unstyledTo auto-assign palette colors to all values (including unmapped ones), set autoAssign: true on the column config:
columns: [
{
key: 'status',
categoryColors: { 'Active': '#2a9d8f' },
autoAssign: true, // unmapped values get colors from the theme palette
},
]Interaction callbacks. Source: core/src/types/events.ts.
Pass these through MountOptions (vanilla) or ChartProps (React).
| Handler | Signature | When it fires |
|---|---|---|
onMarkClick |
(event: MarkEvent) => void |
User clicks a data mark (bar, point, line, arc). |
onMarkHover |
(event: MarkEvent) => void |
Mouse enters a data mark. |
onMarkLeave |
() => void |
Mouse leaves a data mark. |
onEdit |
(transaction: EditTransaction) => void |
One committed authored edit; apply atomically with applyEditTransaction and retain its inverse for undo. |
onLegendToggle |
(series: string, visible: boolean) => void |
User clicks a legend entry to show/hide a series. |
onAnnotationClick |
(annotation: Annotation, event: MouseEvent, element: ElementRef) => void |
User clicks an annotation element. |
onSelect |
(element: ElementRef) => void |
An element is selected by click, Tab, or select(). Requires interaction 'select' or 'edit'. |
onDeselect |
(element: ElementRef) => void |
The selected element is deselected. |
The interaction mount option ('read' | 'select' | 'edit') gates selection and editing; when omitted, onEdit implies 'edit' and onSelect/onDeselect imply 'select'. Every ElementRef carries a JSON Pointer path. See Editing and keyboard navigation for transaction persistence and cancelled-gesture behavior. See Interaction mode for the full table.
| Field | Type | Description |
|---|---|---|
datum |
DataRow |
The data row associated with the mark. For line/area marks, this is the first row of the series. |
series |
string | undefined |
Series identifier for multi-series charts. Matches the color encoding field value. |
position |
{ x: number, y: number } |
Click/hover position relative to the chart container. |
event |
MouseEvent |
The raw browser MouseEvent. |
Table events are passed through TableMountOptions (vanilla) or DataTableProps (React).
| Handler | Signature | When it fires |
|---|---|---|
onRowClick |
(row: Record<string, unknown>) => void |
User clicks a table row. |
onSortChange |
(sort: SortState | null) => void |
Sort state changes (React DataTable only). |
onSearchChange |
(query: string) => void |
Search query changes (React DataTable only). |
onPageChange |
(page: number) => void |
Page changes (React DataTable only). |
onStateChange |
(state: TableState) => void |
Any table state changes (vanilla createTable only). |
Input for network/relationship visualizations. Source: core/src/types/spec.ts.
Graphs render force-directed network visualizations on canvas. They support node interaction (click, drag, double-click), search, zoom/pan, keyboard navigation, and selection. Nodes are positioned by a force simulation running in a web worker.
| Field | Type | Default | Description |
|---|---|---|---|
type |
'graph' |
(required) | Discriminant. |
nodes |
GraphNode[] |
(required) | Node array. Each node must have an id: string field plus arbitrary data fields. |
edges |
GraphEdge[] |
(required) | Edge array. Each edge has source: string and target: string referencing node ids. |
encoding |
GraphEncoding |
undefined |
Visual property mappings for nodes/edges. |
layout |
GraphLayoutConfig |
undefined |
Layout algorithm configuration. |
nodeOverrides |
Record<string, NodeOverride> |
undefined |
Per-node visual overrides keyed by node id. See NodeOverride. |
seedNode |
string | { id, style? } |
undefined |
The graph's seed/root node: gets a ring and an always-on label, and stays lit when a category highlight or filter dims the rest. style is a NodeOverride that layers under any nodeOverrides entry for the same id. An unknown id warns and is ignored. Unrelated to layout.seed. |
chrome |
Chrome |
undefined |
Editorial text. |
annotations |
Annotation[] |
undefined |
Annotations. |
theme |
ThemeConfig |
undefined |
Theme overrides. |
watermark |
boolean |
true |
Whether to show the tryOpenData.ai watermark. Mount/compile options take precedence over spec and active breakpoint values. |
Each channel is a GraphEncodingChannel with field, optional type, and optional scale (same ScaleConfig as chart encodings). When scale.domain and scale.range are provided, the engine uses them directly instead of auto-deriving from the data. This is useful for controlling deterministic color assignment. scale.scheme is not read by the graph compile path and fails validation — use scale.range for explicit colors.
| Channel | Field type constraint | Purpose |
|---|---|---|
nodeColor |
nominal, ordinal, quantitative | Color mapping for nodes. |
nodeSize |
quantitative | Size mapping for nodes (3-12px radius). |
nodeOpacity |
quantitative | Fill opacity for nodes (0.25-1). |
edgeColor |
nominal, ordinal, quantitative | Color mapping for edges. |
edgeWidth |
quantitative | Width mapping for edges (0.5-4px). |
edgeStyle |
nominal, ordinal | Line style mapping (solid/dashed/dotted). |
nodeLabel |
any | Label field for nodes. |
nodeLabelPriority |
quantitative | Which labels survive thinning (0-1). Defaults to node degree when unset; nodeOverrides[id].alwaysShowLabel still hard-pins above it. |
When nodeColor encoding is set, it takes precedence over community-based coloring from layout.clustering. Community assignment still affects spatial grouping, but colors are driven by the encoding.
Per-node visual overrides. Useful for highlighting seed nodes, selected nodes, or applying custom styling to specific nodes.
| Field | Type | Description |
|---|---|---|
fill |
string |
Override fill color. |
radius |
number |
Override node radius. |
strokeWidth |
number |
Override stroke width. |
stroke |
string |
Override stroke color. |
alwaysShowLabel |
boolean |
Force label to always show regardless of zoom/priority. |
| Field | Type | Default | Description |
|---|---|---|---|
type |
'force' | 'radial' | 'hierarchical' |
(required) | Layout algorithm. |
clustering |
{ field: string } |
undefined |
Group nodes by a field for cluster forces. |
chargeStrength |
number |
library default | Charge strength for force layout. Negative = repulsion. |
linkDistance |
number |
library default | Target distance between linked nodes. |
collisionPadding |
number |
2 |
Extra pixels added to node radius for collision detection. |
linkStrength |
number |
undefined |
Link force strength override. |
centerForce |
boolean |
true |
Whether to apply center force to keep graph centered. |
Input for flow/Sankey diagram visualizations. Source: core/src/types/spec.ts.
Sankey diagrams show flows between stages. Each data row represents a link from a source node to a target node with a quantitative value. Nodes are auto-derived from the data. Uses a separate component: <Sankey> (React/Vue/Svelte) or createSankey (vanilla).
| Field | Type | Default | Description |
|---|---|---|---|
type |
'sankey' |
(required) | Discriminant. Always 'sankey'. |
data |
DataRow[] |
(required) | Tabular flow data. Each row is a source-target-value link. |
encoding |
SankeyEncoding |
(required) | Maps data fields to source, target, value channels. See below. |
nodeWidth |
number |
12 |
Width of node rectangles in pixels. |
nodePadding |
number |
16 |
Vertical padding between nodes in pixels. |
nodeAlign |
SankeyNodeAlign |
'justify' |
Node alignment: 'left', 'right', 'center', 'justify'. |
iterations |
number |
6 |
Number of layout relaxation iterations. |
linkStyle |
SankeyLinkColor |
'gradient' |
Link coloring: 'gradient', 'source', 'target', 'neutral'. |
linkOpacity |
number |
0.5 |
Link fill opacity (0-1). Defaults to 0.75 in dark mode. |
nodeLabelAlign |
string |
'auto' |
Label placement: 'auto', 'left', 'right'. |
chrome |
Chrome |
undefined |
Editorial text (title, subtitle, source, byline, footer). |
legend |
LegendConfig |
undefined |
Legend display configuration. |
theme |
ThemeConfig |
undefined |
Theme overrides. |
watermark |
boolean |
true |
Whether to show the tryOpenData.ai watermark. |
animation |
AnimationSpec |
undefined |
Entrance animation configuration. Sankey charts support entrance animations only. |
| Channel | Type | Required | Description |
|---|---|---|---|
source |
EncodingChannel |
yes | Source node field (nominal). |
target |
EncodingChannel |
yes | Target node field (nominal). |
value |
EncodingChannel |
yes | Flow value field (quantitative). |
color |
EncodingChannel |
no | Groups nodes into color categories. Colors cycle theme.colors.categorical; scale.scheme is not read and fails validation. |
tooltip |
EncodingChannel | EncodingChannel[] |
no | Tooltip encoding. |
import { Sankey } from "@opendata-ai/openchart-react";
const spec: SankeySpec = {
type: "sankey",
data: [
{ source: "Coal", target: "Electricity", value: 46.5 },
{ source: "Natural Gas", target: "Electricity", value: 38.2 },
{ source: "Natural Gas", target: "Heating", value: 25.8 },
{ source: "Electricity", target: "Residential", value: 38.5 },
{ source: "Electricity", target: "Commercial", value: 35.8 },
{ source: "Heating", target: "Residential", value: 15.2 },
],
encoding: {
source: { field: "source", type: "nominal" },
target: { field: "target", type: "nominal" },
value: { field: "value", type: "quantitative" },
},
chrome: {
title: "US Energy Flow",
subtitle: "From primary sources to end-use sectors, quadrillion BTU",
source: "U.S. Energy Information Administration",
},
};Input for choropleth and symbol map visualizations. Source: core/src/types/spec.ts.
Maps render TopoJSON geometries as SVG, join tabular data rows to features by id, and fill features by a color encoding. An optional points layer overlays lat/lon symbols above the features. Uses a separate component: <GeoMap> (React/Vue/Svelte) or createGeoMap (vanilla).
| Field | Type | Default | Description |
|---|---|---|---|
type |
'map' |
(required) | Discriminant. Always 'map'. |
geo |
GeoMapGeo |
(required) | TopoJSON features, join key field, projection, and camera focus. See below. |
data |
DataRow[] |
(required) | Tabular data to join to geo features. Can be [] for a basemap-only map (e.g. under a points layer). |
encoding |
GeoMapEncoding |
(required) | Maps data fields to the join key and fill color. See below. |
points |
GeoMapPointsLayer |
undefined |
Point/symbol layer rendered above the features. See below. |
chrome |
Chrome |
undefined |
Editorial text (title, subtitle, source, byline, footer). |
legend |
LegendConfig |
undefined |
Legend display configuration. See Map legends. |
theme |
ThemeConfig |
undefined |
Theme overrides. |
watermark |
boolean |
true |
Whether to show the tryOpenData.ai watermark. |
animation |
AnimationSpec |
undefined |
Entrance animation configuration. |
| Field | Type | Default | Description |
|---|---|---|---|
features |
TopoJSON topology | (required) | The geometry source. Import from us-atlas, world-atlas, or your own TopoJSON file. Must be a Topology, not a GeoJSON FeatureCollection. |
idField |
string |
'id' |
Field in the TopoJSON feature properties used as the join key. |
projection |
GeoMapProjection |
'albersUsa' |
'albersUsa', 'mercator', 'equalEarth', or 'identity'. Use 'identity' for pre-projected files like states-albers-10m.json (coordinates already in pixel space). |
focus |
GeoMapFocus | null |
undefined |
Camera focus. See Focus. null clears focus from a prior story step. |
zoom |
boolean | { maxZoom?, controls? } |
undefined (off) |
Let readers zoom and pan. See Reader zoom. |
| Channel | Type | Required | Description |
|---|---|---|---|
key |
EncodingChannel |
for choropleth | Data field that joins rows to feature ids. Optional in basemap-only mode (empty data plus a points layer). |
color |
EncodingChannel |
for choropleth | Feature fill. Quantitative or nominal (see below). Optional when a points layer is present. |
tooltip |
EncodingChannel | EncodingChannel[] |
no | Tooltip field(s) shown on feature hover. |
Quantitative color bins values with a quantile scale into one class per stop of a sequential scheme. scale.scheme picks the scheme: 'blue' (default), 'green', 'orange', 'purple', or 'teal'. Maps accept only these sequential names -- diverging names and Vega-Lite aliases fail validation here.
Nominal color assigns categorical fills. Colors come from scale.range (with scale.domain for category order), falling back to the theme categorical palette. scale.scheme on a categorical map channel fails validation; use scale.range instead.
Features with no matching data row render in a neutral fill.
Symbol overlay projected through the same geo projection as the features. Independent of the choropleth data join.
| Field | Type | Default | Description |
|---|---|---|---|
data |
DataRow[] |
(required) | Tabular point data. |
longitude |
EncodingChannel |
(required) | Field holding longitude (x coordinate). |
latitude |
EncodingChannel |
(required) | Field holding latitude (y coordinate). |
size |
EncodingChannel |
undefined |
Quantitative radius encoding. Area-proportional (r proportional to sqrt of value), default radius range 3-20px. No size legend is rendered; explain the scale in chrome.subtitle if needed. |
color |
EncodingChannel |
undefined |
Nominal (categorical swatches) or quantitative (sequential scheme, same scale.scheme names as choropleth). Independent scale from the choropleth color. |
tooltip |
EncodingChannel | EncodingChannel[] |
undefined |
Tooltip field(s) for point hover. |
key |
EncodingChannel |
undefined |
Stable id for event callbacks. Data-update transitions for points are not yet supported. |
opacity |
number |
0.65 |
Fill opacity for point circles (0-1). |
geo.focus zooms and pans the camera. Features outside the focus set are dimmed. Changing focus via .update() (or a scrollytelling story step) animates the camera between states.
| Form | Behavior |
|---|---|
'06' or 6 (a feature id) |
Fit the camera to that feature. |
['36', '34', '09'] |
Fit the union of those features. |
{ features, padding? } |
Same, with extra breathing room in pixels. |
{ points: true, padding? } |
Fit the points layer's cluster instead of any feature. Use when points occupy a small part of a large feature. |
{ points: { field, value } } |
Fit only the points where row[field] === value, so a story can pan between sub-clusters. |
null |
Clear focus (back to the full map). |
geo.zoom lets readers zoom into dense areas and pan around. It is off by default, so existing maps don't change.
geo: { features: txCounties, projection: "mercator", zoom: true }
// or tune it:
geo: { features: txCounties, zoom: { maxZoom: 8, controls: true } }| Field | Type | Default | Description |
|---|---|---|---|
maxZoom |
number |
12 |
Zoom ceiling as a multiple of the full map. Readers can never zoom out past the full map (1). |
controls |
boolean |
true |
Show the zoom in / zoom out / reset buttons in the map's top-right corner. |
The gestures follow the cooperative convention used by embedded web maps, so the map never traps page scroll:
| Input | Behavior |
|---|---|
| Mouse wheel | Ctrl + wheel (⌘ + wheel on macOS) zooms around the cursor. A plain wheel scrolls the page and briefly shows a hint. |
| Trackpad | Pinch zooms around the cursor. Two-finger scroll scrolls the page. |
| Mouse drag | Pans once zoomed in. A drag never fires onMarkClick. |
| Double-click | Zooms in 2x at the cursor; shift + double-click zooms out. |
| Touch | Two-finger pinch zooms and pans. One finger scrolls the page at the full view and pans the map once the reader has zoomed in; the reset button hands one-finger scrolling back to the page. |
| Keyboard | The map is focusable: +/- zoom, arrow keys pan, 0 resets. |
Panning is clamped so the map can't leave its frame, and the map is clipped to its area while zoomed so it doesn't slide under the chrome or legend. Points keep their screen radius and strokes stay hairline at every zoom level, and tooltips track the zoomed geometry. Button, key, and double-click zooms animate; under prefers-reduced-motion they snap.
Reader gestures and the imperative camera API (zoomTo, panTo, resetView, setCamera) move the same camera. onCameraChange (mount option, or the onCameraChange prop on <GeoMap>) reports every move as { cx, cy, k }.
A quantitative choropleth gets a class legend above the map by default; legend: { position: 'bottom' } moves it below. A points-layer color legend renders below the map by default; legend: { position: 'top-left' } floats it inside the map area's top-left corner on its own backdrop (useful when vertical space is tight). Other LegendPosition values fall back to the default. legend: { show: false } hides legends.
onMarkClick and onMarkHover (via MountOptions/component props) receive a GeoMapMarkEvent discriminated by kind: 'feature' | 'point'. Calling .update() with new data animates feature fills (recolor transition); the points layer re-renders without transitions.
import { GeoMap } from "@opendata-ai/openchart-react";
import us from "us-atlas/states-albers-10m.json";
const spec: GeoMapSpec = {
type: "map",
geo: { features: us, projection: "identity" },
data: [
{ id: "06", rate: 5.3 },
{ id: "48", rate: 4.3 },
// ...one row per state, keyed by FIPS id
],
encoding: {
key: { field: "id", type: "nominal" },
color: { field: "rate", type: "quantitative", format: ".1f" },
},
chrome: {
title: "Where the Job Market Is Tightest",
subtitle: "State unemployment rate, seasonally adjusted, %",
source: "Bureau of Labor Statistics",
},
animation: true,
};Input for US state tile grid maps. Source: core/src/types/spec.ts.
Every state renders as an equal-size square in a fixed 12x8 grid, so small states get the same visual weight as large ones. Uses a separate component: <TileMap> (React/Vue/Svelte) or createTileMap (vanilla).
| Field | Type | Default | Description |
|---|---|---|---|
type |
'tilemap' |
(required) | Discriminant. Always 'tilemap'. |
data |
Record<string, number | string | null> | DataRow[] |
(required) | Either a record mapping state postal codes to values ({ CA: 5.3, TX: 4.3 }), or tabular rows (requires encoding). Numeric values produce quantitative mode; string values produce categorical mode. |
encoding |
TileMapEncoding |
auto | Required when data is DataRow[]. Auto-generated when data is a record map. |
palette |
'blue' | 'green' | 'orange' | 'purple' | 'teal' |
'blue' |
Sequential color palette. Quantitative mode only. |
colors |
Record<string, string> |
undefined |
Category-to-color mapping for categorical tilemaps. When provided, forces categorical mode. |
chrome |
Chrome |
undefined |
Editorial text (title, subtitle, source, byline, footer). |
legend |
LegendConfig |
undefined |
Legend display configuration. show: false hides it. |
theme |
ThemeConfig |
undefined |
Theme overrides. |
watermark |
boolean |
true |
Whether to show the tryOpenData.ai watermark. |
animation |
AnimationSpec |
undefined |
Entrance animation configuration. |
valueFormat |
string |
undefined |
Tilemap fallback d3-format string for values, legend labels, and tooltips; encoding.value.format takes precedence. |
| Channel | Type | Required | Description |
|---|---|---|---|
state |
EncodingChannel |
yes | State code field (nominal). Maps to US state postal abbreviations. |
value |
EncodingChannel |
yes | Value field. Quantitative values map to the sequential palette; strings switch to categorical mode. |
color |
EncodingChannel |
no | Nominal color channel. When present, enables categorical coloring. |
tooltip |
EncodingChannel | EncodingChannel[] |
no | Tooltip encoding. |
- Quantitative mode renders a gradient legend bar with min/max labels below the grid. Categorical mode (string values, a
colorchannel, or acolorsmap) renders swatch rows instead. - States missing from the data render as empty tiles, keeping their place on the grid.
- Number formatting: set
encoding.value.format(d3-format string, e.g.".1f","$,.0f","~s"). It applies to tile values, legend labels, and tooltips, and wins over top-levelvalueFormat. With record-map data and no explicit encoding,valueFormatremains supported. scale.schemefails validation on tilemap encoding channels. The tilemap compile path never reads it; the top-levelpaletteproperty is the mechanism tilemaps use for color.
import { TileMap } from "@opendata-ai/openchart-react";
const spec: TileMapSpec = {
type: "tilemap",
data: { CA: 5.3, TX: 4.3, NY: 4.5, FL: 3.1 /* ...state code -> value */ },
palette: "blue",
chrome: {
title: "Where the Job Market Is Tightest",
subtitle: "State unemployment rate, seasonally adjusted, %",
source: "Bureau of Labor Statistics",
},
animation: true,
};With tabular data, add the encoding explicitly:
const spec: TileMapSpec = {
type: "tilemap",
data: [
{ code: "CA", rate: 5.3 },
{ code: "TX", rate: 4.3 },
],
encoding: {
state: { field: "code", type: "nominal" },
value: { field: "rate", type: "quantitative", format: ".1f" },
},
};const spec: ChartSpec = {
mark: "line",
data: [
{ date: "2023-01-01", revenue: 42000, region: "North" },
{ date: "2023-04-01", revenue: 58000, region: "North" },
{ date: "2023-07-01", revenue: 63000, region: "North" },
{ date: "2023-01-01", revenue: 31000, region: "South" },
{ date: "2023-04-01", revenue: 44000, region: "South" },
{ date: "2023-07-01", revenue: 52000, region: "South" },
],
encoding: {
x: { field: "date", type: "temporal" },
y: {
field: "revenue",
type: "quantitative",
axis: { label: "Revenue ($)", format: ",.0f" },
},
color: { field: "region", type: "nominal" },
},
chrome: {
title: "Revenue by region",
subtitle: "Quarterly results, 2023",
source: "Source: Finance team",
},
labels: { density: "endpoints" },
annotations: [
{ type: "refline", y: 50000, label: "Target", style: "dashed" },
],
};const spec: ChartSpec = {
mark: "area",
data: [
{ month: "2024-01", users: 1200 },
{ month: "2024-02", users: 1800 },
{ month: "2024-03", users: 2400 },
{ month: "2024-04", users: 3100 },
],
encoding: {
x: { field: "month", type: "temporal" },
y: { field: "users", type: "quantitative" },
},
chrome: { title: "User growth" },
};const spec: ChartSpec = {
mark: "bar",
data: [
{ language: "Python", popularity: 29 },
{ language: "JavaScript", popularity: 24 },
{ language: "TypeScript", popularity: 17 },
{ language: "Java", popularity: 14 },
{ language: "Go", popularity: 10 },
],
encoding: {
x: { field: "popularity", type: "quantitative" },
y: { field: "language", type: "nominal" },
},
chrome: { title: "Language popularity" },
};const spec: ChartSpec = {
mark: "bar",
data: [
{ month: "Jan", sales: 120 },
{ month: "Feb", sales: 180 },
{ month: "Mar", sales: 240 },
{ month: "Apr", sales: 210 },
],
encoding: {
x: { field: "month", type: "nominal" },
y: { field: "sales", type: "quantitative" },
},
chrome: { title: "Monthly sales" },
labels: { density: "all", format: ",.0f" },
};const spec: ChartSpec = {
mark: "arc",
data: [
{ category: "Desktop", share: 58 },
{ category: "Mobile", share: 35 },
{ category: "Tablet", share: 7 },
],
encoding: {
theta: { field: "share", type: "quantitative" },
color: { field: "category", type: "nominal" },
},
chrome: { title: "Traffic by device" },
};const spec: ChartSpec = {
mark: { type: "arc", innerRadius: 40 },
data: [
{ status: "Complete", count: 42 },
{ status: "In Progress", count: 18 },
{ status: "Blocked", count: 5 },
],
encoding: {
theta: { field: "count", type: "quantitative" },
color: { field: "status", type: "nominal" },
},
chrome: { title: "Task status" },
};const spec: ChartSpec = {
mark: "point",
data: [
{ gdp: 21400, lifeExp: 78.9, country: "USA", pop: 331 },
{ gdp: 40300, lifeExp: 83.4, country: "Switzerland", pop: 8.6 },
{ gdp: 1900, lifeExp: 69.4, country: "India", pop: 1380 },
{ gdp: 10500, lifeExp: 76.9, country: "China", pop: 1400 },
],
encoding: {
x: {
field: "gdp",
type: "quantitative",
axis: { label: "GDP per capita ($)" },
},
y: {
field: "lifeExp",
type: "quantitative",
axis: { label: "Life expectancy" },
},
size: { field: "pop", type: "quantitative" },
color: { field: "country", type: "nominal" },
},
chrome: { title: "GDP vs life expectancy" },
};const spec: ChartSpec = {
mark: "circle",
data: [
{ team: "Engineering", satisfaction: 8.2 },
{ team: "Design", satisfaction: 7.9 },
{ team: "Sales", satisfaction: 6.5 },
{ team: "Support", satisfaction: 7.1 },
],
encoding: {
x: { field: "satisfaction", type: "quantitative" },
y: { field: "team", type: "nominal" },
},
chrome: { title: "Team satisfaction scores" },
};const spec: TableSpec = {
type: "table",
data: [
{
city: "San Francisco",
temp: 18.2,
pop: 874961,
trend: [15, 16, 17, 18, 19, 18],
},
{
city: "New York",
temp: 12.8,
pop: 8336817,
trend: [8, 10, 14, 18, 16, 12],
},
{
city: "Austin",
temp: 20.5,
pop: 978908,
trend: [12, 15, 20, 25, 22, 18],
},
],
columns: [
{ key: "city", label: "City" },
{
key: "temp",
label: "Avg Temp (C)",
format: ".1f",
heatmap: { palette: "redBlue" },
},
{ key: "pop", label: "Population", format: ",.0f", bar: {} },
{
key: "trend",
label: "6-Month Trend",
sparkline: { type: "line", valuesField: "trend" },
},
],
chrome: { title: "City comparison" },
search: true,
pagination: { pageSize: 25 },
stickyFirstColumn: true,
};Helper functions that reduce boilerplate. Source: core/src/helpers/spec-builders.ts.
All builders accept field names as strings (auto-infer type from data) or full EncodingChannel objects (when you need to customize type, aggregate, axis, or scale).
| Builder | Signature | Produces |
|---|---|---|
lineChart |
(data, x, y, options?) |
ChartSpec with mark: 'line' |
areaChart |
(data, x, y, options?) |
ChartSpec with mark: 'area' |
barChart |
(data, category, value, options?) |
ChartSpec with mark: 'bar'. category -> y, value -> x |
columnChart |
(data, x, y, options?) |
ChartSpec with mark: 'bar'. x nominal, y quantitative |
pieChart |
(data, category, value, options?) |
ChartSpec with mark: 'arc'. category -> color, value -> y |
donutChart |
(data, category, value, options?) |
ChartSpec with mark: { type: 'arc', innerRadius: 40 }. category -> color, value -> y |
dotChart |
(data, x, y, options?) |
ChartSpec with mark: 'circle' |
scatterChart |
(data, x, y, options?) |
ChartSpec with mark: 'point' |
dataTable |
(data, options?) |
TableSpec. Auto-generates columns from data keys if none provided. |
| Field | Type | Description |
|---|---|---|
color |
FieldRef |
Color encoding for series differentiation. |
size |
FieldRef |
Size encoding (bubble charts). |
chrome |
Chrome |
Editorial chrome. |
annotations |
Annotation[] |
Annotations. |
responsive |
boolean |
Responsive behavior. |
theme |
ThemeConfig |
Theme overrides. |
watermark |
boolean |
Whether to show the tryOpenData.ai watermark. Mount/compile options take precedence over the spec; defaults to true. |
FieldRef is string | EncodingChannel. When a string is provided, inferFieldType() uses the shared builder/compiler inference: sample up to 50 rows, ignore nulls, and require more than 80% agreement. Numeric strings are considered before dates, so a column of '2020' values is quantitative. Set the channel type explicitly when years should be ordinal or temporal.
import { lineChart } from "@opendata-ai/openchart-core";
const spec = lineChart(data, "date", "revenue", {
color: "region",
chrome: { title: "Revenue trend" },
annotations: [{ type: "refline", y: 50000, label: "Target" }],
});The engine validates specs at runtime. Source: engine/src/compiler/validate.ts.
import { validateSpec } from "@opendata-ai/openchart-engine";
const result = validateSpec(spec);
// result.valid: boolean
// result.errors: ValidationError[]
// result.normalized: VizSpec | null| Field | Type | Description |
|---|---|---|
message |
string |
Human-readable error description. |
path |
string |
Root-relative RFC 6901 JSON Pointer to the value that failed, e.g. '/encoding/x/field' or '/layer/1/data/0/value'. The root is ''. |
code |
ValidationErrorCode |
Machine-readable error code. |
suggestion |
string |
Actionable fix suggestion. |
| Code | Meaning |
|---|---|
INVALID_TYPE |
Wrong type (expected object, got array, etc.). |
MISSING_FIELD |
Required field is missing. |
INVALID_VALUE |
Field has an invalid value (bad chart type, bad dark mode string, etc.). |
EMPTY_DATA |
Data array is empty. |
DATA_FIELD_MISSING |
Encoding references a field that doesn't exist in the data. |
ENCODING_MISMATCH |
Encoding type doesn't match the channel requirements or the actual data values. |
- Chart types for a visual gallery with boilerplate specs and live examples
- Tables for data table features (heatmaps, sparklines, flags, and more)
- Graphs for network/relationship visualizations
- Getting started for a hands-on tutorial
- Architecture for how the packages and compilation pipeline work
- Integration guide for building apps on top of the library
These library interfaces support authoring and reader applications. OpenStudio adoption is separate; see Migrating to v9 for the changed defaults and removed APIs.
- Theme: v9 defaults include 12px axes/small text and larger chrome gaps.
clarityis deprecated and retains only background, axis, and eyebrow colors.legacyEditorialrestores v8 typography;migrateSpecV8can materialize the wider v8 styling contract without freezing layout behavior. - Motion:
animation: 'fluid' | 'subtle' (omit the preset for v8 timing); explicit phases override their preset. A phase can usespring: { stiffness: 320, damping: 36, mass: 1 }or the existing duration/easing configuration. Combining a spring with duration/easing in the same phase is an error. Quantitative springs are bounded, with a finite terminal state. Omitted/false animation remains disabled. - Responsiveness:
responsive: { preserveEnd: true, labelSummary: true }retains the final x-axis boundary and moves overflowing endpoint identities to a wrapped list. Explicit axis values and author choices remain authoritative.chromeLayout: 'grow'gives that content space. Tables may opt intomobileLayout: 'priority';cardsremains the default, withscrollalso available. - Field metadata: chart, table and map documents accept
fields, keyed by field name. Each entry may declaretype(text,number,date),role: 'identity',label,unit,measure,provenance,representation(number,percent,proportion),interval: { lower: 'lowerField', upper: 'upperField' }, orstatusField. Supplied intervals are never synthesized. Percent means 0–100 and proportion means 0–1. - Reader identity: a unique authored
encoding.keysupports observation persistence. Missing/duplicate keys clear observation pins on data replacement. Aggregated observations are groups, not raw rows, and the reference reader clears them on data/grouping revision. ThecreateReadercontroller is independent from editor selection and usesChartInstance.sampleLayout()for supported moving geometry. Callreader.update(spec)afterchart.update(spec); it reconciles visibility against the compiled destination marks even when the source data array is unchanged. An explicitvisibleKeysoverride remains supported. Layered charts retain per-layer hover/crosshair inspection, but reader pinning across layers is not yet supported: aLayerSpecclears pins with an announcement rather than guessing cross-layer identity. - Design review:
diagnoseChart(spec, layout)returns structured checks with code, severity, checkability, path, viewport, explanation and origin.CompileOptions.onDiagnosticreceives these checks.recommendCompositions(data, question, roles)requires explicit field roles and returns explained alternatives. A diagnosticfixis aSpecPatchwhosechangesareEditOps.applySpecPatch(spec, patch)rejects stale snapshot preconditions, applies the operations atomically throughapplyEditTransaction, and returns{ spec, inverse }; applyinverseto the patched document to undo. Hosts should preview patches before applying them. The reference editor uses the full authored document for patches even while previewing a filtered scene.
Host-neutral identity, comparison and explicit dataset-binding helpers are also exported from the engine. They provide foundations for future linked views, without implicitly declaring compatible measures or raw-row/aggregate relationships. See the delivery record for the advanced capabilities and release gates still pending.
Measured channels accept finite numbers and nonblank numeric strings, including zero. Null, blank text, booleans, NaN and infinities are absent measurements, not zeroes. This applies to chart quantities, map coordinates/colors, size/domain extraction, aggregates, bins, density and window differences/percentage changes. Supplied missing line/area observations remain gaps during rendering, updates and exports. Sankey requires positive finite flow values; omitted flows produce a warning and an all-invalid dataset produces an empty finite layout. Cumulative sums skip absent/non-numeric contributions; window and density groups distinguish numeric and string keys. Layer domains and continuous legends use the same finite-measurement policy. Text-size fallback and absent-series stack conventions are unchanged.
Maps, tile maps, bar lists and Sankey support arrows for inspection and Enter/Space for their existing click callbacks, including when tooltip: false. Escape dismisses inspection; Tab exits naturally. A keyboard-focused map mark uses arrows for mark navigation; the map root retains its separate zoom/pan shortcuts. Tables restore keyed cell/header focus across updates and keep native mobile detail disclosures operable.
Table rowKey values must be unique to support identity across revisions. Numeric 1 and string "1" are distinct. Missing/duplicate keys emit TABLE_ROW_KEY_AMBIGUOUS and use revision-local identities. Treat compiled row IDs as opaque. CSV export includes all filtered and sorted rows, not just the current pagination page.
responsive.compactAnnotationLimit?: number | null is an optional nonnegative
integer limit below 400px outer width. Omitted means one; null means unlimited,
and zero demotes all eligible callouts. responsive: false callouts are exempt. Priority/source
order precedes coverage/collision thinning. Existing disabled-response and
short-height rules still apply. Facet panel width does not trigger this limit.
Read layout.chrome.noteBandHeight for note space and layout.dimensions.height
for the final rendered/exported height. chromeLayout: 'grow' also supports facets;
chrome.grownHeight reports engine-added height for auto-height consumers.
See annotation migration guidance.