npm package: @internetstiftelsen/charts
Description: A framework-agnostic, composable charting library built on D3.js with TypeScript.
Latest version: 0.10.1

# Chart Library

A framework-agnostic, composable charting library built on D3.js with TypeScript.

## Features

- **Framework Agnostic** - Works with vanilla JS, React, Vue, Svelte, or any framework
- **Composable Architecture** - Build charts by composing components
- **Multiple Chart Types** - XYChart (lines, scatter, areas, bars), WordCloudChart, DonutChart, PieChart, and GaugeChart
- **Combined Chart Layouts** - `ChartGroup` composes existing charts into shared dashboards with one coordinated legend
- **Divergent Bar Support** - Bar charts automatically render from zero and diverge around `0` for mixed positive/negative values
- **Mirrored Bar Sides** - Horizontal bars can mirror a series to the left for population-pyramid style charts without changing source data
- **Custom Value Labels** - XY, pie, and donut charts support optional on-chart labels with custom formatters
- **Optional Gauge Animation** - Animate gauge value transitions with `gauge.animate`
- **Stacking Control** - Bar stacking modes with optional reversed visual series order
- **Axis Direction Control** - Use `scales.x.reverse` / `scales.y.reverse` to flip an axis when needed
- **Flexible Scales** - Band, linear, time, and logarithmic scales (bar value axes stay linear)
- **Explicit or Responsive Sizing** - Set top-level `width`/`height` or let the container drive size
- **Auto Resize** - Built-in ResizeObserver handles responsive behavior
- **Responsive Policy** - Chart-level container-query overrides for theme and components
- **Type Safe** - Written in TypeScript with full type definitions
- **Data Validation** - Built-in validation with helpful error messages
- **Auto Colors** - Smart color palette with sensible defaults

## Installation

```bash
npm install @internetstiftelsen/charts
```

## Agent Skill

This repo also ships a Codex-compatible skill in
`skills/build-internetstiftelsen-charts`.

Install it globally for Codex with `skills.sh`:

```bash
npx skills add git@gitlab.com:internetstiftelsen/internal/webbgruppen/packages/charts.git \
  -a codex \
  -g \
  --skill build-internetstiftelsen-charts
```

Restart Codex after installation so the new skill is discovered.

## Local Development

```bash
pnpm dev
```

Runs the interactive demo app (`index.html`) with sidebar controls and
Chart/Data/Showcase tabs.

```bash
pnpm dev:docs
```

Runs the marketing landing page (`docs.html`) built on
`@internetstiftelsen/styleguide`.

## Build Targets

```bash
pnpm build
```

Builds the publishable chart library output into `dist`.

```bash
pnpm build:docs
```

Builds the static marketing site into `dist-docs` (used for Pages deploys).

```bash
pnpm build:demo
```

Builds the demo app using the default Vite config.

## Quick Start

```javascript
import { XYChart } from '@internetstiftelsen/charts/xy-chart';
import { Line } from '@internetstiftelsen/charts/line';
import { XAxis } from '@internetstiftelsen/charts/x-axis';
import { YAxis } from '@internetstiftelsen/charts/y-axis';

const data = [
    { date: '2023', revenue: 100, expenses: 80 },
    { date: '2024', revenue: 150, expenses: 90 },
];

const chart = new XYChart({ data });

chart
    .addChild(new XAxis({ dataKey: 'date' }))
    .addChild(new YAxis())
    .addChild(new Line({ dataKey: 'revenue' }))
    .addChild(new Line({ dataKey: 'expenses' }));

chart.render('#chart-container');
```

Use top-level `width` and `height` for fixed-size charts, or omit them to size
from the render container.

Theme overrides are deep-partial, so nested overrides like
`theme.axis.fontSize` preserve the rest of the theme defaults.

## Chart Groups

Use `ChartGroup` when you want to combine existing charts into one layout while
keeping each child chart fully functional.

```javascript
import { ChartGroup } from '@internetstiftelsen/charts/chart-group';
import { XYChart } from '@internetstiftelsen/charts/xy-chart';
import { Line } from '@internetstiftelsen/charts/line';
import { Bar } from '@internetstiftelsen/charts/bar';
import { Legend } from '@internetstiftelsen/charts/legend';
import { Title } from '@internetstiftelsen/charts/title';

const lineChart = new XYChart({ data: lineData });
lineChart.addChild(new Line({ dataKey: 'revenue' }));

const barChart = new XYChart({ data: barData });
barChart.addChild(new Bar({ dataKey: 'revenue' }));

const group = new ChartGroup({
    cols: 2,
    gap: 20,
    height: 420,
    syncY: true,
});

group
    .addChild(new Title({ text: 'Revenue vs Expenses' }))
    .addChart(barChart)
    .addChart(lineChart)
    .addChild(new Legend());

group.render('#chart-group');
```

Legend state now works even without mounting a `Legend` on each child chart, so
grouped charts can share one coordinated legend while preserving child tooltip,
axis, and responsive behavior. `ChartGroup.height` behaves like chart height:
set it for a fixed total group height, or omit it to size from the render
container. Set `syncY: true` when you want vertical `XYChart` children to share
the same Y domain so grid lines stay aligned while only one child renders a
visible Y axis.

`ChartGroup` also supports declarative responsive layout overrides. Group
breakpoints can change `cols` and `gap`, while `addChart(..., options)` can
override `span`, `height`, `order`, or `hidden` per child. Just like chart
responsive config, both `minWidth` and `maxWidth` are supported and all
matching breakpoints merge in declaration order:

