Skip to content

Latest commit

 

History

History
1904 lines (1501 loc) · 127 KB

spec-reference.md

File metadata and controls

1904 lines (1501 loc) · 127 KB

Spec reference

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.

Contents

VizSpec

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 a ChartSpec
  • table produces a TableSpec
  • graph produces a GraphSpec
  • sankey produces a SankeySpec
  • tilemap produces a TileMapSpec
  • map produces a GeoMapSpec
  • barlist produces a BarListSpec

Type guards are available: isChartSpec(spec), isTableSpec(spec), isGraphSpec(spec), isSankeySpec(spec), isTileMapSpec(spec), isGeoMapSpec(spec), isBarListSpec(spec).


ChartSpec

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).

DataRow

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.

Mark properties

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.

Gradients

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.


LayerSpec

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' },
};

Encoding

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.

EncodingChannel

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.

FieldType

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

AggregateOp

'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.

AxisConfig

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.

ScaleConfig

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.


Encoding by chart type

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

Channel purpose per chart type

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.


Faceting

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

FacetChannel

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).

resolve

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.

Layout behavior

  • 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.

Facet example

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.


Chrome

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.

ChromeText

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.

maxLines

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.

Chrome layout

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.


Transforms

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.


Annotations

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:

TextAnnotation

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.

Text: wrapping, line breaks, and bold spans

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.

Connectors

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.

Dot 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.

RangeAnnotation

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/x2 only: vertical band (full chart height)
  • y1/y2 only: horizontal band (full chart width)
  • All four: rectangle

RefLineAnnotation

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.

Annotation example

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,
  },
];

Labels

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".

LabelDensity

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.

Animation

Controls entrance animations and data-update transitions. Source: core/src/types/spec.ts.

Quick start

// 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,
  }
}

AnimationSpec

boolean | AnimationConfig

  • true: enables entrance animation with sensible defaults. When animation: true, all three phases (enter, update, exit) are enabled.
  • false / omitted: no animation.
  • AnimationConfig: per-phase control.

AnimationConfig

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.

AnimationPhaseConfig

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.

UpdatePhaseConfig

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.

Data-update transitions

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.key to a stable entity field (state, country) so each dot tracks its entity across updates. Without it the fallback identity is lane|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-motion is 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.

Canvas mark mode

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.

What canvas mode changes

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.opacity for 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

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.

encoding.key channel

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,
}

Interruption behavior

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.


ThemeConfig

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.

Color overrides

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.

Fonts, chrome, and spacing

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

The editorial rule

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.


DarkMode

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.


TableSpec

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.

ColumnConfig

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).

Column visual types

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.

HeatmapColumnConfig

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.

BarColumnConfig

Field Type Default Description
maxValue number auto from data Maximum value for the bar scale.
color string first categorical color Bar fill color.

SparklineColumnConfig

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.

DeltaColumnConfig

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).

ImageColumnConfig

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.

CategoryColorsConfig

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 unstyled

To 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
  },
]

Event handlers

Interaction callbacks. Source: core/src/types/events.ts.

Pass these through MountOptions (vanilla) or ChartProps (React).

ChartEventHandlers

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.

MarkEvent

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 event handlers

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).

GraphSpec

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.

GraphEncoding

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.

NodeOverride

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.

GraphLayoutConfig

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.

SankeySpec

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.

SankeyEncoding

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.

Sankey example

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",
  },
};

GeoMapSpec

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.

GeoMapGeo

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.

GeoMapEncoding

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.

GeoMapPointsLayer

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).

Focus

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).

Reader zoom

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 }.

Map legends

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.

Map events and updates

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.

Map example

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,
};

TileMapSpec

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.

TileMapEncoding

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.

Tilemap behavior

  • Quantitative mode renders a gradient legend bar with min/max labels below the grid. Categorical mode (string values, a color channel, or a colors map) 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-level valueFormat. With record-map data and no explicit encoding, valueFormat remains supported.
  • scale.scheme fails validation on tilemap encoding channels. The tilemap compile path never reads it; the top-level palette property is the mechanism tilemaps use for color.

Tilemap example

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" },
  },
};

Complete spec examples

Line chart

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" },
  ],
};

Live example

Area chart

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" },
};

Live example

Bar chart (horizontal)

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" },
};

Live example

Column chart (vertical bars)

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" },
};

Live example

Pie chart

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" },
};

Live example

Donut chart

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" },
};

Live example

Scatter chart

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" },
};

Live example

Dot plot

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" },
};

Live example

Data table

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,
};

Live example


Spec builder functions

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.

ChartBuilderOptions

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.

Builder example

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" }],
});

Validation

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

ValidationError

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.

ValidationErrorCode

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.

Related docs

  • 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

Authoring foundation additions

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. clarity is deprecated and retains only background, axis, and eyebrow colors. legacyEditorial restores v8 typography; migrateSpecV8 can 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 use spring: { 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 into mobileLayout: 'priority'; cards remains the default, with scroll also available.
  • Field metadata: chart, table and map documents accept fields, keyed by field name. Each entry may declare type (text, number, date), role: 'identity', label, unit, measure, provenance, representation (number, percent, proportion), interval: { lower: 'lowerField', upper: 'upperField' }, or statusField. Supplied intervals are never synthesized. Percent means 0–100 and proportion means 0–1.
  • Reader identity: a unique authored encoding.key supports 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. The createReader controller is independent from editor selection and uses ChartInstance.sampleLayout() for supported moving geometry. Call reader.update(spec) after chart.update(spec); it reconciles visibility against the compiled destination marks even when the source data array is unchanged. An explicit visibleKeys override remains supported. Layered charts retain per-layer hover/crosshair inspection, but reader pinning across layers is not yet supported: a LayerSpec clears 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.onDiagnostic receives these checks. recommendCompositions(data, question, roles) requires explicit field roles and returns explained alternatives. A diagnostic fix is a SpecPatch whose changes are EditOps. applySpecPatch(spec, patch) rejects stale snapshot preconditions, applies the operations atomically through applyEditTransaction, and returns { spec, inverse }; apply inverse to 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.

Numeric observations and interaction contracts

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.

Compact callout limits and note sizing

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.