bar-chart
Ranked bars with labels, values, and staged growth.
import {Scene, Video} from "odori";
import {BarChart} from "../components/bar-chart/bar-chart";
<Video>
<Scene id="bar-chart" duration="4s">
<BarChart
data={[
{
label: "Manual edit",
value: 42
},
{
label: "Template tool",
value: 18
},
{
label: "Odori",
value: 4
}
]}
title="Time to first preview"
unit="s"
highlight={2}
/>
</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/bar-chartnpx odori add @odori/bar-chartbunx odori add @odori/bar-chartSource is copied into videos/components/bar-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/bar-chart/bar-chart.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type BarDatum = {label: string; value: number};
export type BarChartProps = {
data: BarDatum[];
title?: string;
/** Appended to every printed value. */
unit?: string;
/** Index drawn in the accent color. */
highlight?: number;
};
/**
* Ranked bars that grow from a shared baseline. The scale comes from the data,
* never from a rounded guess, so two charts in one video stay comparable.
*/
export const BarChart = ({data, title, unit = "", highlight}: BarChartProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const max = Math.max(...data.map((item) => item.value), 1);
return (
<Fill style={{gap: 40 * 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], {easing: Easing.standard}),
}}
>
{title}
</div>
) : null}
<div style={{display: "grid", gap: 22 * scale}}>
{data.map((item, index) => {
const delay = 10 + index * 6;
const grow = interpolate(frame, [delay, delay + 30], [0, 1], {easing: Easing.standard});
const accent = index === highlight;
return (
<div key={item.label} style={{alignItems: "center", display: "flex", gap: 24 * scale}}>
<span style={{color: brand.colors.muted, fontSize: 30 * scale, width: 320 * scale}}>{item.label}</span>
<div style={{background: brand.colors.border, borderRadius: 999, flex: 1, height: 34 * scale}}>
<div
style={{
background: accent ? brand.colors.accent : brand.colors.foreground,
borderRadius: 999,
height: "100%",
width: `${(item.value / max) * 100 * grow}%`,
}}
/>
</div>
<span
style={{
fontSize: 30 * scale,
fontVariantNumeric: "tabular-nums",
textAlign: "right",
width: 180 * scale,
}}
>
{(item.value * grow).toFixed(item.value % 1 === 0 ? 0 : 1)}
{unit}
</span>
</div>
);
})}
</div>
</Fill>
);
};
videos/components/bar-chart/bar-chart.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {BarChart} from "./bar-chart";
export default defineComponentPreview({
title: "Bar chart",
category: "Data",
description: "Ranked bars with labels, values, and staged growth.",
component: BarChart,
canvas: {width: 1920, height: 1080, duration: "4s"},
controls: {
title: {type: "text", defaultValue: "Time to first preview"},
unit: {type: "text", defaultValue: "s"},
highlight: {type: "number", defaultValue: 2, min: 0, max: 5},
},
examples: [
{
name: "Comparison",
props: {
data: [
{label: "Manual edit", value: 42},
{label: "Template tool", value: 18},
{label: "Odori", value: 4},
],
highlight: 2,
},
},
],
});
Use it in a scene.
import {Scene, Video} from "odori";
import {BarChart} from "../components/bar-chart/bar-chart";
<Video>
<Scene id="bar-chart" duration="4s">
<BarChart
data={[
{
label: "Manual edit",
value: 42
},
{
label: "Template tool",
value: 18
},
{
label: "Odori",
value: 4
}
]}
title="Time to first preview"
unit="s"
highlight={2}
/>
</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 · 9:16 · 1:1
- Recommended duration
- 120 frames · 4s
- Minimum duration
- 60 frames · 2s
- Entrance
- 50 frames
- Exit
- 0 frames
- Reduced motion
- bars appear at full length
- Requires
- sans font
- Content limits
- bars ≤ 6, labelLength ≤ 28
Props
| Prop | Type | Default | Required |
|---|---|---|---|
data | BarDatum[] | — | Yes |
title | string | — | — |
unitAppended to every printed value. | string | "" | — |
highlightIndex drawn in the accent color. | number | — | — |