"use client"; import * as React from "react"; import { cn } from "./lib/utils"; /** * SankeyChart - two-level flow diagram with bezier ribbons */ interface SankeyNode { id: string; label: string; side: "left" | "right"; } interface SankeyFlow { from: string; to: string; value: number; } interface SankeyChartProps { nodes: SankeyNode[]; flows: SankeyFlow[]; } const Ribbon = ({ from, to, value, }: { from: { x: number; y: number }; to: { x: number; y: number }; value: number }) => { const width = Math.max(3, value * 20); // scale value to pixel width const amperage = Math.max(width * 4, 40); // Cubic bezier curve points // from bottom-right to to bottom-left with control points const d = `M ${from.x} ${from.y} C ${from.x + amperage} ${from.y}, ${to.x - amperage} ${to.y}, ${to.x} ${to.y}`; return ( ); }; export const SankeyChart = React.forwardRef( function SankeyChart(props, ref) { const { nodes, flows } = props; // Separate left and right nodes const leftNodes = nodes.filter(n => n.side === "left"); const rightNodes = nodes.filter(n => n.side === "right"); // Create lookup maps const leftNodeMap = new Map(leftNodes.map(n => [n.id, n])); const rightNodeMap = new Map(rightNodes.map(n => [n.id, n])); // Calculate node positions const nodeCount = Math.max(leftNodes.length, rightNodes.length); const nodeSpacing = 80; const diagramWidth = 600; const diagramHeight = (nodeCount + 1) * nodeSpacing; // Position left nodes vertically const leftPositions = new Map( leftNodes.map((n, i) => [n.id, { x: 50, y: 50 + i * nodeSpacing }]), ); // Position right nodes vertically const rightPositions = new Map( rightNodes.map((n, i) => [n.id, { x: diagramWidth - 50, y: 50 + i * nodeSpacing }]), ); // Sort flows by value for rendering order const sortedFlows = [...flows].sort((a, b) => b.value - a.value); return ( {/* Render ribbons for flows */} {sortedFlows.map((flow, i) => { const fromNode = leftNodeMap.get(flow.from) || rightNodeMap.get(flow.from); const toNode = leftNodeMap.get(flow.to) || rightNodeMap.get(flow.to); if (!fromNode || !toNode) return null; const fromPos = fromNode.side === "left" ? leftPositions.get(flow.from)! : rightPositions.get(flow.from)!; const toPos = toNode.side === "left" ? leftPositions.get(flow.to)! : rightPositions.get(flow.to)!; return ( ); })} {/* Render nodes */} {leftNodes.map((node, i) => ( {node.label} ))} {rightNodes.map((node, i) => ( {node.label} ))} {/* Entrance animation: ribbons draw via pathLength */} {/* Already handled via stroke-dashoffset above */} ); }, ); SankeyChart.displayName = "SankeyChart";