Datenfluss
Knoten, verbunden durch Haarlinien, auf denen Lichtpakete laufen, mit einem Ring, wo sie ankommen. Hält außerhalb des Blickfelds an.
Eine von 3 Components in Daten-Animationen
Details
Was Datenfluss mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Daten-Animationen
- Bereich
- Visuals
- Variants
- 3
- Version
- 1.0.0 · seit 1.0.0
- Datei
- components/ui/data-flow.tsx
- Bringt mit
- utilskitmotion-scene
- npm-Pakete
- Keine
- Sprachen
- Texte kommen über Props
- Studio
- Im Code anpassen
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle DataFlowProps aus components/ui/data-flow.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch. Nimmt außerdem alle Props von SceneMotionProps an.
nodesPflichtFlowNode[]edgesPflichtFlowEdge[]aspectoptionalstringScene ratio, width / height. Default "16 / 9".
narrowAspectoptionalstringRatio below 34rem width, where nodes need more height. Default "1 / 1".
labeloptionalstringAccessible description of what flows where. Without it the scene is decorative.
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Setzt aria-label, damit Screenreader Zweck und Zustand ansagen.
- Nutzt die Rolle label.
- Kommt ohne Client-State aus. Der Server schreibt sie vollständig, sie ist auch ohne JavaScript lesbar.
Bewegung
- Ohne Animationen, sie steht sofort ruhig da.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 208 Zeilen
'use client';
import { cn, round } from '@/lib/kit';
import { PlayToggle, SCENE_CSS, keyframes, useSceneId, useScenePlay, type SceneMotionProps } from '@/components/ui/motion-scene';
/*
* Data that travels between nodes: hairline connections, light packets that
* run along them and a soft ring where a packet arrives. One CSS timeline,
* packets are a short dash moved with stroke-dashoffset. Server and reduced
* motion show the calm map with small direction marks on every connection.
*/
export interface FlowNode {
id: string;
label: string;
/** A second, quieter line under the label */
meta?: string;
/** An icon or mark in the tile */
icon?: React.ReactNode;
/** Position of the node center in percent of the scene */
x: number;
y: number;
/** The main node: larger tile in the accent */
emphasis?: boolean;
}
export interface FlowEdge {
from: string;
to: string;
/** Packets run both ways */
both?: boolean;
/** Bend of the connection: horizontal leaves sideways, vertical leaves up or down, straight is a line */
shape?: 'horizontal' | 'vertical' | 'straight';
}
export interface DataFlowProps extends SceneMotionProps {
nodes: FlowNode[];
edges: FlowEdge[];
/** Scene ratio, width / height. Default "16 / 9". */
aspect?: string;
/** Ratio below 34rem width, where nodes need more height. Default "1 / 1". */
narrowAspect?: string;
/** Accessible description of what flows where. Without it the scene is decorative. */
label?: string;
className?: string;
}
const VW = 1000;
function geometry(a: FlowNode, b: FlowNode, shape: FlowEdge['shape'], vh: number) {
const x1 = (a.x / 100) * VW;
const y1 = (a.y / 100) * vh;
const x2 = (b.x / 100) * VW;
const y2 = (b.y / 100) * vh;
const s = shape ?? (Math.abs(x2 - x1) >= Math.abs(y2 - y1) ? 'horizontal' : 'vertical');
let c1: [number, number];
let c2: [number, number];
if (s === 'straight') {
c1 = [x1 + (x2 - x1) / 3, y1 + (y2 - y1) / 3];
c2 = [x1 + ((x2 - x1) * 2) / 3, y1 + ((y2 - y1) * 2) / 3];
} else if (s === 'horizontal') {
const k = (x2 - x1) * 0.55;
c1 = [x1 + k, y1];
c2 = [x2 - k, y2];
} else {
const k = (y2 - y1) * 0.55;
c1 = [x1, y1 + k];
c2 = [x2, y2 - k];
}
const d = `M${round(x1)} ${round(y1)} C${round(c1[0])} ${round(c1[1])} ${round(c2[0])} ${round(c2[1])} ${round(x2)} ${round(y2)}`;
// point and tangent at t = 0.5 for the direction mark
const t = 0.5;
const mt = 1 - t;
const px = mt ** 3 * x1 + 3 * mt * mt * t * c1[0] + 3 * mt * t * t * c2[0] + t ** 3 * x2;
const py = mt ** 3 * y1 + 3 * mt * mt * t * c1[1] + 3 * mt * t * t * c2[1] + t ** 3 * y2;
const dx = 3 * mt * mt * (c1[0] - x1) + 6 * mt * t * (c2[0] - c1[0]) + 3 * t * t * (x2 - c2[0]);
const dy = 3 * mt * mt * (c1[1] - y1) + 6 * mt * t * (c2[1] - c1[1]) + 3 * t * t * (y2 - c2[1]);
const angle = (Math.atan2(dy, dx) * 180) / Math.PI;
const dr = `M${round(x2)} ${round(y2)} C${round(c2[0])} ${round(c2[1])} ${round(c1[0])} ${round(c1[1])} ${round(x1)} ${round(y1)}`;
return { d, dr, mx: round(px), my: round(py), angle: round(angle, 1) };
}
/**
* Nodes connected by lines with packets of light running along them.
* <DataFlow nodes={[{ id: 'form', label: 'Form', x: 12, y: 50 }, …]} edges={[{ from: 'form', to: 'crm' }]} />
*/
export function DataFlow({ nodes, edges, aspect = '16 / 9', narrowAspect = '1 / 1', speed = 1, autoPlay = 'in-view', loop = true, controls = true, label, className }: DataFlowProps) {
const sid = useSceneId('kvdf');
const play = useScenePlay<HTMLDivElement>(autoPlay);
const [aw, ah] = aspect.split('/').map((n) => Number(n.trim()) || 1);
const VH = round((VW * ah) / aw);
const byId = new Map(nodes.map((n) => [n.id, n]));
const n = Math.max(1, edges.length);
// One cycle lasts base seconds at speed 1, a packet travels 1.25 s of it
const base = Math.max(3.4, n * 0.62);
const T = round(base / Math.max(0.1, speed), 2);
const f = round((1.25 / base) * 100, 2);
const lanes = edges
.map((e, i) => {
const a = byId.get(e.from);
const b = byId.get(e.to);
if (!a || !b) return null;
return { e, i, a, b, g: geometry(a, b, e.shape, VH), delay: round((i * T) / n, 2) };
})
.filter(Boolean) as { e: FlowEdge; i: number; a: FlowNode; b: FlowNode; g: ReturnType<typeof geometry>; delay: number }[];
const ease = 'animation-timing-function:cubic-bezier(.5,0,.3,1)';
const css = `${SCENE_CSS}
${keyframes(`${sid}-p`, [[0, `stroke-dashoffset:6;opacity:0;${ease}`], [1.5, 'opacity:1'], [f, 'stroke-dashoffset:-100;opacity:1'], [f + 1, 'opacity:0'], [100, 'stroke-dashoffset:-100;opacity:0']])}
${keyframes(`${sid}-t`, [[0, `stroke-dashoffset:22;opacity:0;${ease}`], [3, 'opacity:.5'], [f + 2, 'stroke-dashoffset:-100;opacity:.35'], [f + 4, 'opacity:0'], [100, 'stroke-dashoffset:-100;opacity:0']])}
${keyframes(`${sid}-r`, [[0, 'opacity:0;transform:scale(.92)'], [f - 1, 'opacity:0;transform:scale(.92)'], [f + 2, 'opacity:.7;transform:scale(1)'], [Math.min(99, f + 26), 'opacity:0;transform:scale(1.32)'], [100, 'opacity:0;transform:scale(1.32)']])}
${keyframes(`${sid}-in`, [[0, 'opacity:0;transform:translate(-50%,-50%) scale(.94)'], [100, 'opacity:1;transform:translate(-50%,-50%) scale(1)']])}
${keyframes(`${sid}-line`, [[0, 'stroke-dashoffset:1'], [100, 'stroke-dashoffset:0']])}
.${sid}:not([data-kv="off"]) .${sid}-p { animation: ${sid}-p ${T}s linear var(--d) ${loop ? 'infinite' : '1'} both; }
.${sid}:not([data-kv="off"]) .${sid}-t { animation: ${sid}-t ${T}s linear var(--d) ${loop ? 'infinite' : '1'} both; }
.${sid}:not([data-kv="off"]) .${sid}-r { animation: ${sid}-r ${T}s linear var(--d) ${loop ? 'infinite' : '1'} both; }
.${sid}:not([data-kv="off"]) .${sid}-n { animation: ${sid}-in .7s var(--ease-kit) var(--i) 1 both; }
.${sid}:not([data-kv="off"]) .${sid}-l { stroke-dasharray: 1 1; animation: ${sid}-line 1.1s var(--ease-kit) var(--i) 1 both; }`;
return (
<div className={cn('@container relative w-full', className)}>
<div
ref={play.ref}
data-kv={play.state}
role={label ? 'img' : undefined}
aria-label={label}
aria-hidden={label ? undefined : true}
className={cn(sid, 'relative w-full select-none [aspect-ratio:var(--df-ar)] @max-[34rem]:[aspect-ratio:var(--df-ar-sm)]')}
style={{ '--df-ar': aspect, '--df-ar-sm': narrowAspect } as React.CSSProperties}
>
<style dangerouslySetInnerHTML={{ __html: css }} />
<svg key={play.cycle} viewBox={`0 0 ${VW} ${VH}`} preserveAspectRatio="none" className="absolute inset-0 size-full overflow-visible" aria-hidden>
{lanes.map(({ g, i }) => (
<g key={`l${i}`}>
<path className={`${sid}-l`} pathLength={1} d={g.d} fill="none" stroke="color-mix(in oklab, var(--foreground) 17%, transparent)" strokeWidth="1.25" vectorEffect="non-scaling-stroke" style={{ '--i': `${round(0.15 + i * 0.08, 2)}s` } as React.CSSProperties} />
<path
className="@max-[34rem]:hidden"
d="M-4 -4.5 L2 0 L-4 4.5"
transform={`translate(${g.mx} ${g.my}) rotate(${g.angle})`}
fill="none"
stroke="color-mix(in oklab, var(--foreground) 30%, transparent)"
strokeWidth="1.4"
strokeLinecap="round"
strokeLinejoin="round"
vectorEffect="non-scaling-stroke"
/>
</g>
))}
{lanes.flatMap(({ g, e, i, delay }) =>
(e.both ? [g.d, g.dr] : [g.d]).map((d, k) => {
const style = { '--d': `${round(delay + k * (T / 2), 2)}s` } as React.CSSProperties;
return (
<g key={`p${i}-${k}`} style={style}>
<path className={`${sid}-t`} pathLength={100} d={d} fill="none" stroke="var(--primary)" strokeWidth="2" strokeLinecap="round" strokeDasharray="16 200" strokeDashoffset="22" opacity="0" vectorEffect="non-scaling-stroke" />
<path className={`${sid}-p`} pathLength={100} d={d} fill="none" stroke="var(--primary)" strokeWidth="9" strokeLinecap="round" strokeDasharray="2.2 200" strokeDashoffset="6" opacity="0" strokeOpacity="0.28" vectorEffect="non-scaling-stroke" />
<path className={`${sid}-p`} pathLength={100} d={d} fill="none" stroke="color-mix(in oklab, var(--primary) 70%, #fff)" strokeWidth="3.2" strokeLinecap="round" strokeDasharray="2.2 200" strokeDashoffset="6" opacity="0" vectorEffect="non-scaling-stroke" />
</g>
);
}),
)}
</svg>
{nodes.map((node, k) => {
const incoming = lanes.filter((l) => l.b.id === node.id || (l.e.both && l.a.id === node.id));
return (
<div
key={node.id}
className={`${sid}-n absolute z-10`}
style={{ left: `${node.x}%`, top: `${node.y}%`, transform: 'translate(-50%,-50%)', '--i': `${round(k * 0.07, 2)}s` } as React.CSSProperties}
>
<div
className={cn(
'relative flex items-center gap-2.5 rounded-xl border bg-card text-left shadow-[var(--highlight),var(--shadow-md)] @max-[34rem]:flex-col @max-[34rem]:gap-1 @max-[34rem]:border-0 @max-[34rem]:bg-transparent @max-[34rem]:p-0 @max-[34rem]:shadow-none',
node.emphasis ? 'border-[color-mix(in_oklab,var(--primary)_45%,var(--border))] p-2.5 pr-4 shadow-[var(--highlight),0_0_0_4px_color-mix(in_oklab,var(--primary)_10%,transparent),var(--shadow-lg)]' : 'border-border p-2 pr-3.5',
)}
>
{incoming.map((l) => (
<span
key={`${l.i}`}
aria-hidden
className={`${sid}-r pointer-events-none absolute -inset-1 rounded-[14px] border-2 border-[color-mix(in_oklab,var(--primary)_70%,transparent)] opacity-0 @max-[34rem]:inset-auto @max-[34rem]:left-1/2 @max-[34rem]:top-0 @max-[34rem]:size-10 @max-[34rem]:-translate-x-1/2`}
style={{ '--d': `${round(l.delay + (l.e.both && l.a.id === node.id ? T / 2 : 0), 2)}s` } as React.CSSProperties}
/>
))}
<span
className={cn(
'grid shrink-0 place-items-center rounded-lg [&_svg]:size-[55%] @max-[34rem]:shadow-[var(--highlight),var(--shadow-md)]',
node.emphasis ? 'size-10 bg-primary text-primary-foreground @max-[34rem]:size-11' : 'size-8 border border-border bg-surface text-foreground @max-[34rem]:size-10 @max-[34rem]:bg-card',
)}
>
{node.icon ?? <span className="size-2 rounded-full bg-current" />}
</span>
<span className="min-w-0 @max-[34rem]:text-center">
<span className={cn('block whitespace-nowrap font-semibold tracking-[-0.01em] text-foreground @max-[34rem]:max-w-[5.5rem] @max-[34rem]:whitespace-normal @max-[34rem]:leading-tight', node.emphasis ? 'text-[14px]' : 'text-[12.5px] @max-[34rem]:text-[10.5px] @max-[34rem]:font-medium')}>{node.label}</span>
{node.meta && <span className="block whitespace-nowrap text-[11px] text-muted-foreground @max-[44rem]:hidden">{node.meta}</span>}
</span>
</div>
</div>
);
})}
</div>
{controls && (
<div className="mt-2 flex justify-end">
<PlayToggle inline state={play.state} reduced={play.reduced} onToggle={play.toggle} />
</div>
)}
</div>
);
}Häufige Fragen
Wie installiere ich Datenfluss?
Mit aiw add @ai-website/data-flow. Der Befehl legt die Datei components/ui/data-flow.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind motion-scene. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Datenfluss?
3 Varianten, Pipeline, Zentrale und Sammeln und berichten.
Kann ein KI-Assistent Datenfluss einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Datenfluss mit aiw_components und installieren sie mit aiw_add @ai-website/data-flow.