Line chart
Time-series path draw with optional focal annotation.
Customize
import {Scene, Video} from "odori";
import {LineChart} from "../components/line-chart/line-chart";
<Video>
<Scene id="line-chart" duration="4s">
<LineChart
values={[
3,
4,
6,
5,
9,
12,
18,
21
]}
title="Videos shipped per week"
annotateIndex={6}
annotation="The week the registry landed."
/>
</Scene>
</Video>Installation
pnpm odori add component line-chartnpx odori add component line-chartbunx odori add component line-chartEdit the source in videos/components/line-chart/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/line-chart/line-chart.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type LineChartProps = {
/** One value per step, drawn left to right. */
values: number[];
title?: string;
/** Index to annotate once the path finishes drawing. */
annotateIndex?: number;
annotation?: string;
};
/**
* A time series drawn as one continuous path. The line reveals by clipping
* from the left rather than by animating point positions, so the shape of the
* data never lies during the reveal.
*/
export const LineChart = ({values, title, annotateIndex, annotation}: LineChartProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const max = Math.max(...values, 1);
const min = Math.min(...values, 0);
const draw = interpolate(frame, [12, 60], [0, 1], {easing: Easing.standard});
const point = (value: number, index: number) => ({
x: (index / Math.max(1, values.length - 1)) * 100,
y: 100 - ((value - min) / Math.max(1, max - min)) * 100,
});
const path = values
.map((value, index) => {
const {x, y} = point(value, index);
return `${index === 0 ? "M" : "L"} ${x} ${y}`;
})
.join(" ");
const marked = annotateIndex !== undefined ? point(values[annotateIndex] ?? 0, annotateIndex) : null;
const markOpacity = interpolate(frame, [58, 76], [0, 1], {easing: Easing.standard});
return (
<Fill style={{gap: 34 * 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, position: "relative", width: "100%"}}>
<svg preserveAspectRatio="none" style={{height: "100%", width: "100%"}} viewBox="0 0 100 100">
<path
d={path}
fill="none"
stroke={brand.colors.foreground}
strokeLinecap="round"
strokeWidth={9 * scale}
style={{clipPath: `inset(0 ${(1 - draw) * 100}% 0 0)`}}
vectorEffect="non-scaling-stroke"
/>
</svg>
{marked ? (
<div
style={{
left: `${marked.x}%`,
opacity: markOpacity,
position: "absolute",
top: `${marked.y}%`,
transform: "translate(-50%, -50%)",
}}
>
<span
style={{
background: brand.colors.accent,
borderRadius: 999,
display: "block",
height: 18 * scale,
width: 18 * scale,
}}
/>
</div>
) : null}
</div>
{annotation ? (
<div style={{color: brand.colors.muted, fontSize: 32 * scale, opacity: markOpacity}}>{annotation}</div>
) : null}
</Fill>
);
};
videos/components/line-chart/line-chart.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {LineChart} from "./line-chart";
export default defineComponentPreview({
title: "Line chart",
category: "Data",
description: "Time-series path draw with optional focal annotation.",
component: LineChart,
canvas: {width: 1920, height: 1080, duration: "4s"},
controls: {
title: {type: "text", defaultValue: "Videos shipped per week"},
annotation: {type: "text", defaultValue: "The week the registry landed."},
annotateIndex: {type: "number", defaultValue: 6, min: 0, max: 11},
},
examples: [
{name: "Growth", props: {values: [3, 4, 6, 5, 9, 12, 18, 21], annotateIndex: 6}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {LineChart} from "../components/line-chart/line-chart";
<Video>
<Scene id="line-chart" duration="4s">
<LineChart
values={[
3,
4,
6,
5,
9,
12,
18,
21
]}
title="Videos shipped per week"
annotateIndex={6}
annotation="The week the registry landed."
/>
</Scene>
</Video>Timing contract
Use these durations and content limits when composing a scene. Run odori test to check your video.
- Family
- Data
- Recommended duration
- 120 frames · 4s
- Minimum duration
- 78 frames · 2.6s
- Entrance
- 64 frames
- Exit
- 0 frames
- Reduced motion
- the full path is drawn immediately
- Requires
- sans font
- Content limits
- points ≤ 16
Props
| Prop | Type | Default | Required |
|---|---|---|---|
valuesOne value per step, drawn left to right. | number[] | — | Yes |
title | string | — | — |
annotateIndexIndex to annotate once the path finishes drawing. | number | — | — |
annotation | string | — | — |
Related components
All componentsArea chartFilled trend reveal for volume and cumulative change.Bar chartRanked bars with labels, values, and staged growth.Data tableAnimated sorting and row selection.Donut chartPart-to-whole display with a stable center metric.HeatmapGrid intensity reveal for activity and distribution.Progress ringCircular completion display with restrained motion.