UNPKG

sankyy

Version:

<a href="https://nivo.rocks"><img alt="nivo" src="https://raw.githubusercontent.com/plouc/nivo/master/nivo.png" width="216" height="68"/></a>

1 lines 100 kB
{"version":3,"file":"nivo-sankey.mjs","sources":["../src/SankeyNodeTooltip.tsx","../src/SankeyLinkTooltip.tsx","../src/props.ts","../src/hooks.ts","../src/SankeyNodesItem.tsx","../src/SankeyNodes.tsx","../src/SankeyLinkGradient.tsx","../src/SankeyLinksItem.tsx","../src/SankeyLinks.tsx","../src/links.ts","../src/SankeyLabels.tsx","../src/Sankey.tsx","../src/ResponsiveSankey.tsx","../src/DraggableSankey.tsx","../src/calculateOptimalNodePositions.ts"],"sourcesContent":["import { BasicTooltip } from '@nivo/tooltip'\nimport { DefaultLink, DefaultNode, SankeyNodeDatum } from './types'\n\nexport interface SankeyNodeTooltipProps<N extends DefaultNode, L extends DefaultLink> {\n node: SankeyNodeDatum<N, L>\n}\n\nexport const SankeyNodeTooltip = <N extends DefaultNode, L extends DefaultLink>({\n node,\n}: SankeyNodeTooltipProps<N, L>) => (\n <BasicTooltip id={node.label} enableChip={true} color={node.color} />\n)\n","import { BasicTooltip, Chip } from '@nivo/tooltip'\nimport { DefaultLink, DefaultNode, SankeyLinkDatum } from './types'\n\nconst tooltipStyles = {\n container: {\n display: 'flex',\n alignItems: 'center',\n },\n sourceChip: {\n marginRight: 7,\n },\n targetChip: {\n marginLeft: 7,\n marginRight: 7,\n },\n}\n\nexport interface SankeyLinkTooltipProps<N extends DefaultNode, L extends DefaultLink> {\n link: SankeyLinkDatum<N, L>\n}\n\nexport const SankeyLinkTooltip = <N extends DefaultNode, L extends DefaultLink>({\n link,\n}: SankeyLinkTooltipProps<N, L>) => (\n <BasicTooltip\n id={\n <span style={tooltipStyles.container}>\n <Chip color={link.source.color} style={tooltipStyles.sourceChip} />\n <strong>{link.source.label}</strong>\n {' > '}\n <strong>{link.target.label}</strong>\n <Chip color={link.target.color} style={tooltipStyles.targetChip} />\n <strong>{link.formattedValue}</strong>\n </span>\n }\n />\n)\n","import { sankeyCenter, sankeyJustify, sankeyLeft, sankeyRight } from 'd3-sankey'\nimport { SankeyLayerId, SankeyNodeDatum, SankeyAlignType } from './types'\nimport { InheritedColorConfig } from '@nivo/colors'\nimport { Text } from '@nivo/text'\nimport { SankeyNodeTooltip } from './SankeyNodeTooltip'\nimport { SankeyLinkTooltip } from './SankeyLinkTooltip'\n\nexport const sankeyAlignmentPropMapping = {\n center: sankeyCenter,\n justify: sankeyJustify,\n start: sankeyLeft,\n end: sankeyRight,\n}\n\nexport const sankeyAlignmentPropKeys = Object.keys(sankeyAlignmentPropMapping) as SankeyAlignType[]\n\nexport const sankeyAlignmentFromProp = (prop: SankeyAlignType) => sankeyAlignmentPropMapping[prop]\n\nexport const svgDefaultProps = {\n layout: 'horizontal' as const,\n align: 'center' as const,\n sort: 'auto' as const,\n\n colors: { scheme: 'nivo' as const },\n\n nodeOpacity: 0.75,\n nodeHoverOpacity: 1,\n nodeHoverOthersOpacity: 0.15,\n nodeThickness: 12,\n nodeSpacing: 12,\n nodeInnerPadding: 0,\n nodeBorderWidth: 1,\n nodeBorderColor: { from: 'color', modifiers: [['darker', 0.5]] } as InheritedColorConfig<\n SankeyNodeDatum<any, any>\n >,\n nodeBorderRadius: 0,\n\n linkOpacity: 0.25,\n linkHoverOpacity: 0.6,\n linkHoverOthersOpacity: 0.15,\n linkContract: 0,\n linkBlendMode: 'multiply' as const,\n enableLinkGradient: false,\n\n enableLabels: true,\n label: 'id',\n labelPosition: 'inside' as const,\n labelPadding: 9,\n labelOrientation: 'horizontal' as const,\n labelTextColor: { from: 'color', modifiers: [['darker', 0.8]] } as InheritedColorConfig<\n SankeyNodeDatum<any, any>\n >,\n labelComponent: Text,\n\n isInteractive: true,\n nodeTooltip: SankeyNodeTooltip,\n linkTooltip: SankeyLinkTooltip,\n\n legends: [],\n\n layers: ['links', 'nodes', 'labels', 'legends'] as SankeyLayerId[],\n\n role: 'img',\n\n animate: true,\n motionConfig: 'gentle',\n}\n","import { useState, useMemo } from 'react'\nimport cloneDeep from 'lodash/cloneDeep.js'\nimport { sankey as d3Sankey } from 'd3-sankey'\nimport { usePropertyAccessor, useValueFormatter } from '@nivo/core'\nimport { useTheme } from '@nivo/theming'\nimport { useOrdinalColorScale, useInheritedColor } from '@nivo/colors'\nimport { sankeyAlignmentFromProp } from './props'\nimport {\n DefaultLink,\n DefaultNode,\n SankeyAlignFunction,\n SankeyCommonProps,\n SankeyDataProps,\n SankeyLinkDatum,\n SankeyNodeDatum,\n SankeySortFunction,\n} from './types'\n\nconst getId = <N extends DefaultNode>(node: N) => node.id\nexport const computeNodeAndLinks = <N extends DefaultNode, L extends DefaultLink>({\n data: _data,\n formatValue,\n layout,\n alignFunction,\n sortFunction,\n linkSortMode,\n nodeThickness,\n nodeSpacing,\n nodeInnerPadding,\n width,\n height,\n getColor,\n getLabel,\n nodePositions,\n}: {\n data: SankeyDataProps<N, L>['data']\n formatValue: (value: number) => string\n layout: SankeyCommonProps<N, L>['layout']\n alignFunction: SankeyAlignFunction\n sortFunction: null | undefined | SankeySortFunction<N, L>\n linkSortMode: null | undefined\n nodeThickness: SankeyCommonProps<N, L>['nodeThickness']\n nodeSpacing: SankeyCommonProps<N, L>['nodeSpacing']\n nodeInnerPadding: SankeyCommonProps<N, L>['nodeInnerPadding']\n width: number\n height: number\n getColor: (node: Omit<SankeyNodeDatum<N, L>, 'color' | 'label'>) => string\n getLabel: (node: Omit<SankeyNodeDatum<N, L>, 'color' | 'label'>) => string\n nodePositions?: Record<string, { x?: number; y?: number }>\n}) => {\n const sankey = d3Sankey()\n .nodeAlign(alignFunction)\n .nodeSort(sortFunction as any)\n // @ts-expect-error linkSort exists in d3-sankey but is missing from type definitions\n .linkSort(linkSortMode)\n .nodeWidth(nodeThickness)\n .nodePadding(nodeSpacing)\n .size(layout === 'horizontal' ? [width, height] : [height, width])\n .nodeId(getId)\n\n // deep clone is required as the sankey diagram mutates data\n // we need a different identity for correct updates\n const data = cloneDeep(_data) as unknown as {\n nodes: SankeyNodeDatum<N, L>[]\n links: SankeyLinkDatum<N, L>[]\n }\n sankey(data)\n\n // Group nodes by their column (depth) so we can adjust\n // intermediary nodes *after* we know how much space the other\n // nodes in that column occupy.\n const columnMap = new Map<number, SankeyNodeDatum<N, L>[]>()\n\n data.nodes.forEach(node => {\n const col = node.depth\n if (!columnMap.has(col)) columnMap.set(col, [])\n columnMap.get(col)!.push(node)\n })\n\n // ------------------------------------------------------------------\n // Ensure each column starts *below* the previous column\n // ------------------------------------------------------------------\n const depths = Array.from(columnMap.keys()).sort((a, b) => a - b)\n\n for (let i = 1; i < depths.length; i++) {\n const prevNodes = columnMap.get(depths[i - 1])!\n const currNodes = columnMap.get(depths[i])!\n\n if (!prevNodes || !currNodes) continue\n\n const prevBottom = Math.max(...prevNodes.map(n => (layout === 'horizontal' ? n.y1 : n.x1)))\n\n // Classify nodes\n const isInter = (n: SankeyNodeDatum<N, L>) =>\n n.sourceLinks.length > 0 && n.targetLinks.length > 0\n\n const intermediates = currNodes\n .filter(isInter)\n .sort((a, b) => (layout === 'horizontal' ? a.y0 - b.y0 : a.x0 - b.x0))\n\n const nonInter = currNodes.filter(n => !isInter(n))\n\n const nonInterBottom = nonInter.length\n ? Math.max(...nonInter.map(n => (layout === 'horizontal' ? n.y1 : n.x1)))\n : 0\n\n const SPACING = 30\n if (intermediates.length) {\n const desiredBottom = Math.max(prevBottom, nonInterBottom) + SPACING\n\n // Start by placing the bottom-most intermediary so its bottom edge\n // sits at desiredBottom, then stack the others immediately above it\n // keeping exactly SPACING between nodes.\n\n // We work bottom-up so we can keep a running cursor of where the\n // previous node starts.\n\n const ordered = [...intermediates].sort((a, b) =>\n layout === 'horizontal' ? b.y1 - a.y1 : b.x1 - a.x1\n ) // bottom-most first\n\n let cursorBottom = desiredBottom\n\n ordered.forEach(node => {\n const nodeHeight = layout === 'horizontal' ? node.y1 - node.y0 : node.x1 - node.x0\n\n const newBottom = cursorBottom\n const newTop = newBottom - nodeHeight\n\n const shift = newTop - (layout === 'horizontal' ? node.y0 : node.x0)\n\n if (layout === 'horizontal') {\n node.y0 += shift\n node.y1 += shift\n node.y += shift\n } else {\n node.x0 += shift\n node.x1 += shift\n node.x += shift\n }\n\n cursorBottom = newTop - SPACING\n })\n }\n }\n\n // Re-compute supplemental properties that depend on color/label/etc.\n data.nodes.forEach(node => {\n // Apply explicit positions provided via nodePositions map first\n if (nodePositions) {\n const override = nodePositions[node.id as unknown as string]\n if (override) {\n if (override.x !== undefined) node.manualX = override.x\n if (override.y !== undefined) node.manualY = override.y\n }\n }\n\n node.color = getColor(node)\n node.label = getLabel(node)\n node.formattedValue = formatValue(node.value)\n\n // Apply manual positions if provided\n if ('manualX' in node && node.manualX !== undefined) {\n const thickness = node.x1 - node.x0\n node.x0 = node.manualX\n node.x1 = node.manualX + thickness\n }\n\n if ('manualY' in node && node.manualY !== undefined) {\n const h = node.y1 - node.y0\n node.y0 = node.manualY\n node.y1 = node.manualY + h\n }\n\n if (layout === 'horizontal') {\n node.x = node.x0 + nodeInnerPadding\n node.y = node.y0\n node.width = Math.max(node.x1 - node.x0 - nodeInnerPadding * 2, 0)\n node.height = Math.max(node.y1 - node.y0, 0)\n } else {\n node.x = node.y0\n node.y = node.x0 + nodeInnerPadding\n node.width = Math.max(node.y1 - node.y0, 0)\n node.height = Math.max(node.x1 - node.x0 - nodeInnerPadding * 2, 0)\n\n const oldX0 = node.x0\n const oldX1 = node.x1\n\n node.x0 = node.y0\n node.x1 = node.y1\n node.y0 = oldX0\n node.y1 = oldX1\n }\n })\n\n data.links.forEach(link => {\n link.formattedValue = formatValue(link.value)\n link.color = link.source.color\n // @ts-expect-error: @types/d3-sankey\n link.thickness = link.width\n })\n\n // ------------------------------------------------------------------\n // Re-compute link positions so they follow nodes after any manual\n // node positioning override. We compute each link vertical/horizontal\n // offset by stacking the link thicknesses within its source and target\n // node. This replaces the original y0/y1 based positions coming from\n // d3-sankey which are now out of sync once nodes have been moved.\n // ------------------------------------------------------------------\n const lastDepth = Math.max(...data.nodes.map(n => n.depth))\n\n data.nodes.forEach((node: SankeyNodeDatum<N, L>) => {\n if (layout === 'horizontal') {\n // Outgoing links (left → right)\n let sy = 0\n const srcLinks =\n node.depth === 0\n ? [...node.sourceLinks].sort((a, b) => {\n const aPriority = a.target.depth === lastDepth ? 0 : 1\n const bPriority = b.target.depth === lastDepth ? 0 : 1\n return aPriority - bPriority\n })\n : node.sourceLinks\n\n srcLinks.forEach(link => {\n link.pos0 = node.y0 + sy + link.thickness / 2\n sy += link.thickness\n })\n\n // Incoming links (left ← right)\n let ty = 0\n node.targetLinks.forEach(link => {\n link.pos1 = node.y0 + ty + link.thickness / 2\n ty += link.thickness\n })\n } else {\n // Vertical layout, we stack along the X axis instead of Y.\n let sx = 0\n node.sourceLinks.forEach(link => {\n link.pos0 = node.x0 + sx + link.thickness / 2\n sx += link.thickness\n })\n\n let tx = 0\n node.targetLinks.forEach(link => {\n link.pos1 = node.x0 + tx + link.thickness / 2\n tx += link.thickness\n })\n }\n })\n\n return data\n}\n\nexport const useSankey = <N extends DefaultNode, L extends DefaultLink>({\n data,\n valueFormat,\n layout,\n width,\n height,\n sort,\n align,\n colors,\n nodeThickness,\n nodeSpacing,\n nodeInnerPadding,\n nodeBorderColor,\n label,\n labelTextColor,\n nodePositions,\n}: {\n data: SankeyDataProps<N, L>['data']\n valueFormat?: SankeyCommonProps<N, L>['valueFormat']\n layout: SankeyCommonProps<N, L>['layout']\n width: number\n height: number\n sort: SankeyCommonProps<N, L>['sort']\n align: SankeyCommonProps<N, L>['align']\n colors: SankeyCommonProps<N, L>['colors']\n nodeThickness: SankeyCommonProps<N, L>['nodeThickness']\n nodeSpacing: SankeyCommonProps<N, L>['nodeSpacing']\n nodeInnerPadding: SankeyCommonProps<N, L>['nodeInnerPadding']\n nodeBorderColor: SankeyCommonProps<N, L>['nodeBorderColor']\n label: SankeyCommonProps<N, L>['label']\n labelTextColor: SankeyCommonProps<N, L>['labelTextColor']\n nodePositions?: Record<string, { x?: number; y?: number }>\n}) => {\n const [currentNode, setCurrentNode] = useState<SankeyNodeDatum<N, L> | null>(null)\n const [currentLink, setCurrentLink] = useState<SankeyLinkDatum<N, L> | null>(null)\n\n const sortFunction = useMemo(() => {\n if (sort === 'auto') return undefined\n if (sort === 'input') return null\n if (sort === 'ascending') {\n return (a: SankeyNodeDatum<N, L>, b: SankeyNodeDatum<N, L>) => a.value - b.value\n }\n if (sort === 'descending') {\n return (a: SankeyNodeDatum<N, L>, b: SankeyNodeDatum<N, L>) => b.value - a.value\n }\n\n return sort\n }, [sort])\n\n // If \"input\" sorting is used, apply this setting to links, too.\n // (In d3, `null` means input sorting and `undefined` is the default)\n const linkSortMode = sort === 'input' ? null : undefined\n\n const alignFunction = useMemo(() => {\n if (typeof align === 'function') return align\n return sankeyAlignmentFromProp(align)\n }, [align])\n\n const theme = useTheme()\n\n const getColor = useOrdinalColorScale(colors, 'id')\n const getNodeBorderColor = useInheritedColor(nodeBorderColor, theme)\n\n const getLabel = usePropertyAccessor<Omit<SankeyNodeDatum<N, L>, 'color' | 'label'>, string>(\n label\n )\n const getLabelTextColor = useInheritedColor(labelTextColor, theme)\n const formatValue = useValueFormatter<number>(valueFormat)\n\n const { nodes, links } = useMemo(\n () =>\n computeNodeAndLinks<N, L>({\n data,\n formatValue,\n layout,\n alignFunction,\n sortFunction,\n linkSortMode,\n nodeThickness,\n nodeSpacing,\n nodeInnerPadding,\n width,\n height,\n getColor,\n getLabel,\n nodePositions,\n }),\n [\n data,\n formatValue,\n layout,\n alignFunction,\n sortFunction,\n linkSortMode,\n nodeThickness,\n nodeSpacing,\n nodeInnerPadding,\n width,\n height,\n getColor,\n getLabel,\n nodePositions,\n ]\n )\n\n const legendData = useMemo(\n () =>\n nodes.map((node: SankeyNodeDatum<N, L>) => ({\n id: node.id,\n label: node.label,\n color: node.color,\n })),\n [nodes]\n )\n\n return {\n nodes,\n links,\n legendData,\n getNodeBorderColor,\n currentNode,\n setCurrentNode,\n currentLink,\n setCurrentLink,\n getLabelTextColor,\n }\n}\n","import { createElement, useCallback, MouseEvent } from 'react'\nimport { useSpring, animated } from '@react-spring/web'\nimport { useMotionConfig } from '@nivo/core'\nimport { useTooltip } from '@nivo/tooltip'\nimport { DefaultLink, DefaultNode, SankeyCommonProps, SankeyNodeDatum } from './types'\n\ninterface SankeyNodesItemProps<N extends DefaultNode, L extends DefaultLink> {\n node: SankeyNodeDatum<N, L>\n x: number\n y: number\n width: number\n height: number\n color: string\n opacity: number\n borderWidth: SankeyCommonProps<N, L>['nodeBorderWidth']\n borderColor: string\n borderRadius: SankeyCommonProps<N, L>['nodeBorderRadius']\n setCurrent: (node: SankeyNodeDatum<N, L> | null) => void\n isInteractive: SankeyCommonProps<N, L>['isInteractive']\n onClick?: SankeyCommonProps<N, L>['onClick']\n tooltip: SankeyCommonProps<N, L>['nodeTooltip']\n}\n\nexport const SankeyNodesItem = <N extends DefaultNode, L extends DefaultLink>({\n node,\n x,\n y,\n width,\n height,\n color,\n opacity,\n borderWidth,\n borderColor,\n borderRadius,\n setCurrent,\n isInteractive,\n onClick,\n tooltip,\n}: SankeyNodesItemProps<N, L>) => {\n const { animate, config: springConfig } = useMotionConfig()\n const animatedProps = useSpring({\n x,\n y,\n width,\n height,\n opacity,\n color,\n config: springConfig,\n immediate: !animate,\n })\n\n const { showTooltipFromEvent, hideTooltip } = useTooltip()\n\n const handleMouseEnter = useCallback(\n (event: MouseEvent<SVGRectElement>) => {\n setCurrent(node)\n showTooltipFromEvent(createElement(tooltip, { node }), event, 'left')\n },\n [setCurrent, node, showTooltipFromEvent, tooltip]\n )\n\n const handleMouseMove = useCallback(\n (event: MouseEvent<SVGRectElement>) => {\n showTooltipFromEvent(createElement(tooltip, { node }), event, 'left')\n },\n [showTooltipFromEvent, node, tooltip]\n )\n\n const handleMouseLeave = useCallback(() => {\n setCurrent(null)\n hideTooltip()\n }, [setCurrent, hideTooltip])\n\n const handleClick = useCallback(\n (event: MouseEvent<SVGRectElement>) => {\n onClick?.(node, event)\n },\n [onClick, node]\n )\n\n return (\n <animated.rect\n x={animatedProps.x}\n y={animatedProps.y}\n rx={borderRadius}\n ry={borderRadius}\n width={animatedProps.width.to(v => Math.max(v, 0))}\n height={animatedProps.height.to(v => Math.max(v, 0))}\n fill={animatedProps.color}\n fillOpacity={animatedProps.opacity}\n strokeWidth={borderWidth}\n stroke={borderColor}\n strokeOpacity={opacity}\n onMouseEnter={isInteractive ? handleMouseEnter : undefined}\n onMouseMove={isInteractive ? handleMouseMove : undefined}\n onMouseLeave={isInteractive ? handleMouseLeave : undefined}\n onClick={isInteractive ? handleClick : undefined}\n />\n )\n}\n","import {\n DefaultLink,\n DefaultNode,\n SankeyCommonProps,\n SankeyLinkDatum,\n SankeyNodeDatum,\n} from './types'\nimport { SankeyNodesItem } from './SankeyNodesItem'\n\ninterface SankeyNodesProps<N extends DefaultNode, L extends DefaultLink> {\n nodes: SankeyNodeDatum<N, L>[]\n nodeOpacity: SankeyCommonProps<N, L>['nodeOpacity']\n nodeHoverOpacity: SankeyCommonProps<N, L>['nodeHoverOpacity']\n nodeHoverOthersOpacity: SankeyCommonProps<N, L>['nodeHoverOthersOpacity']\n borderWidth: SankeyCommonProps<N, L>['nodeBorderWidth']\n getBorderColor: (node: SankeyNodeDatum<N, L>) => string\n borderRadius: SankeyCommonProps<N, L>['nodeBorderRadius']\n setCurrentNode: (node: SankeyNodeDatum<N, L> | null) => void\n currentNode: SankeyNodeDatum<N, L> | null\n currentLink: SankeyLinkDatum<N, L> | null\n isCurrentNode: (node: SankeyNodeDatum<N, L>) => boolean\n isInteractive: SankeyCommonProps<N, L>['isInteractive']\n onClick?: SankeyCommonProps<N, L>['onClick']\n tooltip: SankeyCommonProps<N, L>['nodeTooltip']\n}\n\nexport const SankeyNodes = <N extends DefaultNode, L extends DefaultLink>({\n nodes,\n nodeOpacity,\n nodeHoverOpacity,\n nodeHoverOthersOpacity,\n borderWidth,\n getBorderColor,\n borderRadius,\n setCurrentNode,\n currentNode,\n currentLink,\n isCurrentNode,\n isInteractive,\n onClick,\n tooltip,\n}: SankeyNodesProps<N, L>) => {\n const getOpacity = (node: SankeyNodeDatum<N, L>) => {\n if (!currentNode && !currentLink) return nodeOpacity\n if (isCurrentNode(node)) return nodeHoverOpacity\n return nodeHoverOthersOpacity\n }\n\n return (\n <>\n {nodes.map(node => (\n <SankeyNodesItem<N, L>\n key={node.id}\n node={node}\n x={node.x}\n y={node.y}\n width={node.width}\n height={node.height}\n color={node.color}\n opacity={getOpacity(node)}\n borderWidth={borderWidth}\n borderColor={getBorderColor(node)}\n borderRadius={borderRadius}\n setCurrent={setCurrentNode}\n isInteractive={isInteractive}\n onClick={onClick}\n tooltip={tooltip}\n />\n ))}\n </>\n )\n}\n","import { SankeyCommonProps } from './types'\n\ninterface SankeyLinkGradientProps {\n id: string\n layout: SankeyCommonProps<any, any>['layout']\n startColor: string\n endColor: string\n}\n\nexport const SankeyLinkGradient = ({\n id,\n layout,\n startColor,\n endColor,\n}: SankeyLinkGradientProps) => {\n let gradientProps: {\n x1: string\n x2: string\n y1: string\n y2: string\n }\n if (layout === 'horizontal') {\n gradientProps = {\n x1: '0%',\n x2: '100%',\n y1: '0%',\n y2: '0%',\n }\n } else {\n gradientProps = {\n x1: '0%',\n x2: '0%',\n y1: '0%',\n y2: '100%',\n }\n }\n\n return (\n <linearGradient id={id} spreadMethod=\"pad\" {...gradientProps}>\n <stop offset=\"0%\" stopColor={startColor} />\n <stop offset=\"100%\" stopColor={endColor} />\n </linearGradient>\n )\n}\n","import { createElement, useCallback, MouseEvent } from 'react'\nimport { useSpring, animated } from '@react-spring/web'\nimport { useAnimatedPath, useMotionConfig } from '@nivo/core'\nimport { useTooltip } from '@nivo/tooltip'\nimport { SankeyLinkGradient } from './SankeyLinkGradient'\nimport { DefaultLink, DefaultNode, SankeyCommonProps, SankeyLinkDatum } from './types'\n\ninterface SankeyLinksItemProps<N extends DefaultNode, L extends DefaultLink> {\n link: SankeyLinkDatum<N, L>\n layout: SankeyCommonProps<N, L>['layout']\n path: string\n color: string\n opacity: number\n blendMode: SankeyCommonProps<N, L>['linkBlendMode']\n enableGradient: SankeyCommonProps<N, L>['enableLinkGradient']\n setCurrent: (link: SankeyLinkDatum<N, L> | null) => void\n isInteractive: SankeyCommonProps<N, L>['isInteractive']\n onClick?: SankeyCommonProps<N, L>['onClick']\n tooltip: SankeyCommonProps<N, L>['linkTooltip']\n}\n\nexport const SankeyLinksItem = <N extends DefaultNode, L extends DefaultLink>({\n link,\n layout,\n path,\n color,\n opacity,\n blendMode,\n enableGradient,\n setCurrent,\n tooltip,\n isInteractive,\n onClick,\n}: SankeyLinksItemProps<N, L>) => {\n const linkId = `${link.source.id}.${link.target.id}.${link.index}`\n\n const { animate, config: springConfig } = useMotionConfig()\n const animatedPath = useAnimatedPath(path)\n const animatedProps = useSpring({\n color,\n opacity,\n config: springConfig,\n immediate: !animate,\n })\n\n const { showTooltipFromEvent, hideTooltip } = useTooltip()\n\n const handleMouseEnter = useCallback(\n (event: MouseEvent<SVGPathElement>) => {\n setCurrent(link)\n showTooltipFromEvent(createElement(tooltip, { link }), event, 'left')\n },\n [setCurrent, link, showTooltipFromEvent, tooltip]\n )\n\n const handleMouseMove = useCallback(\n (event: MouseEvent<SVGPathElement>) => {\n showTooltipFromEvent(createElement(tooltip, { link }), event, 'left')\n },\n [showTooltipFromEvent, link, tooltip]\n )\n\n const handleMouseLeave = useCallback(() => {\n setCurrent(null)\n hideTooltip()\n }, [setCurrent, hideTooltip])\n\n const handleClick = useCallback(\n (event: MouseEvent<SVGPathElement>) => {\n onClick?.(link, event)\n },\n [onClick, link]\n )\n\n return (\n <>\n {enableGradient && (\n <SankeyLinkGradient\n id={linkId}\n layout={layout}\n startColor={link.startColor || link.source.color}\n endColor={link.endColor || link.target.color}\n />\n )}\n <animated.path\n fill={enableGradient ? `url(\"#${encodeURI(linkId)}\")` : animatedProps.color}\n d={animatedPath}\n fillOpacity={animatedProps.opacity}\n onMouseEnter={isInteractive ? handleMouseEnter : undefined}\n onMouseMove={isInteractive ? handleMouseMove : undefined}\n onMouseLeave={isInteractive ? handleMouseLeave : undefined}\n onClick={isInteractive ? handleClick : undefined}\n style={{ mixBlendMode: blendMode }}\n />\n </>\n )\n}\n","import { sankeyLinkHorizontal, sankeyLinkVertical } from './links'\nimport {\n DefaultLink,\n DefaultNode,\n SankeyCommonProps,\n SankeyLinkDatum,\n SankeyNodeDatum,\n} from './types'\nimport { SankeyLinksItem } from './SankeyLinksItem'\nimport { useMemo } from 'react'\n\ninterface SankeyLinksProps<N extends DefaultNode, L extends DefaultLink> {\n layout: SankeyCommonProps<N, L>['layout']\n links: SankeyLinkDatum<N, L>[]\n linkOpacity: SankeyCommonProps<N, L>['linkOpacity']\n linkHoverOpacity: SankeyCommonProps<N, L>['linkHoverOpacity']\n linkHoverOthersOpacity: SankeyCommonProps<N, L>['linkHoverOthersOpacity']\n linkContract: SankeyCommonProps<N, L>['linkContract']\n linkBlendMode: SankeyCommonProps<N, L>['linkBlendMode']\n enableLinkGradient: SankeyCommonProps<N, L>['enableLinkGradient']\n tooltip: SankeyCommonProps<N, L>['linkTooltip']\n setCurrentLink: (link: SankeyLinkDatum<N, L> | null) => void\n currentLink: SankeyLinkDatum<N, L> | null\n currentNode: SankeyNodeDatum<N, L> | null\n isCurrentLink: (link: SankeyLinkDatum<N, L>) => boolean\n isInteractive: SankeyCommonProps<N, L>['isInteractive']\n onClick?: SankeyCommonProps<N, L>['onClick']\n}\n\nexport const SankeyLinks = <N extends DefaultNode, L extends DefaultLink>({\n links,\n layout,\n linkOpacity,\n linkHoverOpacity,\n linkHoverOthersOpacity,\n linkContract,\n linkBlendMode,\n enableLinkGradient,\n setCurrentLink,\n currentLink,\n currentNode,\n isCurrentLink,\n isInteractive,\n onClick,\n tooltip,\n}: SankeyLinksProps<N, L>) => {\n const getOpacity = (link: SankeyLinkDatum<N, L>) => {\n if (!currentNode && !currentLink) return linkOpacity\n if (isCurrentLink(link)) return linkHoverOpacity\n return linkHoverOthersOpacity\n }\n\n const getLinkPath = useMemo(\n () => (layout === 'horizontal' ? sankeyLinkHorizontal() : sankeyLinkVertical()),\n [layout]\n )\n\n return (\n <>\n {links.map(link => (\n <SankeyLinksItem<N, L>\n key={`${link.source.id}.${link.target.id}.${link.index}`}\n link={link}\n layout={layout}\n path={getLinkPath(link, linkContract)}\n color={link.color}\n opacity={getOpacity(link)}\n blendMode={linkBlendMode}\n enableGradient={enableLinkGradient}\n setCurrent={setCurrentLink}\n isInteractive={isInteractive}\n onClick={onClick}\n tooltip={tooltip}\n />\n ))}\n </>\n )\n}\n","import { line, curveMonotoneX, curveMonotoneY } from 'd3-shape'\nimport { DefaultLink, DefaultNode, SankeyLinkDatum } from './types'\n\nexport const sankeyLinkHorizontal = <N extends DefaultNode, L extends DefaultLink>() => {\n const lineGenerator = line().curve(curveMonotoneX)\n\n return (link: SankeyLinkDatum<N, L>, contract: number) => {\n const thickness = Math.max(1, link.thickness - contract * 2)\n const halfThickness = thickness / 2\n const linkLength = link.target.x0 - link.source.x1\n const padLength = linkLength * 0.12\n\n const dots: [number, number][] = [\n [link.source.x1, link.pos0 - halfThickness],\n [link.source.x1 + padLength, link.pos0 - halfThickness],\n [link.target.x0 - padLength, link.pos1 - halfThickness],\n [link.target.x0, link.pos1 - halfThickness],\n [link.target.x0, link.pos1 + halfThickness],\n [link.target.x0 - padLength, link.pos1 + halfThickness],\n [link.source.x1 + padLength, link.pos0 + halfThickness],\n [link.source.x1, link.pos0 + halfThickness],\n [link.source.x1, link.pos0 - halfThickness],\n ]\n\n return lineGenerator(dots) + 'Z'\n }\n}\n\nexport const sankeyLinkVertical = <N extends DefaultNode, L extends DefaultLink>() => {\n const lineGenerator = line().curve(curveMonotoneY)\n\n return (link: SankeyLinkDatum<N, L>, contract: number) => {\n const thickness = Math.max(1, link.thickness - contract * 2)\n const halfThickness = thickness / 2\n const linkLength = link.target.y0 - link.source.y1\n const padLength = linkLength * 0.12\n\n const dots: [number, number][] = [\n [link.pos0 + halfThickness, link.source.y1],\n [link.pos0 + halfThickness, link.source.y1 + padLength],\n [link.pos1 + halfThickness, link.target.y0 - padLength],\n [link.pos1 + halfThickness, link.target.y0],\n [link.pos1 - halfThickness, link.target.y0],\n [link.pos1 - halfThickness, link.target.y0 - padLength],\n [link.pos0 - halfThickness, link.source.y1 + padLength],\n [link.pos0 - halfThickness, link.source.y1],\n [link.pos0 + halfThickness, link.source.y1],\n ]\n\n return lineGenerator(dots) + 'Z'\n }\n}\n","import { useSprings } from '@react-spring/web'\nimport { useMotionConfig } from '@nivo/core'\nimport { useTheme } from '@nivo/theming'\nimport { DefaultLink, DefaultNode, SankeyCommonProps, SankeyNodeDatum } from './types'\n\ninterface SankeyLabelsProps<N extends DefaultNode, L extends DefaultLink> {\n nodes: SankeyNodeDatum<N, L>[]\n layout: SankeyCommonProps<N, L>['layout']\n width: number\n height: number\n labelComponent: SankeyCommonProps<N, L>['labelComponent']\n labelPosition: SankeyCommonProps<N, L>['labelPosition']\n labelPadding: SankeyCommonProps<N, L>['labelPadding']\n labelOrientation: SankeyCommonProps<N, L>['labelOrientation']\n getLabelTextColor: (node: SankeyNodeDatum<N, L>) => string\n}\n\nexport const SankeyLabels = <N extends DefaultNode, L extends DefaultLink>({\n nodes,\n layout,\n width,\n height,\n labelPosition,\n labelPadding,\n labelOrientation,\n getLabelTextColor,\n labelComponent: LabelComponent,\n}: SankeyLabelsProps<N, L>) => {\n const theme = useTheme()\n\n const labelRotation = labelOrientation === 'vertical' ? -90 : 0\n const labels = nodes.map(node => {\n let x\n let y\n let textAnchor: 'middle' | 'start' | 'end' | undefined\n if (layout === 'horizontal') {\n y = node.y + node.height / 2\n if (node.x < width / 2) {\n if (labelPosition === 'inside') {\n x = node.x1 + labelPadding\n textAnchor = labelOrientation === 'vertical' ? 'middle' : 'start'\n } else {\n x = node.x - labelPadding\n textAnchor = labelOrientation === 'vertical' ? 'middle' : 'end'\n }\n } else {\n if (labelPosition === 'inside') {\n x = node.x - labelPadding\n textAnchor = labelOrientation === 'vertical' ? 'middle' : 'end'\n } else {\n x = node.x1 + labelPadding\n textAnchor = labelOrientation === 'vertical' ? 'middle' : 'start'\n }\n }\n } else if (layout === 'vertical') {\n x = node.x + node.width / 2\n if (node.y < height / 2) {\n if (labelPosition === 'inside') {\n y = node.y1 + labelPadding\n textAnchor = labelOrientation === 'vertical' ? 'end' : 'middle'\n } else {\n y = node.y - labelPadding\n textAnchor = labelOrientation === 'vertical' ? 'start' : 'middle'\n }\n } else {\n if (labelPosition === 'inside') {\n y = node.y - labelPadding\n textAnchor = labelOrientation === 'vertical' ? 'start' : 'middle'\n } else {\n y = node.y1 + labelPadding\n textAnchor = labelOrientation === 'vertical' ? 'end' : 'middle'\n }\n }\n }\n\n return {\n id: node.id,\n label: node.label,\n x,\n y,\n textAnchor,\n color: getLabelTextColor(node),\n }\n })\n\n const { animate, config: springConfig } = useMotionConfig()\n const springs = useSprings(\n labels.length,\n labels.map(label => ({\n transform: `translate(${label.x}, ${label.y}) rotate(${labelRotation})`,\n color: label.color,\n config: springConfig,\n immediate: !animate,\n }))\n )\n\n return (\n <>\n {springs.map((animatedProps, index) => {\n const label = labels[index]\n\n return (\n <LabelComponent\n key={label.id}\n dominantBaseline=\"central\"\n textAnchor={label.textAnchor}\n transform={animatedProps.transform}\n style={{\n ...theme.labels.text,\n fill: animatedProps.color,\n pointerEvents: 'none',\n }}\n node={nodes[index]}\n >\n {label.label}\n </LabelComponent>\n )\n })}\n </>\n )\n}\n","import { createElement, Fragment, ReactNode, useMemo } from 'react'\nimport uniq from 'lodash/uniq.js'\nimport { SvgWrapper, useDimensions, Container } from '@nivo/core'\nimport { BoxLegendSvg } from '@nivo/legends'\nimport { svgDefaultProps } from './props'\nimport { useSankey } from './hooks'\nimport { SankeyNodes } from './SankeyNodes'\nimport { SankeyLinks } from './SankeyLinks'\nimport { SankeyLabels } from './SankeyLabels'\nimport {\n DefaultLink,\n DefaultNode,\n SankeyLayerId,\n SankeyLinkDatum,\n SankeyNodeDatum,\n SankeySvgProps,\n} from './types'\n\ntype InnerSankeyProps<N extends DefaultNode, L extends DefaultLink> = Omit<\n SankeySvgProps<N, L>,\n 'animate' | 'motionConfig' | 'renderWrapper' | 'theme'\n>\n\nconst InnerSankey = <N extends DefaultNode, L extends DefaultLink>({\n data,\n valueFormat,\n layout = svgDefaultProps.layout,\n sort = svgDefaultProps.sort,\n align = svgDefaultProps.align,\n width,\n height,\n margin: partialMargin,\n colors = svgDefaultProps.colors,\n nodeThickness = svgDefaultProps.nodeThickness,\n nodeSpacing = svgDefaultProps.nodeThickness,\n nodeInnerPadding = svgDefaultProps.nodeInnerPadding,\n nodePositions,\n nodeBorderColor = svgDefaultProps.nodeBorderColor,\n nodeOpacity = svgDefaultProps.nodeOpacity,\n nodeHoverOpacity = svgDefaultProps.nodeHoverOpacity,\n nodeHoverOthersOpacity = svgDefaultProps.nodeHoverOthersOpacity,\n nodeBorderWidth = svgDefaultProps.nodeBorderWidth,\n nodeBorderRadius = svgDefaultProps.nodeBorderRadius,\n linkOpacity = svgDefaultProps.linkOpacity,\n linkHoverOpacity = svgDefaultProps.linkHoverOpacity,\n linkHoverOthersOpacity = svgDefaultProps.linkHoverOthersOpacity,\n linkContract = svgDefaultProps.linkContract,\n linkBlendMode = svgDefaultProps.linkBlendMode,\n enableLinkGradient = svgDefaultProps.enableLinkGradient,\n enableLabels = svgDefaultProps.enableLabels,\n labelComponent = svgDefaultProps.labelComponent,\n labelPosition = svgDefaultProps.labelPosition,\n labelPadding = svgDefaultProps.labelPadding,\n labelOrientation = svgDefaultProps.labelOrientation,\n label = svgDefaultProps.label,\n labelTextColor = svgDefaultProps.labelTextColor,\n nodeTooltip = svgDefaultProps.nodeTooltip,\n linkTooltip = svgDefaultProps.linkTooltip,\n isInteractive = svgDefaultProps.isInteractive,\n onClick,\n legends = svgDefaultProps.legends,\n layers = svgDefaultProps.layers,\n role = svgDefaultProps.role,\n ariaLabel,\n ariaLabelledBy,\n ariaDescribedBy,\n}: InnerSankeyProps<N, L>) => {\n const { margin, innerWidth, innerHeight, outerWidth, outerHeight } = useDimensions(\n width,\n height,\n partialMargin\n )\n\n const {\n nodes,\n links,\n legendData,\n getNodeBorderColor,\n currentNode,\n setCurrentNode,\n currentLink,\n setCurrentLink,\n getLabelTextColor,\n } = useSankey<N, L>({\n data,\n valueFormat,\n layout,\n width: innerWidth,\n height: innerHeight,\n sort,\n align,\n colors,\n nodeThickness,\n nodeSpacing,\n nodeInnerPadding,\n nodePositions,\n nodeBorderColor,\n label,\n labelTextColor,\n })\n\n const { isCurrentNode, isCurrentLink } = useMemo(() => {\n let isCurrentNode: (node: SankeyNodeDatum<N, L>) => boolean = () => false\n let isCurrentLink: (link: SankeyLinkDatum<N, L>) => boolean = () => false\n\n if (currentLink) {\n isCurrentNode = ({ id }) => id === currentLink.source.id || id === currentLink.target.id\n isCurrentLink = ({ source, target }) =>\n source.id === currentLink.source.id && target.id === currentLink.target.id\n }\n\n if (currentNode) {\n let currentNodeIds = [currentNode.id]\n links\n .filter(\n ({ source, target }) =>\n source.id === currentNode.id || target.id === currentNode.id\n )\n .forEach(({ source, target }) => {\n currentNodeIds.push(source.id)\n currentNodeIds.push(target.id)\n })\n currentNodeIds = uniq(currentNodeIds)\n\n isCurrentNode = ({ id }) => currentNodeIds.includes(id)\n isCurrentLink = ({ source, target }) =>\n source.id === currentNode.id || target.id === currentNode.id\n }\n\n return {\n isCurrentNode,\n isCurrentLink,\n }\n }, [currentLink, currentNode, links])\n\n const layerProps = useMemo(\n () => ({\n links,\n nodes,\n margin,\n width,\n height,\n outerWidth,\n outerHeight,\n currentNode,\n isCurrentNode,\n setCurrentNode,\n currentLink,\n isCurrentLink,\n setCurrentLink,\n isInteractive,\n }),\n [\n links,\n nodes,\n margin,\n width,\n height,\n outerWidth,\n outerHeight,\n currentNode,\n isCurrentNode,\n setCurrentNode,\n currentLink,\n isCurrentLink,\n setCurrentLink,\n isInteractive,\n ]\n )\n\n const layerById: Record<SankeyLayerId, ReactNode> = {\n links: null,\n nodes: null,\n labels: null,\n legends: null,\n }\n\n if (layers.includes('links')) {\n layerById.links = (\n <SankeyLinks<N, L>\n key=\"links\"\n links={links}\n layout={layout}\n linkContract={linkContract}\n linkOpacity={linkOpacity}\n linkHoverOpacity={linkHoverOpacity}\n linkHoverOthersOpacity={linkHoverOthersOpacity}\n linkBlendMode={linkBlendMode}\n enableLinkGradient={enableLinkGradient}\n setCurrentLink={setCurrentLink}\n currentNode={currentNode}\n currentLink={currentLink}\n isCurrentLink={isCurrentLink}\n isInteractive={isInteractive}\n onClick={onClick}\n tooltip={linkTooltip}\n />\n )\n }\n\n if (layers.includes('nodes')) {\n layerById.nodes = (\n <SankeyNodes<N, L>\n key=\"nodes\"\n nodes={nodes}\n nodeOpacity={nodeOpacity}\n nodeHoverOpacity={nodeHoverOpacity}\n nodeHoverOthersOpacity={nodeHoverOthersOpacity}\n borderWidth={nodeBorderWidth}\n borderRadius={nodeBorderRadius}\n getBorderColor={getNodeBorderColor}\n setCurrentNode={setCurrentNode}\n currentNode={currentNode}\n currentLink={currentLink}\n isCurrentNode={isCurrentNode}\n isInteractive={isInteractive}\n onClick={onClick}\n tooltip={nodeTooltip}\n />\n )\n }\n\n if (layers.includes('labels') && enableLabels) {\n layerById.labels = (\n <SankeyLabels<N, L>\n key=\"labels\"\n nodes={nodes}\n layout={layout}\n width={innerWidth}\n height={innerHeight}\n labelPosition={labelPosition}\n labelPadding={labelPadding}\n labelOrientation={labelOrientation}\n getLabelTextColor={getLabelTextColor}\n labelComponent={labelComponent}\n />\n )\n }\n\n if (layers.includes('legends')) {\n layerById.legends = (\n <Fragment key=\"legends\">\n {legends.map((legend, i) => (\n <BoxLegendSvg\n key={`legend${i}`}\n {...legend}\n containerWidth={innerWidth}\n containerHeight={innerHeight}\n data={legendData}\n />\n ))}\n </Fragment>\n )\n }\n\n return (\n <SvgWrapper\n width={outerWidth}\n height={outerHeight}\n margin={margin}\n role={role}\n ariaLabel={ariaLabel}\n ariaLabelledBy={ariaLabelledBy}\n ariaDescribedBy={ariaDescribedBy}\n >\n {layers.map((layer, i) => {\n if (typeof layer === 'function') {\n return <Fragment key={i}>{createElement(layer, layerProps)}</Fragment>\n }\n\n return layerById?.[layer] ?? null\n })}\n </SvgWrapper>\n )\n}\n\nexport const Sankey = <N extends DefaultNode = DefaultNode, L extends DefaultLink = DefaultLink>({\n isInteractive = svgDefaultProps.isInteractive,\n animate = svgDefaultProps.animate,\n motionConfig = svgDefaultProps.motionConfig,\n theme,\n renderWrapper,\n ...otherProps\n}: SankeySvgProps<N, L>) => (\n <Container\n {...{\n animate,\n isInteractive,\n motionConfig,\n renderWrapper,\n theme,\n }}\n >\n <InnerSankey<N, L> isInteractive={isInteractive} {...otherProps} />\n </Container>\n)\n","import { ResponsiveWrapper } from '@nivo/core'\nimport { DefaultLink, DefaultNode, SankeySvgProps } from './types'\nimport { Sankey } from './Sankey'\n\nexport const ResponsiveSankey = <\n N extends DefaultNode = DefaultNode,\n L extends DefaultLink = DefaultLink,\n>(\n props: Omit<SankeySvgProps<N, L>, 'height' | 'width'>\n) => (\n <ResponsiveWrapper>\n {({ width, height }) => <Sankey<N, L> width={width} height={height} {...props} />}\n </ResponsiveWrapper>\n)\n","import {\n forwardRef,\n useCallback,\n useImperativeHandle,\n useRef,\n useState,\n useMemo,\n useEffect,\n} from 'react'\nimport { Sankey } from './Sankey'\nimport {\n DefaultLink,\n DefaultNode,\n SankeyCustomLayer,\n SankeyNodeDatum,\n SankeySvgProps,\n} from './types'\n\n\n\nexport interface DraggableSankeyProps<N extends DefaultNode, L extends DefaultLink>\n extends SankeySvgProps<N, L> {\n /** called on every mousemove while a node is dragged */\n onNodeDrag?: (nodeId: string, x: number, y: number) => void\n /** called once on mouseup (drag finished) */\n onNodeDragEnd?: (nodeId: string, x: number, y: number) => void\n /**\n * Manually override the vertical positions of nodes.\n * Provide a map from node.id to an object containing a `y` value.\n * The node's x-coordinate will still be computed by d3-sankey.\n */\n initialNodePositions?: Record<string, { x?: number; y?: number }>\n}\n\nexport interface DraggableSankeyHandle {\n /** reset all manual overrides so the diagram reverts to its computed layout */\n resetNodePositions: () => void\n}\n\n/**\n * A thin wrapper around the Nivo Sankey component that lets users\n * drag nodes around. It keeps a `nodePositions` map in its own state\n * and forwards it to the underlying <Sankey>. Links automatically\n * follow because the custom hook inside the library recomputes their\n * layout whenever positions change.\n */\nexport const DraggableSankey = forwardRef(function DraggableSankey<\n N extends DefaultNode = DefaultNode,\n L extends DefaultLink = DefaultLink,\n>(\n {\n onNodeDrag,\n onNodeDragEnd,\n layers: userLayers,\n initialNodePositions,\n ...rest\n }: DraggableSankeyProps<N, L>,\n ref: React.Ref<DraggableSankeyHandle>\n) {\n /* ------------------------------------------------------------------ */\n /* State & refs */\n /* ------------------------------------------------------------------ */\n const computedInitialPositions = useMemo(() => {\n // Users can manually override node positions via the `initialNodePositions` prop.\n // If nothing is provided we fall back to d3-sankey's computed layout by returning an empty object.\n return initialNodePositions ?? {}\n }, [initialNodePositions])\n\n\n const [nodePositions, setNodePositions] =\n useState<Record<string, { x?: number; y?: number }>>(computedInitialPositions)\n\n // Track whether the user has manually dragged at least one node.\n const hasUserDraggedRef = useRef(false)\n\n const draggingRef = useRef<{\n nodeId: string\n startClientX: number\n startClientY: number\n startX: number\n startY: number\n } | null>(null)\n\n // Keep a