```typescript
const group = new ChartGroup({
    cols: 3,
    responsive: {
        breakpoints: {
            tablet: { maxWidth: 1023, cols: 2 },
            mobile: { maxWidth: 640, cols: 1, gap: 16 },
        },
    },
});

group
    .addChart(barChart, {
        responsive: {
            breakpoints: {
                mobile: { maxWidth: 640, hidden: true },
            },
        },
    })
    .addChart(lineChart, {
        span: 2,
        responsive: {
            breakpoints: {
                mobile: { maxWidth: 640, span: 1, order: -1 },
            },
        },
    });
```

## Divergent Bars

Bar charts always render from a zero baseline. When bar data contains both
positive and negative values, the bars automatically diverge around `0` in both
vertical and horizontal orientations.

```javascript
import { XYChart } from '@internetstiftelsen/charts/xy-chart';
import { Bar } from '@internetstiftelsen/charts/bar';
import { XAxis } from '@internetstiftelsen/charts/x-axis';
import { YAxis } from '@internetstiftelsen/charts/y-axis';

const chart = new XYChart({
    data: [
        { metric: 'Pricing', delta: -18 },
        { metric: 'Feature set', delta: 24 },
        { metric: 'Support', delta: 11 },
    ],
    orientation: 'horizontal',
});

chart
    .addChild(new XAxis({ dataKey: 'metric' }))
    .addChild(new YAxis())
    .addChild(new Bar({ dataKey: 'delta' }));
```

Automatic numeric bar domains always include `0`. If you configure an explicit
numeric domain or `min`/`max` for the bar value axis, that final domain must
still include `0`.

Categorical `y` axes now preserve data order from top to bottom by default.
Use `scales.x.reverse` or `scales.y.reverse` when you want to intentionally flip
an axis direction.

Responsive overrides are declarative and merge all matching breakpoints in
declaration order:

```typescript
const chart = new XYChart({
    data,
    responsive: {
        breakpoints: {
            sm: {
                maxWidth: 640,
                theme: {
                    axis: {
                        fontSize: 11,
                    },
                },
                components: [
                    {
                        match: { type: 'xAxis' },
                        override: { display: false },
                    },
                ],
            },
            md: {
                minWidth: 641,
                maxWidth: 768,
                theme: {
                    axis: {
                        fontSize: 12,
                    },
                },
            },
        },
    },
});
```

## Word Cloud

```javascript
import { WordCloudChart } from '@internetstiftelsen/charts/word-cloud-chart';

const data = [
    { word: 'internet', count: 96 },
    { word: 'social', count: 82 },
    { word: 'news', count: 75 },
];

const chart = new WordCloudChart({
    data,
    wordCloud: {
        minValue: 5,
        minWordLength: 3,
        minFontSize: 3,
        maxFontSize: 20,
        padding: 1,
        spiral: 'archimedean',
    },
});

chart.render('#word-cloud');
```

`minFontSize` and `maxFontSize` are percentages of the smaller plot-area
dimension and define the relative size range passed into `d3-cloud`. The chart
expects flat `{ word, count }` rows, aggregates duplicate words after trimming,
and maps theme typography and colors directly into the layout and rendered SVG.

## Export

`chart.export()` supports `svg`, `json`, `csv`, `xlsx`, `png`, `jpg`, and `pdf`.

```javascript
await chart.export('png', { download: true });
await chart.export('csv', { download: true, delimiter: ';' });
await chart.export('xlsx', { download: true, sheetName: 'Data' });
await chart.export('pdf', { download: true, pdfMargin: 16 });
```

`xlsx` and `pdf` are lazy-loaded and require optional dependencies (`xlsx` and
`jspdf`) only when those formats are used.

## Import

`toChartData()` converts tab-delimited string input into chart JSON data.
It auto-detects grouped and normal (flat) table layouts.

```typescript
import { toChartData } from '@internetstiftelsen/charts/utils';
import { XYChart } from '@internetstiftelsen/charts/xy-chart';

const data = toChartData('\t\tDaily\tWeekly\nAll users\tSegment A\t85%\t92%\n\tSegment B\t84%\t91%', {
    categoryKey: 'Category',
});

const chart = new XYChart({ data });
chart.render('#chart-container');
```

The parser supports JSON-escaped string payloads and grouped carry-forward row
structure (blank first column on continuation rows).

Supported input shapes:

- Plain tab-delimited strings
- JSON-escaped string payloads

Auto-detection behavior:

- Grouped rows when a carry-forward group structure is present
- Flat rows when no grouped continuation rows are detected

Grouped parsing rules:

- Header row starts with two structural columns (`group`, `category`) before metrics
- Continuation rows leave the first column blank to inherit the previous group
- Blank separator rows are ignored

## Documentation

- [Getting Started](./docs/getting-started.md) - Installation, Vanilla JS, React integration
- [XYChart](./docs/xy-chart.md) - Line, area, and bar charts API
- [ChartGroup](./docs/chart-group.md) - Combined chart layouts with a shared legend
- [WordCloudChart](./docs/word-cloud-chart.md) - Word frequency visualization API
- [DonutChart](./docs/donut-chart.md) - Donut/pie charts API
- [PieChart](./docs/pie-chart.md) - Pie chart API
- [GaugeChart](./docs/gauge-chart.md) - Gauge chart API
- [Components](./docs/components.md) - Axes, Grid, Tooltip, Legend, Title
- [Theming](./docs/theming.md) - Colors, fonts, and styling

## Browser Support

Modern browsers with ES6+ support. Uses D3.js v7.

## License

MIT