There’s a gap in charting libraries that nobody talks about directly, but everyone has felt. You want your charts to look like they belong in The Economist or a Chartr newsletter. Clean typography, proper source attribution, a headline that carries the insight. But the library you’re using gives you a default bar chart that screams “made with a library,” and the path from there to editorial polish is 200 lines of config overrides, CSS hacks, and fighting the framework’s opinions about where labels should go.
Most teams never cross that gap. They ship the default look, maybe adjust the colors, and move on. Not because they don’t care about quality, but because the effort required to get from “functional chart” to “chart worth sharing” is wildly disproportionate to what they’re trying to communicate.
Today we’re open-sourcing OpenChart, the library that powers all the data visualizations on OpenData. It’s free, MIT-licensed, and available now at github.com/tryopendata/openchart.
What OpenChart Actually Is
OpenChart is a spec-driven visualization library. You describe what the chart should communicate in a JSON spec, and the engine handles everything else: scales, axes, labels, colors, responsive layout, accessibility.
const spec = {
type: "bar",
data: [
{ category: "Food", value: 332 },
{ category: "Housing", value: 287 },
{ category: "Transport", value: 198 },
],
encoding: {
x: { field: "category", type: "nominal" },
y: { field: "value", type: "quantitative" },
color: { field: "category", type: "nominal" },
},
chrome: {
title: "CPI by Category",
subtitle: "Consumer Price Index, December 2025",
source: "Bureau of Labor Statistics",
},
};
The spec IS the API. Not a component tree. Not imperative drawing calls. A declarative description that can be stored in a database, generated by an LLM, version-controlled, or validated before anything touches the screen. You can diff two specs to see exactly what changed between chart versions. You can generate them programmatically and inspect them before rendering. The spec serializes as plain JSON, so it works anywhere JSON works.
The encoding model follows Vega-Lite conventions. Each channel (x, y, color, size) maps a field from your data to a visual property, with a type that tells the engine how to interpret the values: quantitative for continuous numbers, temporal for dates, nominal for unordered categories, ordinal for ordered ones. The engine picks the right scale type automatically. A temporal x-axis gets a time scale. A nominal color encoding gets a categorical palette from the theme.
Headless Architecture
The engine is split into packages with a strict dependency chain:
core → engine → vanilla → react/vue/svelte
The key insight: the math layer (@openchart/engine) has zero DOM dependencies. It takes a spec plus dimensions and produces a layout object with every position computed to the pixel. Scales, tick marks, label positions, mark coordinates, annotation anchors, accessibility metadata. All of it resolved to concrete values. The renderers (@openchart/vanilla, @openchart/react, etc.) are thin layers that walk the layout object and draw.
This split has real consequences for how you work. You can test chart logic with plain Vitest, no jsdom, no browser. “Does this dataset produce the right Y-axis domain?” is a unit test that runs in milliseconds. You can render on the server without Puppeteer. The same compilation step produces identical output in Node.js and the browser. And if you need to support a framework that doesn’t have a wrapper yet, you’re writing a thin rendering adapter, not reimplementing chart math.
The @openchart/vanilla package handles SVG and HTML rendering, resize observation, and chart exports. The framework-specific packages (@openchart/react, @openchart/vue, @openchart/svelte) are lifecycle bridges. The React <Chart /> component is roughly 30 lines: it creates a ref, calls createChart() on mount, and chart.update(spec) when props change. That’s it.
Editorial-First Design
Most chart libraries treat titles and source attribution as afterthoughts. You pass a string, the library sticks it somewhere with default styling, and you’re left tweaking font sizes in CSS. Annotations are worse: most libraries either don’t support them or offer escape hatches into the raw SVG.
OpenChart treats these as first-class structural elements because that’s what separates a chart from a visualization. The core principles: the headline IS the insight, one chart communicates one thing, minimal chrome, strategic annotation. Those principles are baked into OpenChart’s chrome system. Titles, subtitles, source attribution, and annotations are all first-class fields in the spec, not afterthoughts you bolt on with CSS.
Annotations come in three types: reference lines (baselines, thresholds), range highlights (recession bands, target zones), and text callouts (labels pointing to key data points with connector lines). These are what turn a chart from “here’s some data” into “here’s the story.” A reference line at the national average. A shaded band marking a recession. A callout on the inflection point where the trend reversed. All declarative, all in the spec, all positioned by the engine’s math layer.
What You Can Build
Chart types: line, area, bar, column, scatter, dot, pie, and donut. Each with multi-series support via the color encoding channel. Scatter charts support bubble sizing and built-in trendlines. Donut is the same engine as pie with a non-zero inner radius.
Rich data tables go well beyond basic HTML: sorting, search, pagination, heatmap cells (color-coded by value), inline bars (mini bar charts in each cell), and sparklines (embedded line, bar, or column mini-charts showing trends). The table compiler handles all the layout math headlessly, same as charts.
Force-directed network graphs with canvas rendering handle larger datasets where SVG would choke. Nodes, edges, labels, collision detection, all computed by the engine and rendered to a canvas element.
All chart types support dark mode, responsive breakpoints, deep-mergeable theme configuration, and export to SVG, PNG, or CSV.
Accessibility
Accessibility is computed during compilation, not bolted on after rendering. This is a meaningful distinction because it means accessibility is testable without a browser.
The engine auto-generates alt text from the spec and data. “Bar chart showing CPI by Category across 3 categories (3 data points).” Every mark in the layout carries an ARIA label. “Food: 332.” The renderer maps these to aria-label attributes on the corresponding SVG elements.
Color-blindness simulation uses Brettel, Vienot, and Mollon matrices for protanopia, deuteranopia, and tritanopia. checkPaletteDistinguishability() takes a palette and a deficiency type, simulates how each color appears, and verifies that all pairs maintain sufficient perceptual distance. WCAG contrast checking is built into the color system with contrastRatio(), meetsAA(), and findAccessibleColor() (which binary-searches for an adjusted variant that preserves hue while meeting the 4.5:1 threshold).
You can verify all of this in a unit test running in Node. No browser, no screen reader emulation. Just assertions against the compiled layout.
Getting Started
Installation is one line:
npm install @openchart/react
# or
npm install @openchart/vue
npm install @openchart/svelte
npm install @openchart/vanilla
The live demo has interactive examples for every chart type, annotation editing, dark mode, responsive layouts, and table features.
Source is on GitHub: github.com/tryopendata/openchart
If you’ve ever spent more time fighting your chart library than analyzing your data, give OpenChart a look. And if you build something cool with it, we’d love to see it.