dependency-graph
Packages and modules connected as a readable graph.
import {Scene, Video} from "odori";
import {DependencyGraph} from "../components/dependency-graph/dependency-graph";
<Video>
<Scene id="dependency-graph" duration="5s">
<DependencyGraph
nodes={[
{
label: "videos/launch",
x: 0.24,
y: 0.4
},
{
label: "videos/components",
x: 0.56,
y: 0.26
},
{
label: "odori",
x: 0.78,
y: 0.56
},
{
label: "public/audio",
x: 0.36,
y: 0.74
}
]}
edges={[
[
0,
1
],
[
1,
2
],
[
0,
3
]
]}
headline="One graph, one render path."
/>
</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/dependency-graphnpx odori add @odori/dependency-graphbunx odori add @odori/dependency-graphSource is copied into videos/components/dependency-graph/ 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/dependency-graph/dependency-graph.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type GraphNode = {
label: string;
/** Position in the frame, 0 to 1 on each axis. */
x: number;
y: number;
};
export type DependencyGraphProps = {
nodes: GraphNode[];
/** Pairs of node indexes, drawn in order. */
edges: Array<[number, number]>;
headline?: string;
};
/**
* Packages connected as a readable graph. Edges draw before the node they
* point at appears, so the eye follows the dependency rather than hunting for
* what moved.
*/
export const DependencyGraph = ({nodes, edges, headline}: DependencyGraphProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
return (
<Fill style={{padding: 100 * scale}}>
{headline ? (
<div
style={{
fontSize: 56 * scale,
fontWeight: 560,
letterSpacing: "-0.04em",
opacity: interpolate(frame, [0, 18], [0, 1], {easing: Easing.standard}),
}}
>
{headline}
</div>
) : null}
<Fill>
<svg style={{height: "100%", width: "100%"}}>
{edges.map(([from, to], index) => {
const delay = 14 + index * 8;
const draw = interpolate(frame, [delay, delay + 20], [0, 1], {easing: Easing.standard});
const a = nodes[from];
const b = nodes[to];
if (!a || !b) return null;
return (
<line
key={`${from}-${to}`}
stroke={brand.colors.border}
strokeWidth={2 * scale}
x1={`${a.x * 100}%`}
x2={`${(a.x + (b.x - a.x) * draw) * 100}%`}
y1={`${a.y * 100}%`}
y2={`${(a.y + (b.y - a.y) * draw) * 100}%`}
/>
);
})}
</svg>
</Fill>
{nodes.map((node, index) => {
const delay = 10 + index * 8;
const enter = interpolate(frame, [delay, delay + 18], [0, 1], {easing: Easing.standard});
return (
<div
key={node.label}
style={{
background: brand.colors.surface,
border: `1px solid ${brand.colors.border}`,
borderRadius: 999,
fontFamily: brand.typography.mono,
fontSize: 28 * scale,
left: `${node.x * 100}%`,
opacity: enter,
padding: `${14 * scale}px ${26 * scale}px`,
position: "absolute",
top: `${node.y * 100}%`,
transform: `translate(-50%, -50%) scale(${0.9 + enter * 0.1})`,
}}
>
{node.label}
</div>
);
})}
</Fill>
);
};
videos/components/dependency-graph/dependency-graph.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {DependencyGraph} from "./dependency-graph";
export default defineComponentPreview({
title: "Dependency graph",
category: "Developer proof",
description: "Packages and modules connected as a readable graph.",
component: DependencyGraph,
canvas: {width: 1920, height: 1080, duration: "5s"},
controls: {
headline: {type: "text", defaultValue: "One graph, one render path."},
},
examples: [
{
name: "Project",
props: {
nodes: [
{label: "videos/launch", x: 0.24, y: 0.4},
{label: "videos/components", x: 0.56, y: 0.26},
{label: "odori", x: 0.78, y: 0.56},
{label: "public/audio", x: 0.36, y: 0.74},
],
edges: [
[0, 1],
[1, 2],
[0, 3],
],
},
},
],
});
Use it in a scene.
import {Scene, Video} from "odori";
import {DependencyGraph} from "../components/dependency-graph/dependency-graph";
<Video>
<Scene id="dependency-graph" duration="5s">
<DependencyGraph
nodes={[
{
label: "videos/launch",
x: 0.24,
y: 0.4
},
{
label: "videos/components",
x: 0.56,
y: 0.26
},
{
label: "odori",
x: 0.78,
y: 0.56
},
{
label: "public/audio",
x: 0.36,
y: 0.74
}
]}
edges={[
[
0,
1
],
[
1,
2
],
[
0,
3
]
]}
headline="One graph, one render path."
/>
</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
- Developer proof
- Aspect ratios
- 16:9 · 1:1
- Recommended duration
- 150 frames · 5s
- Minimum duration
- 75 frames · 2.5s
- Entrance
- 60 frames
- Exit
- 0 frames
- Reduced motion
- edges and nodes appear without drawing
- Requires
- sans font, mono font
- Content limits
- nodes ≤ 6, labelLength ≤ 24
Props
| Prop | Type | Default | Required |
|---|---|---|---|
nodes | GraphNode[] | — | Yes |
edgesPairs of node indexes, drawn in order. | Array<[number, number]> | — | Yes |
headline | string | — | — |