diff --git a/packages/dev/src/examples/networks-and-flows/sankey/sankey-node-collapse/data.ts b/packages/dev/src/examples/networks-and-flows/sankey/sankey-node-collapse/data.ts
new file mode 100644
index 000000000..98d03aaa0
--- /dev/null
+++ b/packages/dev/src/examples/networks-and-flows/sankey/sankey-node-collapse/data.ts
@@ -0,0 +1,53 @@
+export type Node = {
+ id: string;
+ label: string;
+ value: number;
+ color?: string;
+ disabled?: boolean; // New field to test the disabledField functionality
+}
+
+export type Link = {
+ source: string;
+ target: string;
+ value: number;
+}
+
+export const collapseExampleData = {
+ nodes: [
+ // Layer 0 (sources)
+ { id: 'A', label: 'Source A', value: 100 },
+ { id: 'B', label: 'Source B', value: 80 },
+
+ // Layer 1 (intermediate level 1) - Process D is disabled/pre-collapsed
+ { id: 'C', label: 'Process C', value: 90 },
+ { id: 'D', label: 'Process D (Disabled)', value: 70, disabled: true },
+ { id: 'E', label: 'Process E', value: 60 },
+
+ // Layer 2 (intermediate level 2) - Transform F is disabled/pre-collapsed
+ { id: 'F', label: 'Transform F (Disabled)', value: 80, disabled: true },
+ { id: 'G', label: 'Transform G', value: 60 },
+
+ // Layer 3 (destinations)
+ { id: 'H', label: 'End H', value: 60 },
+ { id: 'I', label: 'End I', value: 70 },
+ ],
+ links: [
+ // Layer 0 to 1
+ { source: 'A', target: 'C', value: 60 },
+ { source: 'A', target: 'D', value: 40 },
+ { source: 'B', target: 'D', value: 30 },
+ { source: 'B', target: 'E', value: 50 },
+
+ // Layer 1 to 2
+ { source: 'C', target: 'F', value: 50 },
+ { source: 'C', target: 'G', value: 40 },
+ { source: 'D', target: 'F', value: 30 },
+ { source: 'E', target: 'G', value: 20 },
+
+ // Layer 2 to 3
+ { source: 'F', target: 'H', value: 40 },
+ { source: 'F', target: 'I', value: 40 },
+ { source: 'G', target: 'H', value: 25 },
+ { source: 'G', target: 'I', value: 35 },
+ ],
+}
diff --git a/packages/dev/src/examples/networks-and-flows/sankey/sankey-node-collapse/index.tsx b/packages/dev/src/examples/networks-and-flows/sankey/sankey-node-collapse/index.tsx
new file mode 100644
index 000000000..5a5efdaa5
--- /dev/null
+++ b/packages/dev/src/examples/networks-and-flows/sankey/sankey-node-collapse/index.tsx
@@ -0,0 +1,32 @@
+import React, { useState } from 'react'
+import { VisSingleContainer, VisSankey } from '@unovis/react'
+import { Sizing } from '@unovis/ts'
+
+import { collapseExampleData, Node, Link } from './data'
+
+export const title = 'Sankey Node Collapse'
+export const subTitle = 'Click on nodes to collapse/expand them'
+
+export const component = (): React.ReactNode => {
+ const [data] = useState<{ nodes: Node[]; links: Link[] }>(collapseExampleData)
+
+ return (
+
+
+
+ // Enable the new collapse functionality
+ enableNodeCollapse={true}
+ collapseAnimationDuration={500}
+ disabledField="disabled" // Pre-collapse nodes with disabled: true
+ nodeWidth={30}
+ nodePadding={10}
+ nodeColor={(d: Node) => d.color}
+ label={(d: Node) => d.label}
+ labelPosition="auto"
+ labelMaxWidth={100}
+ linkValue={(d) => d.value}
+ />
+
+
+ )
+}
diff --git a/packages/ts/src/components/sankey/config.ts b/packages/ts/src/components/sankey/config.ts
index 3b4983284..6698f9639 100644
--- a/packages/ts/src/components/sankey/config.ts
+++ b/packages/ts/src/components/sankey/config.ts
@@ -51,6 +51,16 @@ export interface SankeyConfigInterface, collapsed: boolean): void {
+ const { config } = this
+
+ // Clear any active highlights before changing state
+ this.disableHighlight()
+
+ node._state = node._state || {}
+ node._state.collapsed = collapsed
+ this._render(config.collapseAnimationDuration)
+ }
+
+ /**
+ * Toggles the collapse state of a node.
+ * Collapses a node by hiding only the links directly connected to it.
+ * All other nodes (including children and descendants) remain visible in their original positions.
+ * Only the immediate incoming and outgoing links of the collapsed node are hidden.
+ *
+ * Expands a previously collapsed node by showing its directly connected links.
+ * @param node The node to toggle
+ */
+ toggleNodeCollapse (node: SankeyNode): void {
+ const isCurrentlyCollapsed = node._state?.collapsed ?? false
+ this._setNodeCollapseState(node, !isCurrentlyCollapsed)
+ }
+
private _hasLinks (): boolean {
const { datamodel } = this
return datamodel.links.length > 0
@@ -694,6 +728,30 @@ export class Sankey<
const nextLayerNode = nodes.find(d => d.layer === firstLayerNode.layer + 1)
return nextLayerNode ? nextLayerNode.x0 - (firstLayerNode.x0 + config.nodeWidth) : this._width - firstLayerNode.x1
}
+
+ /**
+ * Applies initial collapse state to nodes based on the disabledField configuration.
+ * If disabledField is set (e.g., "disabled"), nodes with that field set to true
+ * will be pre-collapsed when the component loads.
+ */
+ private _applyInitialCollapseState (): void {
+ const { config, datamodel } = this
+
+ if (!config.disabledField) return
+
+ // Check each node for the disabled field and set initial collapse state
+ for (const node of datamodel.nodes) {
+ const inputData = node as unknown as N
+ const isDisabled = inputData && typeof inputData === 'object' &&
+ config.disabledField in inputData &&
+ (inputData as any)[config.disabledField] === true
+
+ if (isDisabled) {
+ node._state = node._state || {}
+ node._state.collapsed = true
+ }
+ }
+ }
private _onNodeMouseOver (d: SankeyNode, event: MouseEvent): void {
const { datamodel } = this
@@ -729,4 +787,11 @@ export class Sankey<
private _onLinkMouseOut (): void {
this.disableHighlight()
}
+
+ private _onNodeClick (d: SankeyNode, event: MouseEvent): void {
+ const { config } = this
+ if (config.enableNodeCollapse) {
+ this.toggleNodeCollapse(d)
+ }
+ }
}
diff --git a/packages/ts/src/components/sankey/modules/link.ts b/packages/ts/src/components/sankey/modules/link.ts
index 0836f8979..4d8d2b893 100644
--- a/packages/ts/src/components/sankey/modules/link.ts
+++ b/packages/ts/src/components/sankey/modules/link.ts
@@ -81,10 +81,19 @@ export function updateLinks) => d._state.greyout ? 0.2 : 1)
+ .style('opacity', (d: SankeyLink) => {
+ // Hide links if either connected node is collapsed
+ if (d.source._state?.collapsed || d.target._state?.collapsed) return 0
+ // Apply greyout effect
+ return d._state?.greyout ? 0.2 : 1
+ })
const linkSelection = sel.select(`.${s.linkPath}`)
.style('cursor', (d: SankeyLink) => getString(d, config.linkCursor))
+ .style('pointer-events', (d: SankeyLink) => {
+ // Disable pointer events for collapsed links to prevent hover interference
+ return (d.source._state?.collapsed || d.target._state?.collapsed) ? 'none' : null
+ })
const selectionTransition = smartTransition(linkSelection, duration)
.style('fill', (link: SankeyLink) => getColor(link, config.linkColor))
@@ -133,6 +142,7 @@ export function updateLinks getString(d, config.linkCursor))
+ .style('pointer-events', (d: SankeyLink) => (d.source._state?.collapsed || d.target._state?.collapsed) ? 'none' : null)
}
export function removeLinks (
diff --git a/packages/ts/src/components/sankey/types.ts b/packages/ts/src/components/sankey/types.ts
index 74ae1a84c..6702b9fe9 100644
--- a/packages/ts/src/components/sankey/types.ts
+++ b/packages/ts/src/components/sankey/types.ts
@@ -42,6 +42,8 @@ export type SankeyNode = G
greyout?: boolean;
/* Pre-calculated node height value in pixels that will be used to manually generate the layout when data has no links */
precalculatedHeight?: number;
+ /** Whether this node is collapsed (hides all connected links) */
+ collapsed?: boolean;
};
}
diff --git a/packages/website/docs/networks-and-flows/Sankey.mdx b/packages/website/docs/networks-and-flows/Sankey.mdx
index 903bfb80e..2d955cd19 100644
--- a/packages/website/docs/networks-and-flows/Sankey.mdx
+++ b/packages/website/docs/networks-and-flows/Sankey.mdx
@@ -32,6 +32,15 @@ export const SankeyDoc = (props) => (
)
+export const sankeyCollapseProps = {
+ data: sankeyData(100, [[1,2], [3,4], [5,6]], 4, ['D', 'F']),
+ enableNodeCollapse: true,
+ collapseAnimationDuration: 500,
+ disabledField: "disabled",
+ label: d => d.label || d.id,
+ nodePadding: 15,
+}
+
## Basic Configuration
_Sankey_ is a popular kind of flow diagram that visualizes flows between multiple nodes. To define a Sankey diagram
you'll need to have data about its nodes and flows between them.
@@ -243,6 +252,31 @@ Use **selectedNodeIds: string[]** to programmatically set selected nodes. Select
selectedNodeIds={['A','C']}
showContext="minimal"
/>
+### Pre-collapsed Nodes
+You can specify nodes to have a collapsed state by using the `disabledField` property. Any nodes with this field set to `true` will be pre-collapsed when the diagram loads:
+
+```ts
+// In your data
+const nodes = [
+ { id: 'A', label: 'Active Node' },
+ { id: 'B', label: 'Pre-collapsed Node', disabled: true }, // This node starts collapsed
+]
+
+// In your configuration
+{
+ enableNodeCollapse: true,
+ disabledField: 'disabled', // Field name that indicates pre-collapsed nodes
+}
+```
+
+When a node is collapsed:
+- Only the links **directly connected** to that node are hidden
+- All other nodes (including children and descendants) remain visible in their original positions
+- The collapsed node itself remains visible and clickable
+
+This approach differs from traditional tree-like collapse where entire subtrees disappear.
+
+
## Sorting
By default, _Sankey_ will sort the links based on their `value` in descending order from top to bottom.
diff --git a/packages/website/docs/utils/data.ts b/packages/website/docs/utils/data.ts
index 55ff6eeec..1beabae4d 100644
--- a/packages/website/docs/utils/data.ts
+++ b/packages/website/docs/utils/data.ts
@@ -65,6 +65,7 @@ type NodeDatum = {
id: string;
label?: string;
value?: number;
+ disabled?: boolean; // For collapse functionality
}
type LinkDatum = {
@@ -110,18 +111,23 @@ function generateLinks (n: number, count: number): number[] {
return [val, ...generateLinks(n - val, count - 1)]
}
-export const sankeyData = (src: number, edges: [[number, number]], subDataCount = 4): NodeLinkData => {
+export const sankeyData = (src: number, edges: [[number, number]], subDataCount = 4, disabledNodes?: string[]): NodeLinkData => {
const nodes = [{ id: 'A', val: src, x: 0 }]
const links = []
for (let i = 0; i < edges.length; i++) {
const vals = generateLinks(nodes[i].val, edges[i].length)
for (let j = 0; j < edges[i].length; j++) {
if (edges[i][j] >= nodes.length) {
- nodes.push({
- id: String.fromCharCode(65 + nodes.length),
+ const nodeId = String.fromCharCode(65 + nodes.length)
+ const node: any = {
+ id: nodeId,
val: vals[j],
x: Math.floor(Math.random() * subDataCount),
- })
+ }
+ if (disabledNodes?.includes(nodeId)) {
+ node.disabled = true
+ }
+ nodes.push(node)
}
links.push({ source: nodes[i].id, target: nodes[edges[i][j]].id, value: vals[j] })
}