@internetstiftelsen/charts
v0.0.8
Published
A framework-agnostic, composable charting library built on D3.js with TypeScript.
Readme
Chart Library
A framework-agnostic, composable charting library built on D3.js with TypeScript.
Features
- Framework Agnostic: Core library has zero framework dependencies - works with vanilla JS, React, Vue, Svelte, or any other framework
- Flexible Scale System: Support for band, linear, time, and logarithmic scales
- Composable Architecture: Build charts by composing components (lines, axes, grids, tooltips, legends, titles)
- Layout-Driven Design: Components self-measure and automatically adjust chart dimensions
- Automatic Resize: Built-in ResizeObserver handles responsive behavior automatically
- Type Safe: Written in TypeScript with comprehensive type definitions
- Data Validation: Built-in validation with helpful error messages
- Performance Optimized: Data caching and minimized redundant calculations
- Automatic Color Assignment: Smart color palette system with sensible defaults
Installation
npm install @internetstiftelsen/chartsQuick Start
Vanilla JavaScript
import { XYChart } from '@internetstiftelsen/charts/xy-chart';
import { Line } from '@internetstiftelsen/charts/line';
import { Bar } from '@internetstiftelsen/charts/bar';
import { XAxis } from '@internetstiftelsen/charts/x-axis';
import { YAxis } from '@internetstiftelsen/charts/y-axis';
import { Grid } from '@internetstiftelsen/charts/grid';
import { Tooltip } from '@internetstiftelsen/charts/tooltip';
import { Legend } from '@internetstiftelsen/charts/legend';
// Your data
const data = [
{ date: '2010', revenue: 100, expenses: 80 },
{ date: '2011', revenue: 150, expenses: 90 },
{ date: '2012', revenue: 200, expenses: 110 },
{ date: '2013', revenue: 250, expenses: 130 },
];
// Create chart
const chart = new XYChart({ data });
// Add components
chart
.addChild(new Title({ text: 'Revenue vs Expenses' }))
.addChild(new Grid({ horizontal: true, vertical: false }))
.addChild(new XAxis({ dataKey: 'date' }))
.addChild(new YAxis())
.addChild(
new Tooltip({
formatter: (dataKey, value) => `<strong>${dataKey}</strong>: $${value}k`,
})
)
.addChild(new Legend({ position: 'bottom' }))
.addChild(new Line({ dataKey: 'revenue' })) // Auto-assigned color
.addChild(new Line({ dataKey: 'expenses' })); // Auto-assigned color
// Render to DOM (automatically resizes with container)
chart.render('#chart-container');
// Later: update with new data
chart.update(newData);
// Clean up when done
chart.destroy();With React (Demo Wrapper)
import { useRef, useEffect } from 'react';
import { XYChart, Line, Bar, XAxis, YAxis, Grid, Tooltip, Legend } from './charts';
function Chart({ data }) {
const containerRef = useRef(null);
const chartRef = useRef(null);
useEffect(() => {
if (containerRef.current) {
// Create chart
const chart = new XYChart({ data });
chart
.addChild(new Grid({ horizontal: true }))
.addChild(new XAxis({ dataKey: 'column' }))
.addChild(new YAxis())
.addChild(new Tooltip())
.addChild(new Legend({ position: 'bottom' }))
.addChild(new Line({ dataKey: 'value1' }))
.addChild(new Line({ dataKey: 'value2' }));
chart.render(containerRef.current);
chartRef.current = chart;
return () => {
chart.destroy();
};
}
}, []);
// Update when data changes
useEffect(() => {
if (chartRef.current && data) {
chartRef.current.update(data);
}
}, [data]);
return <div ref={containerRef} />;
}API Reference
XYChart
The main chart class for creating XY-coordinate charts (line, bar, or mixed).
Constructor
new XYChart(config: XYChartConfig)Config Options:
data: DataItem[]- Array of data objects (required)theme?: Partial<ChartTheme>- Theme customizationwidth: number- Chart max-width in pixels (default: 928)height: number- Chart height in pixels (default: 600)margins: { top, right, bottom, left }- Base margins around plot area (default: { top: 20, right: 20, bottom: 30, left: 40 })colorPalette: string[]- Array of colors for auto-assignmentgridColor: string- Grid line color (default: '#e0e0e0')axis: { fontFamily, fontSize }- Axis text styling
scales?: AxisScaleConfig- Scale configurationx?: { type: 'band' | 'linear' | 'time' | 'log', domain?: any[], padding?: number, nice?: boolean }y?: { type: 'band' | 'linear' | 'time' | 'log', domain?: any[], padding?: number, nice?: boolean }
Methods
addChild(component: ChartComponent): this
Adds a component to the chart (chainable).
render(target: string): HTMLElement
Renders the chart to a DOM element specified by CSS selector. Automatically sets up resize handling.
update(data: DataItem[]): void
Updates the chart with new data and re-renders.
destroy(): void
Cleans up all resources, removes resize observer, and clears the chart from the DOM.
Components
Line
Renders a line series on the chart.
new Line({
dataKey: string, // Key in data objects for Y values (required)
stroke? : string, // Line color (auto-assigned if omitted)
strokeWidth? : number, // Line width in pixels (default: 2)
})Bar
Renders a bar series on the chart.
new Bar({
dataKey: string, // Key in data objects for Y values (required)
fill? : string, // Bar color (auto-assigned if omitted)
})XAxis
Renders the X axis.
new XAxis({
dataKey? : string, // Key in data objects for X values (auto-detected if omitted)
})YAxis
Renders the Y axis.
new YAxis({
tickFormat?: string | null, // D3 format specifier (e.g., 's' for SI-prefix like "35k"). Default: null (no formatting)
})Examples:
new YAxis() // Shows raw numbers: 35000
new YAxis({ tickFormat: 's' }) // Shows SI-prefix: 35k
new YAxis({ tickFormat: '$,' }) // Shows formatted: $35,000Grid
Renders grid lines.
new Grid({
horizontal? : boolean, // Show horizontal lines (default: true)
vertical? : boolean, // Show vertical lines (default: true)
})Tooltip
Renders interactive tooltips on hover.
new Tooltip({
formatter? : (dataKey: string, value: any, data: DataItem) => string
})Example formatter:
new Tooltip({
formatter: (dataKey, value, data) =>
`<strong>${dataKey}</strong><br/>Value: ${value}<br/>Date: ${data.date}`
})Legend
Renders a legend for the chart.
new Legend({
position?: 'bottom', // Position (currently only 'bottom' supported)
marginTop?: number, // Space above legend (default: 20)
marginBottom?: number, // Space below legend (default: 10)
})Title
Renders a title for the chart.
new Title({
text: string, // Title text (required)
fontSize?: number, // Font size in pixels (default: 18)
fontWeight?: string, // Font weight (default: 'bold')
align?: 'left' | 'center' | 'right', // Alignment (default: 'center')
marginTop?: number, // Space above title (default: 10)
marginBottom?: number, // Space below title (default: 15)
})Advanced Usage
Custom Scale Types
Use time scales for temporal data:
const chart = new XYChart({
data: [
{ date: new Date('2024-01-01'), value: 100 },
{ date: new Date('2024-01-02'), value: 150 },
],
scales: {
x: { type: 'time', nice: true },
y: { type: 'linear', nice: true },
},
});Use logarithmic scales for exponential data:
const chart = new XYChart({
data: [
{ x: 1, y: 10 },
{ x: 2, y: 100 },
{ x: 3, y: 1000 },
],
scales: {
y: { type: 'log', domain: [1, 10000] },
},
});Custom Theming
const chart = new XYChart({
data,
theme: {
width: 1200, // Max-width (chart won't exceed this)
height: 600,
margins: {
top: 30,
right: 30,
bottom: 40,
left: 60,
},
colorPalette: ['#ff6b6b', '#4ecdc4', '#45b7d1', '#f7b731'],
gridColor: '#333333',
axis: {
fontFamily: 'Inter, sans-serif',
fontSize: '12',
},
},
});Manual Color Assignment
chart
.addChild(new Line({ dataKey: 'revenue', stroke: '#00ff00' }))
.addChild(new Line({ dataKey: 'expenses', stroke: '#ff0000' }));Responsive Charts
Charts automatically resize with their container using ResizeObserver. The chart width adapts to the container up to the theme.width (which acts as max-width).
// Container width: 500px → Chart width: 500px
// Container width: 1200px → Chart width: 928px (theme default max-width)
// Custom max-width
const chart = new XYChart({
data,
theme: { width: 1200 }, // Chart won't exceed 1200px
});No manual resize calls needed - the chart automatically responds to container size changes!
Data Validation
The library includes built-in validation with helpful error messages:
// Empty data
new XYChart({ data: [] });
// Error: Data array cannot be empty
// Missing dataKey
new Line({ dataKey: 'nonexistent' });
// Error: Line: dataKey "nonexistent" not found in data items at indices: 0, 1, 2
// Invalid numeric data
new Line({ dataKey: 'textField' });
// Error: Line: No valid numeric values found for dataKey "textField"Browser Support
Modern browsers with ES6+ support. Uses D3.js v7.
TypeScript
Full TypeScript support included:
import type { DataItem, ChartTheme, LineConfig } from './charts/types';
const data: DataItem[] = [
{ x: 1, y: 100 },
{ x: 2, y: 200 },
];
const config: LineConfig = {
dataKey: 'y',
stroke: '#8884d8',
strokeWidth: 2,
};Architecture
The library follows a composable, layout-driven design:
- BaseChart: Abstract base class providing common functionality (lifecycle, rendering, validation)
- XYChart: Concrete implementation for XY-coordinate charts (lines, bars, or mixed)
- LayoutManager: Calculates component positions and plot area dimensions (D3 margin convention)
- LayoutAwareComponent: Interface for self-measuring components (Title, Legend, Axes)
- Components: Modular components that implement
ChartComponentorLayoutAwareComponent - Validation: Centralized validation layer with
ChartValidator - Scales: Flexible scale factory supporting multiple D3 scale types
Key principles:
- Layout-driven: Components report their space requirements, plot area adjusts automatically
- Separation of concerns: Only the plot area (grid) scales; UI elements stay fixed size
- D3 conventions: Follows D3's margin convention pattern for clean, predictable layouts
This architecture makes it easy to add new chart types or series (Area, Scatter, etc.) by extending BaseChart or implementing new series components.
Performance
- Data Caching: Sorted data is cached to avoid redundant sorting operations
- Smart Re-rendering: Only re-renders when necessary (data updates or container resize)
- Automatic Cleanup: ResizeObserver and tooltips properly cleaned up on destroy
- Minimal DOM Manipulation: Uses D3's efficient data binding
- SVG Optimization: Clean SVG generation with proper cleanup
- Small Bundle: ~105 KB gzipped (including D3)
- Small Bundle: ~105 KB gzipped (including D3)
