Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/INTERACTIVITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,26 @@ const html = interactiveEmbed(svgString, { title: 'Sales' });
The MCP server exposes the same via the `export_interactive_html` tool, so an
agent can emit an interactive chart a reader can hover — not just a static image.

## Data-change transitions

Pass `transitions={{ enabled: true }}` to `BarChart`, `LineChart`, `AreaChart`,
or `PieChart` and the chart will tween between data snapshots (400 ms by
default; override with `durationMs`). Off by default — existing renders are
byte-identical. Honors `prefers-reduced-motion` (snaps instead of tweening).

```tsx
<BarChart
width={400}
height={300}
data={data}
transitions={{ enabled: true, durationMs: 600 }}
/>
```

Under the hood the chart wraps its `data` prop in `useDataTransition`
(exported from `goldenchart/interactive` for direct use in custom
compositions). Rough.js seeds are index-based and don't shimmer across frames.

## Accessibility

Marks are keyboard-focusable with `aria-label`s; the tooltip path is reachable
Expand Down
9 changes: 8 additions & 1 deletion src/components/AreaChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { colorAt } from '../core/palette';
import { resolveBrand } from '../brand/resolveBrand';
import { seriesTable } from '../core/dataTable';
import { describeSeries } from '../core/a11yDescribe';
import { useDataTransition } from '../interactive/useDataTransition';
import { layoutLegend } from '../core/legend';
import type { LegendItem } from '../core/legend';
import { resolveVibe } from '../vibe/resolveVibe';
Expand Down Expand Up @@ -44,7 +45,7 @@ export interface AreaChartProps extends BaseChartProps {
* d3-shape's `area` builds the fill path; the vibe's `fillStyle` textures it.
*/
export function AreaChart({
series,
series: rawSeries,
width,
height,
margin,
Expand All @@ -67,7 +68,13 @@ export function AreaChart({
annotations,
xAxis,
yAxis,
transitions,
}: AreaChartProps) {
const series = useDataTransition(
rawSeries,
transitions?.durationMs ?? 400,
transitions?.enabled ?? false,
);
const fullPlot = getPlotArea(width, height, margin);
const rv = resolveVibe(vibe);
const palette = resolveBrand(brand).palette;
Expand Down
9 changes: 8 additions & 1 deletion src/components/BarChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { colorAt } from '../core/palette';
import { resolveBrand } from '../brand/resolveBrand';
import { datumTable } from '../core/dataTable';
import { describeBars } from '../core/a11yDescribe';
import { useDataTransition } from '../interactive/useDataTransition';
import { groupMax, seriesKeysOf, stackLayout, stackMax } from '../core/stack';
import { Surface } from './Surface';
import { Axis } from './Axis';
Expand Down Expand Up @@ -63,7 +64,7 @@ interface LaidBar {
* multi-series modes.
*/
export function BarChart({
data,
data: rawData,
width,
height,
margin,
Expand All @@ -84,7 +85,13 @@ export function BarChart({
annotations,
xAxis,
yAxis,
transitions,
}: BarChartProps) {
const data = useDataTransition(
rawData,
transitions?.durationMs ?? 400,
transitions?.enabled ?? false,
);
const fullPlot = getPlotArea(width, height, margin);
const resolved = resolveVibe(vibe);
const palette = resolveBrand(brand).palette;
Expand Down
9 changes: 8 additions & 1 deletion src/components/LineChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { colorAt } from '../core/palette';
import { resolveBrand } from '../brand/resolveBrand';
import { seriesTable } from '../core/dataTable';
import { describeSeries } from '../core/a11yDescribe';
import { useDataTransition } from '../interactive/useDataTransition';
import { layoutLegend } from '../core/legend';
import type { LegendItem } from '../core/legend';
import { resolveVibe } from '../vibe/resolveVibe';
Expand Down Expand Up @@ -42,7 +43,7 @@ export interface LineChartProps extends BaseChartProps {

/** Multi-series line chart: d3-shape builds each path, `<RoughPath>` sketches it. */
export function LineChart({
series,
series: rawSeries,
width,
height,
margin,
Expand All @@ -64,7 +65,13 @@ export function LineChart({
emphasis,
xAxis,
yAxis,
transitions,
}: LineChartProps) {
const series = useDataTransition(
rawSeries,
transitions?.durationMs ?? 400,
transitions?.enabled ?? false,
);
const fullPlot = getPlotArea(width, height, margin);
const rv = resolveVibe(vibe);
const palette = resolveBrand(brand).palette;
Expand Down
9 changes: 8 additions & 1 deletion src/components/PieChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { colorAt } from '../core/palette';
import { resolveBrand } from '../brand/resolveBrand';
import { datumTable } from '../core/dataTable';
import { describePie } from '../core/a11yDescribe';
import { useDataTransition } from '../interactive/useDataTransition';
import { resolveVibe } from '../vibe/resolveVibe';
import { Surface } from './Surface';
import { RoughPath } from '../primitives/RoughPath';
Expand All @@ -25,7 +26,7 @@ export interface PieChartProps extends BaseChartProps {
* origin; we translate to the plot center and let `<RoughPath>` sketch them.
*/
export function PieChart({
data,
data: rawData,
width,
height,
margin,
Expand All @@ -41,7 +42,13 @@ export function PieChart({
innerRadius = 0,
padAngle = 0.02,
showLabels = true,
transitions,
}: PieChartProps) {
const data = useDataTransition(
rawData,
transitions?.durationMs ?? 400,
transitions?.enabled ?? false,
);
const plot = getPlotArea(width, height, margin);
const cx = plot.x + plot.width / 2;
const cy = plot.y + plot.height / 2;
Expand Down
10 changes: 10 additions & 0 deletions src/types/charts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ export interface BaseChartProps {
children?: ReactNode;
/** Render only the bare `<svg>` (no wrapper div) for headless/SVG-string use. */
bare?: boolean;
/**
* Opt-in enter/update transitions when the chart's data prop changes. Off
* by default so existing renders are byte-identical; honours
* `prefers-reduced-motion` (snaps to the new state instead of tweening).
*/
transitions?: {
enabled?: boolean;
/** Tween length in ms. Default 400. */
durationMs?: number;
};
}

/** A tabular mirror of a chart's data, rendered visually-hidden for screen readers. */
Expand Down
Loading