Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 69 additions & 15 deletions packages/sankey/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
height,
getColor,
getLabel,
nodePositions,
}: {
data: SankeyDataProps<N, L>['data']
formatValue: (value: number) => string
Expand All @@ -46,13 +47,12 @@
height: number
getColor: (node: Omit<SankeyNodeDatum<N, L>, 'color' | 'label'>) => string
getLabel: (node: Omit<SankeyNodeDatum<N, L>, 'color' | 'label'>) => string
nodePositions?: Record<string, { x?: number; y?: number }>
}) => {
const sankey = d3Sankey()
.nodeAlign(alignFunction)
// @ts-expect-error: this method signature is incorrect in current @types/d3-sankey
.nodeSort(sortFunction)
// @ts-expect-error: this method is not available in current @types/d3-sankey
.linkSort(linkSortMode)
.nodeSort(sortFunction as any)
.linkSort(linkSortMode as any)

Check failure on line 55 in packages/sankey/src/hooks.ts

View workflow job for this annotation

GitHub Actions / Storybook Tests

Property 'linkSort' does not exist on type 'SankeyLayout<SankeyGraph<{}, {}>, {}, {}>'.

Check failure on line 55 in packages/sankey/src/hooks.ts

View workflow job for this annotation

GitHub Actions / End-to-End Tests

Property 'linkSort' does not exist on type 'SankeyLayout<SankeyGraph<{}, {}>, {}, {}>'.

Check failure on line 55 in packages/sankey/src/hooks.ts

View workflow job for this annotation

GitHub Actions / Tests

Property 'linkSort' does not exist on type 'SankeyLayout<SankeyGraph<{}, {}>, {}, {}>'.
.nodeWidth(nodeThickness)
.nodePadding(nodeSpacing)
.size(layout === 'horizontal' ? [width, height] : [height, width])
Expand All @@ -67,10 +67,32 @@
sankey(data)

data.nodes.forEach(node => {
// Apply explicit positions provided via nodePositions map first
if (nodePositions) {
const override = nodePositions[node.id as unknown as string]
if (override) {
if (override.x !== undefined) node.manualX = override.x
if (override.y !== undefined) node.manualY = override.y
}
}

node.color = getColor(node)
node.label = getLabel(node)
node.formattedValue = formatValue(node.value)

// Apply manual positions if provided
if ('manualX' in node && node.manualX !== undefined) {
const thickness = node.x1 - node.x0;
node.x0 = node.manualX;
node.x1 = node.manualX + thickness;
}

if ('manualY' in node && node.manualY !== undefined) {
const height = node.y1 - node.y0;
node.y0 = node.manualY;
node.y1 = node.manualY + height;
}

if (layout === 'horizontal') {
node.x = node.x0 + nodeInnerPadding
node.y = node.y0
Expand All @@ -96,17 +118,45 @@
link.formattedValue = formatValue(link.value)
link.color = link.source.color
// @ts-expect-error: @types/d3-sankey
link.pos0 = link.y0
// @ts-expect-error: @types/d3-sankey
link.pos1 = link.y1
// @ts-expect-error: @types/d3-sankey
link.thickness = link.width
// @ts-expect-error: @types/d3-sankey
delete link.y0
// @ts-expect-error: @types/d3-sankey
delete link.y1
// @ts-expect-error: @types/d3-sankey
delete link.width
})

// ------------------------------------------------------------------
// Re-compute link positions so they follow nodes after any manual
// node positioning override. We compute each link vertical/horizontal
// offset by stacking the link thicknesses within its source and target
// node. This replaces the original y0/y1 based positions coming from
// d3-sankey which are now out of sync once nodes have been moved.
// ------------------------------------------------------------------
data.nodes.forEach((node: SankeyNodeDatum<N, L>) => {
if (layout === 'horizontal') {
// Outgoing links (left → right)
let sy = 0
node.sourceLinks.forEach(link => {
link.pos0 = node.y0 + sy + link.thickness / 2
sy += link.thickness
})

// Incoming links (left ← right)
let ty = 0
node.targetLinks.forEach(link => {
link.pos1 = node.y0 + ty + link.thickness / 2
ty += link.thickness
})
} else {
// Vertical layout, we stack along the X axis instead of Y.
let sx = 0
node.sourceLinks.forEach(link => {
link.pos0 = node.x0 + sx + link.thickness / 2
sx += link.thickness
})

let tx = 0
node.targetLinks.forEach(link => {
link.pos1 = node.x0 + tx + link.thickness / 2
tx += link.thickness
})
}
})

return data
Expand All @@ -127,6 +177,7 @@
nodeBorderColor,
label,
labelTextColor,
nodePositions,
}: {
data: SankeyDataProps<N, L>['data']
valueFormat?: SankeyCommonProps<N, L>['valueFormat']
Expand All @@ -142,6 +193,7 @@
nodeBorderColor: SankeyCommonProps<N, L>['nodeBorderColor']
label: SankeyCommonProps<N, L>['label']
labelTextColor: SankeyCommonProps<N, L>['labelTextColor']
nodePositions?: Record<string, { x?: number; y?: number }>
}) => {
const [currentNode, setCurrentNode] = useState<SankeyNodeDatum<N, L> | null>(null)
const [currentLink, setCurrentLink] = useState<SankeyLinkDatum<N, L> | null>(null)
Expand Down Expand Up @@ -195,6 +247,7 @@
height,
getColor,
getLabel,
nodePositions,
}),
[
data,
Expand All @@ -210,12 +263,13 @@
height,
getColor,
getLabel,
nodePositions,
]
)

const legendData = useMemo(
() =>
nodes.map(node => ({
nodes.map((node: SankeyNodeDatum<N, L>) => ({
id: node.id,
label: node.label,
color: node.color,
Expand Down
3 changes: 3 additions & 0 deletions packages/sankey/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ export type SankeyNodeDatum<N extends DefaultNode, L extends DefaultLink> = N &
y1: number
value: number
// custom nivo properties
manualX?: number
manualY?: number
color: string
label: string
formattedValue: string
Expand Down Expand Up @@ -138,6 +140,7 @@ export interface SankeyCommonProps<N extends DefaultNode, L extends DefaultLink>
sort: SankeySortType | SankeySortFunction<N, L>

layers: readonly SankeyLayer<N, L>[]
nodePositions?: Record<string, { x?: number, y?: number }>

margin: Box

Expand Down
Loading