expressive-charts 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,74 @@
1
+ # Changelog
2
+
3
+ Each release is listed here, newest first. [The development log](https://github.com/Deaversbp/expressive-charts/blob/main/docs/plan.md) records what each milestone built, and why.
4
+
5
+ ## The 0.x promise
6
+
7
+ The library follows [semantic versioning](https://semver.org) from 0.1.0, with this promise for the 0.x releases.
8
+
9
+ **The effect-author API** is:
10
+
11
+ - `defineEffect` and `defineDataEffect`, with `GeometryEffectSpec` and `DataEffectSpec`;
12
+ - `matrix` and `offset`;
13
+ - `describe.bar`, `describe.arc`, `describe.line` and `describe.point`, with their specs: `BarSpec`, `BarSegmentSpec`, `Baseline`, `ArcSpec`, `LineSpec` and `PointSpec`;
14
+ - the effect contracts (`GeometryEffect`, `DataEffect`, `TransitionEffect`, `MarkEffect`, `AnyMarkEffect`, `Combined`), the contexts (`EffectContext`, `TransitionContext`), the mark and its data (`MarkGeometry`, `Outline`, `Point`, `Rect`, `MarkData`, `Sample`), the drawing API (`Layer`, `MarkHandle`, `MarkStyle`, `Matrix`, `Paint`, `TextStyle`, `Color`, and the handles `IconHandle`, `PathHandle` and `TextHandle`), and icons as an effect gets them (`Icon`, `IconShape`, `IconSource`);
15
+ - the motion types the context uses: `MotionView`, `MotionMode`, `Timing`, `Tween`, `TweenOptions`, `TweenResult` and `Easing`;
16
+ - `expressive-charts/testing`: `checkEffect`, `checkTransition`, `fixtures`, `fixturePairs`, `testContext` and `play`, with their options (`CheckEffectOptions`, `CheckTransitionOptions`, `TestContextOptions`, `PlayOptions`), the fixtures' names (`FixtureName`, `FixturePairName`), the test context (`TestContext`) and what it records (`RecordingLayer`, `RecordingMark`, `Drawn`, `DrawnIcon`, `DrawnPath`, `DrawnText`, `LayerCall`, `MarkCall`, `Picture`), and the motion types they use: `Motion`, `Clock`, `ManualClock`, `ScalingOverrides` and `ScalingRule`.
17
+
18
+ That's every type these names use, so code written against them needs nothing outside the list. A test imports every name in it, so none can be renamed or removed by accident.
19
+
20
+ From 0.1.0:
21
+
22
+ - **A breaking change to that API comes only in a minor release** (0.1 → 0.2), never in a patch, and this file lists it with a migration note: what changed, and what to write instead.
23
+ - **A deprecated name keeps working for one minor release.** It's marked `@deprecated` with its replacement, and removed in the minor release after that.
24
+ - Additions (a new optional field, a new helper) can come in any release.
25
+
26
+ Everything else can still change in a minor release while the library is 0.x: the motion core (`motion`, `createMotion`, the clocks, `tween`, `animated`, `playback`, and the types the list doesn't name), the charts, the adapters, the built-in effects and the controls. Those changes are listed here too. [docs/effects-guide.md](https://github.com/Deaversbp/expressive-charts/blob/main/docs/effects-guide.md) is the guide to the author API.
27
+
28
+ ## 0.1.0 (24 October 2026)
29
+
30
+ The first release, published to npm from CI with provenance. Everything in it is new. [docs/api.md](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md) documents every export.
31
+
32
+ ### What's in it
33
+
34
+ - **The motion core.** One clock and three modes: Presentation (data transitions staged over about a second), Analysis (at most about 200 ms, unstaged) and Off. The device's reduced-motion setting turns motion off unless the viewer picks a mode on the page; a mode the page sets in code never overrides it. With motion off, every animation jumps to its end state. `motion` and `createMotion`, the clocks (`defaultClock`, `createClock`, `manualClock`), `tween`, `animated` with its interpolators, the easings, and `playback`, which plays a value through a range.
35
+ - **Bar, donut and line charts.** `barChart` draws bars or columns, plain or stacked; `donutChart` shares of a total; `lineChart` a line per series, on a number axis or a time axis. Each sizes itself to its container, follows the light or dark theme, gives every mark a key, and has a tooltip, a legend, a data table and keyboard focus, with announcements as an option. Colours follow the entity, not its rank (`colorAssignment`). The handle has `update()`, which animates a change by the motion mode (in Presentation, values first and then positions), `on()` for mark events, `marks()`, `focus(key)`, `leave()`, `labels()` and `destroy()`.
36
+ - **Switching between charts.** `chartSwitch` shows views of the same data in one place, and turns each mark into its partner in the next with `morph()`.
37
+ - **Adapters.** `expressive()` brings the same effects, keyboard focus and names to SVG marks drawn by D3, Observable Plot or by hand. It draws on an overlay and leaves the host's styling alone. `fromVegaView()` does the same for Vega and Vega-Lite charts rendered as SVG, and `samplesAlong()` gives data effects their samples on such marks.
38
+ - **Effects.** `travel` carries an icon along a mark, rolling, stepping or sliding (`gaits`); `flash`, `wiggle` and `pulse` draw attention without changing a value; `threshold` colours a mark where it's below or above a reference; `morph` turns one mark into another. `sequence`, `parallel`, `stagger` and `only` put effects together. Each goes in an effect slot (`hover`, `enter`, `update`, `exit`, `decorate`), as an effect or a preset's name, and `registerPreset` adds names.
39
+ - **Icons.** A neutral core set in theme colours (`icons`: a dot, an arrow and a chevron), `defineIcon` and `iconFromSvg` for your own, `registerIcon` and `registerIcons` to use them by name, and a ball and a boot in `expressive-charts/icons/sport`.
40
+ - **Controls.** `motionSwitch()`, the viewer's own choice of mode, kept across pages if you ask; `playButton()`, the Play/Pause button for a playback (WCAG 2.2.2).
41
+ - **Styling.** One injected stylesheet, or `expressive-charts/style.css` as a file; `--ec-*` custom properties for theming; and `configure()` for a Content-Security-Policy nonce, or for injecting nothing.
42
+ - **The effect-author API**, under the 0.x promise above: `defineEffect`, `defineDataEffect`, `describe`, `matrix`, `offset` and the contracts, with `expressive-charts/testing` (`checkEffect`, `checkTransition`) to check an effect on every kind of mark.
43
+ - **Accessibility.** Each chart is a named group and each mark one named image. A data table follows every chart, keyboard focus plays the hover effects, Escape ends a hover, and nothing plays by itself. The demo pages are checked against WCAG 2.2 AA, with axe's rules in Chromium, Firefox and WebKit, and with screen-reader scripts for NVDA + Chrome.
44
+ - **Packaging.** ES modules and CommonJS, each with its own types, which need TypeScript 5.4 or newer. No runtime dependencies and no side effects in its JavaScript, so a bundler keeps only what a page imports. A script-tag bundle defines one global, `ExpressiveCharts`. Node.js 22 or newer imports it with no DOM.
45
+
46
+ ### In more detail
47
+
48
+ What each part does, and behaviour worth knowing:
49
+
50
+ - **The effect-author API** ([guide](https://github.com/Deaversbp/expressive-charts/blob/main/docs/effects-guide.md)). `defineEffect(spec)` and `defineDataEffect<D>(spec)` make an effect from its `run` and `end`: they fill in the kind and a `clear()` that does nothing, throw a `TypeError` without `run` or `end` (every effect needs an end state), and freeze the effect. A data effect's spec is checked against its row type strictly.
51
+ - **`expressive-charts/testing`**, a separate entry with the conformance suite. `checkEffect(effect, { marks, icons, data, batch, moved, name })` runs five checks on every mark (a full run ends as `end()` draws; clearing leaves nothing; an abort settles; with motion off no frames run; after a run or `end()`, a `change()` to the mark moved somewhere else leaves what `end()` draws there) and throws one `Error` listing every failure, in any test runner. The fixtures have moved twins for the fifth check; a page's own marks pass `moved(mark)` or skip it. With it: `fixtures` (one of each kind of mark), `testContext` and `play`.
52
+ - **`threshold({ compareTo, when, color })`** is the built-in data effect. It colours the parts of a mark's walk where its values are below a constant or a related series (`when: "above"` for above), in `"negative"` by default, with crossings interpolated between samples. A series is matched by `at` where both have it, else by `t`, and goes in `needs.related`. It draws at once, with no animation, and redraws on every change, so it belongs in `on.decorate`.
53
+ - **`samplesAlong(mark, points)`** gives data effects their samples on marks the library didn't draw. It projects each point `{ x, y, value, at? }`, in the marks' coordinates, onto the nearest point of the mark's walk and returns the samples in `t` order; a point on the walk, such as a vertex of the line it drew, lands exactly there. It pairs with `expressive()`'s `samples` option, and its points' type is `SamplePoint`.
54
+ - **`expressive()`'s `watch` option** follows the host's own transitions frame by frame, without `refresh()`. A `MutationObserver` notes the marks' geometry attributes, and `transform` and `style` on their ancestors up to the `<svg>`; once a frame, only the marks that moved are read again (their rows, geometry and samples), and their effects follow with no events. `refresh()`, called when the host joins new data, still fires `enter`, `update` and `exit`. It's opt-in: its cost grows with the number of marks that move each frame.
55
+ - **Transition effects.** `TransitionEffect`'s `run(from, to, ctx)` and `end(from, to, ctx)` take a `TransitionContext`: an `EffectContext` with `start`, from 0 to 1, so a run can begin part way and a switch back mid-way retraces. Its `mark` does nothing and its `onChange` never fires, since both views' marks are hidden while a transition plays.
56
+ - **`checkTransition(effect, { pairs, icons, name })`** in `expressive-charts/testing` checks a transition effect on pairs of marks, both ways: checks 1 to 4 of `checkEffect`, a run from `start: 0.5`, and that a run back from `start: 1 − p` begins with the picture the run forward draws at p. Its default pairs are `fixturePairs`: a slice and a column, a bar and a column, two slices, two lines and two points.
57
+ - **`morph({ points, align })`** is the built-in transition effect. It samples two closed outlines at `points` (default 160) and moves each point in a straight line to its partner, as one filled path; two lines are sampled along their strokes, their width moving between thicknesses; a closed and an open outline crossfade. `align: "unroll"` matches each side with the same side, and `"drop"` turns the matching by a side from the mark that goes round its pivot, so a slice's outer arc meets a column's top. The default is `"unroll"`, picked by comparing the two in the morph demo. 950 ms base with `transition` timing, eased in and out; a run from `start` takes the rest of the time.
58
+ - **`chartSwitch(target, { views, initial, transition, colors, announce })`** shows views of the same data in one place, one at a time, and turns each mark into the mark with its key in the next with a transition effect, `morph()` by default. Each view is a function that draws its chart in the container it's given, with the switch's `ColorAssignment`. Labels move between their places and swap their text half way, unmatched marks fade, and the old view's other parts fade out as the new one's fade in. A `show()` back mid-way reverses from where the shapes are. Only the shown view can be seen, reached or read, focus on a mark follows its key, and `announce` says the new view's label. It returns `{ show(view), current, chart(view), destroy() }`.
59
+ - **Dates on `lineChart`'s x.** An `x` that gives `Date`s makes a time axis, with no dependency. Its ticks fall on second, minute, hour, day, week, month and year boundaries, at both ends and a step of about a quarter of the range, each worded for its level in the page's locale through `Intl.DateTimeFormat` ("2026", "Apr", "Feb 8", "3:00 PM"). Each x is said as a medium date ("Jan 5, 2026", with the time when any x has one) in its name, the tooltip's title, the table's row headers and announcements, and `xFormat(x)` words it instead. `timeZone` is `"UTC"` (the default) or `"local"`. A date's key is its ISO string (`goals@2026-01-05T00:00:00.000Z`), samples' `at` is a `Date`, and `reveal()` takes a `Date` or its time in ms. Numbers and dates together throw a `TypeError`, as does an invalid `Date`. `LineChartOptions.x` is `Accessor<D, number | Date>`.
60
+ - **`update({ on })`** swaps a chart's effects in place, on the bar, donut and line charts: a changed slot's runs are cleared and the new effect starts where they were. `decorate` starts again on every mark, a held hover replays with the new hover effect once the change settles, and `enter`, `update` and `exit` draw the new effect's end state, so nothing enters again. Focus stays, and a slot given the same effect or preset name keeps its runs. An unknown preset name rejects before anything changes.
61
+ - **`leave()`** on every chart handle ends its hover, pointer or keyboard, as the viewer moving away would, and fires `leave`. Focus stays where it is. A chart switch ends the old view's hover with it, so `ChartSwitchView` has `leave()` too.
62
+ - **`table.series`** heads each series' column in the data table, `{ shots: "Shots" }`, defaulting to its `seriesLabels` name. It names a donut's value column without renaming its slices or its centre.
63
+ - **`fromVegaView(view, options)`** attaches effects to a Vega or Vega-Lite chart rendered as SVG. It reads the named mark (`mark`, Vega-Lite's `"marks"` by default) from the View's scenegraph: rects are bars (stacks too), arcs slices, symbols points and lines lines. `sample(row)` gives data effects each item's value and `at`. It takes `expressive()`'s other options, and follows the View's own renders, new data or a resize, with no `refresh()` to call. Duck-typed on the View: the library imports nothing from Vega, which stays a dev dependency.
64
+ - **Icons from icon sets.** `iconFromSvg` keeps `polyline` beside `path`, `circle`, `rect`, `ellipse`, `line` and `polygon`, drawn as a polygon left open, and `currentColor`, in any case, becomes the `text` token. So an icon copied from a set such as Lucide, Heroicons or Tabler follows the chart's theme, not the page's `color`. Fill rules and opacity aren't kept, so a solid icon's holes can fill in. The Icons section of docs/api.md has a recipe.
65
+ - `sequence` starts each part on the mark's geometry and data as they are when that part starts, not as they were when the sequence started, so a part after a travel follows a line that grew meanwhile.
66
+ - `onChange` listeners in the `update` slot get data that keeps the previous values the run was given, so `stagger` and `sequence` keep them when they start an effect on the latest data.
67
+ - `expressive()`'s hovered mark under `dim: true` takes its strong colour as our charts' marks do: its colour stepped a quarter of the way toward `--ec-text` (in OKLab), so lighter in the dark theme. `emphasis` on host marks mixes toward the same colour.
68
+ - `expressive()` reads a `<rect>` through its parent's coordinates and its own `transform` attribute, not through its own screen CTM, so the CSS transform an effect gives a bar is never read back as geometry. A CSS transform a host puts on the rect element itself isn't seen either (on its groups, it is).
69
+ - **CommonJS has its own types.** Each entry's `require` condition points at `.d.cts` declarations, so a CommonJS project resolving with `node16` is given CommonJS types for the `.cjs` file it loads. `import` has the `.d.ts` files.
70
+ - The types need TypeScript 5.4 or newer, since they use `NoInfer`. Under the old `node10` resolution, TypeScript can't find the subpath entries' types.
71
+ - The package includes this changelog, where the promise is.
72
+ - **Escape dismisses a pointer's hover.** While something is hovered, Escape anywhere on the page ends the hover and hides the tooltip, as `leave()` does, so the tooltip can be dismissed without moving the pointer (WCAG 1.4.13). An Escape the page has already handled is left alone.
73
+ - With `markLabel`, `expressive()` takes an `img`, `graphics-symbol` or `graphics-object` role (and its `aria-roledescription`) off unnamed host elements around the marks it names, as Vega gives the groups round its lines, so each mark is read once, by its name. `destroy()` puts them back.
74
+ - Under forced colours (Windows contrast themes), the motion switch's pressed mode shows in `Highlight` and `HighlightText`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Deaversbp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,251 @@
1
- # Temporary Holding Version
1
+ # expressive-charts
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Composable motion and expressive effects for web charts. Hover, update and morph animations that work on bar, line and donut charts, including SVG charts you've already built with D3 or Observable Plot, and Vega or Vega-Lite charts rendered as SVG. Presentation, analysis and reduced-motion modes are built in.
4
+
5
+ <table>
6
+ <tr>
7
+ <td width="50%" valign="top"><a href="https://deaversbp.github.io/expressive-charts/bars.html"><img src="https://raw.githubusercontent.com/Deaversbp/expressive-charts/main/docs/media/arrow-on-hover.gif" width="100%" alt="A bar chart of revenue by region. The pointer moves onto North's bar, the other bars dim, and an arrow runs along it to its end. Then the same on East's bar."></a><br><code>travel(icons.arrow)</code> on hover, from the <a href="#quick-start">quick start</a></td>
8
+ <td width="50%" valign="top"><a href="https://deaversbp.github.io/expressive-charts/bars.html"><img src="https://raw.githubusercontent.com/Deaversbp/expressive-charts/main/docs/media/ball-and-boot.gif" width="100%" alt="Stacked bars of goals and assists by player. On Tanaka's bar a ball rolls along the goals and a boot steps along the assists, then the same on Lindqvist's."></a><br>A <code>sequence</code> of two <code>travel</code>s on stacked bars</td>
9
+ </tr>
10
+ <tr>
11
+ <td width="50%" valign="top"><a href="https://deaversbp.github.io/expressive-charts/line.html"><img src="https://raw.githubusercontent.com/Deaversbp/expressive-charts/main/docs/media/threshold-line.gif" width="100%" alt="A line chart of goals against expected goals. Play is pressed and the season draws week by week: the goals line is red wherever it's below the dashed xG line, and a ball rides its tip."></a><br><code>threshold</code> and a riding <code>travel</code>, played by <code>playback()</code></td>
12
+ <td width="50%" valign="top"><a href="https://deaversbp.github.io/expressive-charts/morph.html"><img src="https://raw.githubusercontent.com/Deaversbp/expressive-charts/main/docs/media/donut-to-columns.gif" width="100%" alt="A donut chart of shot outcomes. Show as columns is clicked, and each slice morphs into its own column, keeping its colour."></a><br><code>chartSwitch</code> and <code>morph()</code>, from a donut to columns</td>
13
+ </tr>
14
+ </table>
15
+
16
+ Each picture plays once and stops where its effect ends. **[The demo site](https://deaversbp.github.io/expressive-charts/)** has every effect to try, on the library's charts and on D3, Vega-Lite and hand-written ones, with the motion modes and reduced motion.
17
+
18
+ > **Status: 0.1.0, the first release (October 2026).** The effect-author API is under [the 0.x promise](#the-0x-promise); the charts, adapters, built-in effects and motion core can still change in a minor release. [CHANGELOG.md](https://github.com/Deaversbp/expressive-charts/blob/main/CHANGELOG.md#010-24-october-2026) lists what's in it.
19
+
20
+ - **Effects first.** The mark effects `travel`, `flash`, `wiggle`, `pulse` and the data effect `threshold`, put together with `sequence`, `parallel`, `only` and `stagger`; and the transition effect `morph`, which `chartSwitch` plays to turn each mark of one chart into its mark in the next (a transition can't go into a combinator). An effect reads only a shared description of the mark, so one effect works on every chart, and effects of your own work everywhere the built-in ones do.
21
+ - **Charts and adapters.** Bar, donut and line charts, a switch that morphs one into another, and adapters that bring the same effects to SVG marks drawn by other code.
22
+ - **Motion that stays readable.** One clock and three modes: Presentation (staged, about a second), Analysis (at most about 200 ms) and Off. The device's reduced-motion setting is followed unless the viewer picks a mode on the page, and every effect has an end state, which is what reduced motion shows.
23
+ - **Accessible.** Keyboard focus, a name for every mark, a data table, announcements and a pause control for anything that plays, checked against WCAG 2.2 AA.
24
+
25
+ ## Install
26
+
27
+ ```sh
28
+ npm install expressive-charts
29
+ ```
30
+
31
+ It has no runtime dependencies. It's an ES module and CommonJS, each with its own types, and its JavaScript has no side effects, so a bundler keeps only what a page imports: a bar chart is about 28 KB gzipped, and everything 65 KB.
32
+
33
+ On a page with no bundler, the script-tag bundle defines one global, `ExpressiveCharts`, holding the main entry:
34
+
35
+ ```html
36
+ <div id="sales"></div>
37
+ <script src="https://cdn.jsdelivr.net/npm/expressive-charts@0.1/dist/expressive-charts.global.js"></script>
38
+ <script>
39
+ const { barChart } = ExpressiveCharts;
40
+ barChart("#sales", {
41
+ label: "Revenue by region",
42
+ data: [{ region: "North", revenue: 42 }, { region: "South", revenue: 35 }],
43
+ y: "region",
44
+ x: "revenue",
45
+ on: { hover: "wiggle" }
46
+ });
47
+ </script>
48
+ ```
49
+
50
+ ## Quick start
51
+
52
+ A bar chart where an arrow runs out a bar on hover, and the bars an update changes blink:
53
+
54
+ ```js
55
+ import { barChart, icons, travel } from "expressive-charts";
56
+
57
+ const chart = barChart("#sales", {
58
+ label: "Revenue by region", // the chart's accessible name
59
+ data: regions, // [{ region: "North", revenue: 42 }, …]
60
+ y: "region", // not numbers, so it's the category
61
+ x: "revenue",
62
+ sort: "value",
63
+ on: {
64
+ hover: travel(icons.arrow), // a core icon, in the theme's colours
65
+ update: "flash" // a preset: the bars that changed blink
66
+ }
67
+ });
68
+ await chart.update({ data: nextQuarter }); // values change, then rows move, by the motion mode
69
+ ```
70
+
71
+ `travel` takes any icon: the core `dot`, `arrow` and `chevron`, the sport pack's ball and boot, or [an SVG icon from a set such as Lucide or Heroicons](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md#an-icon-from-an-icon-set). With the sport pack, a stacked bar tells a story on hover:
72
+
73
+ ```js
74
+ import { barChart, sequence, travel } from "expressive-charts";
75
+ import { cleat, soccerBall } from "expressive-charts/icons/sport";
76
+
77
+ barChart("#goals", {
78
+ label: "Goals and assists by player",
79
+ data: players, // [{ name: "Okafor", goals: 16, assists: 4 }, …]
80
+ y: "name",
81
+ stack: ["goals", "assists"],
82
+ on: { hover: sequence(travel(soccerBall, { along: "goals" }), travel(cleat, { along: "assists" })) } // a ball rolls out the goals, then a boot steps along the assists
83
+ });
84
+ ```
85
+
86
+ The same story on bars you already draw with D3:
87
+
88
+ ```js
89
+ import { expressive, sequence, travel } from "expressive-charts";
90
+ import { cleat, soccerBall } from "expressive-charts/icons/sport";
91
+
92
+ // bars: your D3 selection of the stacked <rect>s. d3.stack() keeps a series' key on its <g>, so copy it onto each point:
93
+ // .data((series) => series.map((d) => Object.assign(d, { key: series.key }))).
94
+ const fx = expressive(bars, {
95
+ key: (d) => d.data.name, // a player's rects are one bar...
96
+ segment: (d) => d.key, // ...and its series are the bar's segments
97
+ value: (d) => d[1] - d[0], // what each rect shows, so a resize isn't an update
98
+ keyboard: true, // one tab stop; the arrow keys move between bars
99
+ markLabel: (d) => `${d.data.name}: ${d.data.goals} goals, ${d.data.assists} assists`,
100
+ on: { hover: sequence(travel(soccerBall, { along: "goals" }), travel(cleat, { along: "assists" })) }
101
+ });
102
+ fx.refresh(bars); // after your chart redraws
103
+ ```
104
+
105
+ A line that turns red where goals fall below expected goals, with a ball riding its tip, and a season played with a pause control:
106
+
107
+ ```js
108
+ import { lineChart, only, parallel, playButton, playback, threshold, travel } from "expressive-charts";
109
+ import { soccerBall } from "expressive-charts/icons/sport";
110
+
111
+ const xg = lineChart("#xg", {
112
+ label: "Goals vs expected goals",
113
+ data: weeks, // [{ week: 1, goals: 0, xg: 1.2 }, …]: one row per x, a number or a Date
114
+ x: "week",
115
+ series: ["goals", "xg"],
116
+ legend: { entries: [{ label: "Goals below xG", color: "negative" }] },
117
+ on: { decorate: only("goals", parallel(threshold({ compareTo: "xg" }), travel(soccerBall, { ride: "on" }))) }
118
+ });
119
+ const season = playback({ from: 1, to: 38, duration: 6500, onFrame: (week) => xg.reveal(week) }); // 6.5 s; 1.95 s in Analysis
120
+ playButton("#play", season, { labels: { play: "Play season" } });
121
+ ```
122
+
123
+ ## What's in it
124
+
125
+ [docs/api.md](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md) documents every export, with its options, their defaults and what it returns.
126
+
127
+ - **Charts.** [`barChart`](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md#barchart) draws bars or columns, plain or stacked; [`donutChart`](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md#donutchart) shares of a total; [`lineChart`](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md#linechart) a line per series, on a number axis or, when `x` gives `Date`s, a time axis in UTC (or `timeZone: "local"`) worded in the page's locale. Each sizes itself to its container, follows the light or dark theme, and has a tooltip, a legend, a data table and keyboard focus. Its handle has `update()`, which animates a change by the motion mode, and `update({ on })` swaps its effects in place; `focus(key)`; `leave()`, which ends its hover; and `labels()`. `table.series` heads a series' column in the data table.
128
+ - **Switching.** [`chartSwitch`](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md#chartswitch) shows views of the same data one at a time, and turns each mark into its own mark in the next with [`morph()`](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md#morph): a donut becoming columns, and back from wherever it is.
129
+ - **Adapters.** [`expressive()`](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md#expressive) reads keys, rows and geometry from SVG marks drawn by D3, Observable Plot or by hand, and draws on an overlay, leaving the host's styling alone; `watch: true` follows the host's own transitions. Observable Plot binds each rect to its row's index, so pass `datum: (el) => rows[el.__data__]`; Plot draws a new `<svg>` each time, so pass `refresh()` the new one's rects. The Plot accessors are tested against Plot 0.6.17. [`fromVegaView()`](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md#fromvegaview) does the same for a Vega or Vega-Lite View rendered as SVG, and follows its renders.
130
+ - **Effects.** [`travel`](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md#travel) carries an icon along a mark; [`flash`](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md#flash), [`wiggle`](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md#wiggle) and [`pulse`](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md#pulse) draw attention without changing a value; [`threshold`](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md#threshold) colours the parts of a mark below or above a reference. Each goes in an effect slot (`hover`, `enter`, `update`, `exit`, `decorate`), as an effect or a preset's name (`"wiggle"`).
131
+ - **Controls.** [`motionSwitch()`](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md#motionswitch) is the viewer's own choice of mode. [`playback()`](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md#playback) plays a value through a range, and [`playButton()`](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md#playbutton) is its Play/Pause button.
132
+ - **The motion core.** [`motion`](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md#motion), [`tween()`](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md#tween) and [`animated()`](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md#animated), for animations of your own that follow the same modes.
133
+
134
+ ### Writing effects
135
+
136
+ [`defineEffect`](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md#defineeffect) takes a `run` that animates the mark and an `end` that draws where the run ends, which is all reduced motion shows. Both read only the mark's shared description, so this underline works along a bar, round a donut slice or along a line, on our charts and through `expressive()`:
137
+
138
+ ```js
139
+ import { barChart, defineEffect } from "expressive-charts";
140
+
141
+ const underline = defineEffect({
142
+ async run(mark, ctx) {
143
+ const line = ctx.layer.path(mark.slice(0, 0), { stroke: "text-muted", width: 2 });
144
+ let drawn = 0;
145
+ ctx.onChange((next) => line.update((mark = next).slice(0, drawn))); // follows the mark when it moves
146
+ await ctx.tween({ duration: 300, timing: "effect", update: (p) => line.update(mark.slice(0, (drawn = p))) }).finished;
147
+ },
148
+ end(mark, ctx) {
149
+ const line = ctx.layer.path(mark.slice(0, 1), { stroke: "text-muted", width: 2 });
150
+ ctx.onChange((next) => line.update(next.slice(0, 1)));
151
+ }
152
+ });
153
+ barChart("#sales", { label: "Revenue by region", data: regions, y: "region", x: "revenue", on: { hover: underline } });
154
+ ```
155
+
156
+ `expressive-charts/testing` has the conformance suite the built-in effects pass, for any test runner: `await checkEffect(underline)` throws one `Error` listing every check that failed, on every kind of mark. [The effects guide](https://github.com/Deaversbp/expressive-charts/blob/main/docs/effects-guide.md) covers the mark, drawing, timing, end states, data effects, composing, testing and transition effects.
157
+
158
+ ## Entries
159
+
160
+ | Import | What it is |
161
+ | --- | --- |
162
+ | `expressive-charts` | The charts, adapters, effects, controls, icons, geometry and motion core. |
163
+ | `expressive-charts/icons/sport` | A ball and a boot for `travel`: `soccerBall`, `cleat`, and `sport` for `registerIcons`. |
164
+ | `expressive-charts/testing` | The conformance suite for effects: `checkEffect`, `checkTransition`, their fixtures, `testContext` and `play`. |
165
+ | `expressive-charts/style.css` | The stylesheet the charts inject, as a file, for a page that loads it itself. |
166
+
167
+ The script-tag bundle, `dist/expressive-charts.global.js`, is the main entry alone.
168
+
169
+ ## Browsers and TypeScript
170
+
171
+ - **Browsers.** Current Chrome and Edge, Firefox, and Safari. Every DOM check in the test suite (names, roles, tab order, focus rings, tooltips, keys, switches and reduced motion), with axe's accessibility rules, runs in Chromium, Firefox and WebKit, in Playwright 1.64; the screenshots are Chromium's. That's each browser's engine, not a device: Safari on macOS and iOS hasn't been run itself. The build targets ES2020.
172
+ - **TypeScript 5.4 or newer**, since the types use `NoInfer`. The packed package is checked under `bundler` and `node16` resolution, as an ES module and as CommonJS. Under the old `node10` (`"node"`), TypeScript can't find the subpath entries' types.
173
+ - **Node.js 22 or newer** to import it on a server: nothing touches the DOM until a chart, an adapter or a control is made, and those need one.
174
+
175
+ ## Content-Security-Policy
176
+
177
+ The first chart, adapter or control on a page injects one `<style>`; everything else is set through the CSSOM, which a strict policy allows, and the library has no `eval`. Under a policy with a `style-src` nonce, give the stylesheet your nonce. Under one with no inline styles at all, load the stylesheet as a file and inject nothing. Call [`configure()`](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md#configure) before creating charts:
178
+
179
+ ```js
180
+ import { configure } from "expressive-charts";
181
+ configure({ nonce: cspNonce }); // the injected <style> takes it
182
+ ```
183
+
184
+ ```js
185
+ import { configure } from "expressive-charts";
186
+ import "expressive-charts/style.css"; // the same rules as a file (dist/expressive-charts.css)...
187
+ configure({ injectStyles: false }); // ...and nothing injected
188
+ ```
189
+
190
+ The package lists its CSS under `sideEffects`, so bundlers keep that import. Charts follow the device's light or dark setting; theme them with `--ec-*` custom properties, which your CSS always overrides, and pin a theme with `data-theme="light"` or `"dark"` on any ancestor ([the stylesheet](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md#expressive-chartsstylecss)).
191
+
192
+ ## Accessibility
193
+
194
+ - **Names.** Each chart is a `role="group"` named by its `label`, and each mark one image named by what it shows: "Okafor: 16 goals, 4 assists", or a slice's "Saved: 112 shots, 28%". `markLabel` words the names ("1 goal") and `markDescription` adds what they don't say.
195
+ - **Keyboard.** A chart is one tab stop: the arrow keys move between its marks, Home and End go to the ends, and focus shows a ring and plays the same effects as hover. Escape ends a hover, and so does `leave()`.
196
+ - **Fallbacks.** A data table after every chart, which every screen reader can read, and with `announce: true` a polite live region that says what each update changed.
197
+ - **Motion.** Reduced motion jumps every animation to its end state unless the viewer picks a mode on the page, with `motionSwitch()`. Nothing plays by itself, and a playback has its pause control in `playButton()` (WCAG 2.2.2).
198
+ - **Other people's charts.** `expressive()` adds the same keyboard focus with `keyboard: true`, and one name per mark with `markLabel`.
199
+
200
+ The demo pages with the library's charts on them have had a written WCAG 2.2 AA audit ([a11y-audit.md](https://github.com/Deaversbp/expressive-charts/blob/main/docs/a11y-audit.md)), and the test suite runs axe's WCAG rules on every demo page in Chromium, Firefox and WebKit, in light and dark, at 320 px wide and under forced colours. Screen-reader scripts for NVDA and Chrome, with their results, are in [a11y-check.md](https://github.com/Deaversbp/expressive-charts/blob/main/docs/a11y-check.md#results). Known gaps: the tooltip can't be hovered itself (it can be dismissed with Escape), dimming lowers the other marks' labels below 4.5:1 while one is hovered, and VoiceOver hasn't been run.
201
+
202
+ ## Limits
203
+
204
+ SVG charts stay smooth up to a few hundred moving marks. Measured on `demo/stress.html` with `npm run bench` (headless Chromium, each case alone, on an Intel Core i7-12700KF, October 2026), and with Chrome's 4× CPU slowdown standing in for a mid-range phone:
205
+
206
+ | Case | Smooth up to | At 4× slowdown |
207
+ | --- | --- | --- |
208
+ | A bar chart's staged update, every value changing | 100 bars | under 100 |
209
+ | Hover, dimming the other bars | 1,000 bars | 500 |
210
+ | A line on a date axis, played with `threshold` and a riding icon | 5 years of days (1–2 series), 2 years (3–4) | 1 year (1 series) |
211
+ | D3 columns in a transition, followed by `watch` | 500 | 20 |
212
+ | The same, with `refresh()` at the end | 1,000 | 100 |
213
+ | A `chartSwitch` with `morph()` | 200 pairs | 20 |
214
+
215
+ The first draw of 1,000 static bars takes about 90 ms, and of 10,000 about a second. [architecture.md](https://github.com/Deaversbp/expressive-charts/blob/main/docs/architecture.md#limits) has the method and the numbers.
216
+
217
+ ## The 0.x promise
218
+
219
+ From 0.1.0 the library follows semantic versioning. While it's 0.x, the effect-author API (`defineEffect`, `defineDataEffect`, `describe`, `matrix`, `offset`, the effect contracts and `expressive-charts/testing`) breaks only in a minor release, with a migration note, and a deprecated name keeps working for one minor release. The charts, adapters, built-in effects, controls and motion core can still change in a minor release, and [CHANGELOG.md](https://github.com/Deaversbp/expressive-charts/blob/main/CHANGELOG.md#the-0x-promise) lists every change. [docs/api.md](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md) marks each promised name.
220
+
221
+ ## Links
222
+
223
+ - [API reference](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md): every export of every entry.
224
+ - [Architecture](https://github.com/Deaversbp/expressive-charts/blob/main/docs/architecture.md): how the layers fit together, and how each part behaves.
225
+ - [Writing effects](https://github.com/Deaversbp/expressive-charts/blob/main/docs/effects-guide.md): the guide for effect authors.
226
+ - [Examples](https://github.com/Deaversbp/expressive-charts/tree/main/docs/sketches/examples): pages written against the API, which `npm run typecheck` checks: ball-and-cleat bars, a goals vs xG line with playback, a donut that morphs into columns, D3 charts, effects of your own and presets.
227
+ - [Changelog](https://github.com/Deaversbp/expressive-charts/blob/main/CHANGELOG.md), and [the development log](https://github.com/Deaversbp/expressive-charts/blob/main/docs/plan.md): the plan the library was built to, milestone by milestone, with the reasoning behind each decision.
228
+ - [Accessibility audit](https://github.com/Deaversbp/expressive-charts/blob/main/docs/a11y-audit.md) and [screen-reader scripts](https://github.com/Deaversbp/expressive-charts/blob/main/docs/a11y-check.md).
229
+ - [Demos](https://github.com/Deaversbp/expressive-charts/tree/main/demo), on [the demo site](https://deaversbp.github.io/expressive-charts/), or served by `npm run dev`. `bars.html`, `donut.html`, `line.html` and `morph.html` put the library's charts beside D3, Vega-Lite and hand-written ones, with the same effects on each; `stress.html` is the limits above; and `lab.html` is the single-file prototype the library grew out of.
230
+ - [Contributing](https://github.com/Deaversbp/expressive-charts/blob/main/CONTRIBUTING.md) and [issues](https://github.com/Deaversbp/expressive-charts/issues).
231
+
232
+ ## Development
233
+
234
+ | Command | What it does |
235
+ | --- | --- |
236
+ | `npm run dev` | Serves the `demo/` folder with live reload |
237
+ | `npm run build` | Builds the npm package (ESM + CommonJS, each with its own types, with the `icons/sport` and `testing` entries), a script-tag bundle (`dist/expressive-charts.global.js`) and the stylesheet as a file (`dist/expressive-charts.css`) |
238
+ | `npm run size` | Measures the built package and fails when a size is over its budget in `scripts/size-budget.json` (see [CONTRIBUTING.md](https://github.com/Deaversbp/expressive-charts/blob/main/CONTRIBUTING.md#bundle-size)) |
239
+ | `npm run pack-check` | After a build: packs the package, installs it in a new project and checks every entry as ESM and CommonJS, with its types (see [CONTRIBUTING.md](https://github.com/Deaversbp/expressive-charts/blob/main/CONTRIBUTING.md#setup)) |
240
+ | `npm test` | Runs the test suite, which also checks [docs/api.md](https://github.com/Deaversbp/expressive-charts/blob/main/docs/api.md) against the exports and type-checks this README's examples |
241
+ | `npm run test:visual:docker` | Runs the screenshot tests of `demo/bars.html`, `donut.html`, `line.html`, `morph.html` and `stress.html` in Chromium, and their DOM checks and the accessibility checks (axe) in Chromium, Firefox and WebKit, in Playwright's Linux image (needs Docker; see [CONTRIBUTING.md](https://github.com/Deaversbp/expressive-charts/blob/main/CONTRIBUTING.md#visual-tests)) |
242
+ | `npm run build:site` | Builds the demo pages into `site/`, which `.github/workflows/pages.yml` publishes as [the demo site](https://deaversbp.github.io/expressive-charts/) |
243
+ | `npm run media` | Captures README's pictures (`docs/media/*.gif`) from the demo pages, frame by frame, in Playwright's Linux image (needs Docker; see [CONTRIBUTING.md](https://github.com/Deaversbp/expressive-charts/blob/main/CONTRIBUTING.md#readmes-pictures-and-the-demo-site)) |
244
+ | `npm run bench` | Measures the stress page's cases in headless Chromium against `npm run dev`, unthrottled and at 4×, about 47 minutes; not in CI |
245
+ | `npm run typecheck` | Checks types without building |
246
+
247
+ Requires Node.js 22 or newer (CI tests 22 and 24).
248
+
249
+ ## License
250
+
251
+ [MIT](https://github.com/Deaversbp/expressive-charts/blob/main/LICENSE)