Dependency graph
Packages and modules connected as a readable graph.
Customize
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>Installation
pnpm odori add component dependency-graphnpx odori add component dependency-graphbunx odori add component dependency-graphEdit the source in videos/components/dependency-graph/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files 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/Code",
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],
],
},
},
],
});
Add it to 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>Timing contract
Use these durations and content limits when composing a scene. Run odori test to check your video.
- Family
- Developer proof
- 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 | — | — |
Related components
All componentsProduct launchA framed source excerpt that reveals line by line.Code walkthroughProgressive code reveal with authored focal regions.Diff proofBefore and after source changes with semantic emphasis.File treeProgressive repository navigation with active file state.Log streamPaced runtime logs with levels and bounded scrolling.TerminalA typed command session with deterministic output reveals.