area-chart
Filled trend reveal for volume and cumulative change.
import {Scene, Video} from "odori";
import {AreaChart} from "../components/area-chart/area-chart";
<Video>
<Scene id="area-chart" duration="4s">
<AreaChart
values={[
2,
5,
9,
14,
22,
31,
44,
60
]}
title="Frames rendered"
detail="Cumulative, across every format."
/>
</Scene>
</Video>The same runtime that renders the export. Scrub to any frame, switch format, and edit the fixture props.
Installation
pnpm odori add @odori/area-chartnpx odori add @odori/area-chartbunx odori add @odori/area-chartSource is copied into videos/components/area-chart/ and belongs to your repository. odori diff compares it with upstream later; odori update applies the change you accept. It has no registry dependencies.
Copy and paste the following code into your project.
videos/components/area-chart/area-chart.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type AreaChartProps = {
values: number[];
title?: string;
detail?: string;
};
/**
* A filled trend for volume and cumulative change. The fill wipes in with the
* path, so the area never appears ahead of the line that bounds it.
*/
export const AreaChart = ({values, title, detail}: AreaChartProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const max = Math.max(...values, 1);
const draw = interpolate(frame, [12, 62], [0, 1], {easing: Easing.standard});
const points = values.map((value, index) => ({
x: (index / Math.max(1, values.length - 1)) * 100,
y: 100 - (value / max) * 92,
}));
const line = points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ");
const area = `${line} L 100 100 L 0 100 Z`;
return (
<Fill style={{gap: 30 * scale, justifyContent: "center", padding: `${110 * scale}px ${130 * scale}px`}}>
{title ? (
<div style={{fontSize: 58 * scale, fontWeight: 560, letterSpacing: "-0.04em", opacity: interpolate(frame, [0, 16], [0, 1])}}>
{title}
</div>
) : null}
<div style={{height: 460 * scale, width: "100%"}}>
<svg preserveAspectRatio="none" style={{clipPath: `inset(0 ${(1 - draw) * 100}% 0 0)`, height: "100%", width: "100%"}} viewBox="0 0 100 100">
<path d={area} fill={brand.colors.accent} opacity={0.18} />
<path d={line} fill="none" stroke={brand.colors.accent} strokeWidth={9 * scale} vectorEffect="non-scaling-stroke" />
</svg>
</div>
{detail ? (
<div style={{color: brand.colors.muted, fontSize: 32 * scale, opacity: interpolate(frame, [56, 74], [0, 1])}}>{detail}</div>
) : null}
</Fill>
);
};
videos/components/area-chart/area-chart.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {AreaChart} from "./area-chart";
export default defineComponentPreview({
title: "Area chart",
category: "Data",
description: "Filled trend reveal for volume and cumulative change.",
component: AreaChart,
canvas: {width: 1920, height: 1080, duration: "4s"},
controls: {
title: {type: "text", defaultValue: "Frames rendered"},
detail: {type: "text", defaultValue: "Cumulative, across every format."},
},
examples: [{name: "Adoption", props: {values: [2, 5, 9, 14, 22, 31, 44, 60]}}],
});
Use it in a scene.
import {Scene, Video} from "odori";
import {AreaChart} from "../components/area-chart/area-chart";
<Video>
<Scene id="area-chart" duration="4s">
<AreaChart
values={[
2,
5,
9,
14,
22,
31,
44,
60
]}
title="Frames rendered"
detail="Cumulative, across every format."
/>
</Scene>
</Video>Nothing else to install.
The component imports only from odori, so there are no dependencies to add and no import paths to rewrite.
Timing contract
Every registry component declares how it behaves in time.odori test fails a cut that violates these.
- Family
- Data
- Aspect ratios
- 16:9 · 1:1
- Recommended duration
- 120 frames · 4s
- Minimum duration
- 78 frames · 2.6s
- Entrance
- 62 frames
- Exit
- 0 frames
- Reduced motion
- the filled area appears at once
- Requires
- sans font
- Content limits
- points ≤ 16
Props
| Prop | Type | Default | Required |
|---|---|---|---|
values | number[] | — | Yes |
title | string | — | — |
detail | string | — | — |