Path Highlighting and Interaction
When a user hovers a node or edge, ApexSankey can highlight the entire connected flow path and dim everything else. This makes it easy to trace where a flow comes from and where it goes in a dense diagram. Clicking a node then pins that path, and the optional pathTrace plugin sends an animated pulse cascading along it — hover to skim, click to commit.
Connected-path highlighting
highlightConnectedPath is enabled by default. On hover, the connected upstream and downstream flows stay at full opacity while unrelated elements fade to dimOpacity.
| Option | Type | Default | Description |
|---|---|---|---|
highlightConnectedPath | boolean | true | Highlight the connected flow path when hovering a node or edge |
dimOpacity | number | 0.15 | Opacity applied to unrelated (dimmed) elements while highlighting is active |
const sankey = new ApexSankey(document.getElementById('chart'), {
highlightConnectedPath: true,
dimOpacity: 0.2,
})
sankey.render(data)
Disabling highlighting
Set highlightConnectedPath: false for a static diagram where hover has no effect:
const sankey = new ApexSankey(el, {
highlightConnectedPath: false,
})
Tuning the dim level
Lower dimOpacity values make the highlighted path stand out more sharply against the dimmed background; higher values keep more context visible:
const sankey = new ApexSankey(el, {
highlightConnectedPath: true,
dimOpacity: 0.08, // strong contrast — unrelated flows nearly vanish
})
Animated path tracing
The built-in highlight is static: the connected path lights up and holds. The pathTrace plugin adds motion on top of it — when a node is picked, a bright pulse cascades along the connected flow path, ribbon by ribbon, cueing the eye to where the flow goes. It composes with highlightConnectedPath (a click both pins the highlighted path and fires the pulse down it) and is skipped under prefers-reduced-motion.
Install it with use(), importing it as pathTrace or from ApexSankey.plugins:
const sankey = new ApexSankey(el, {
highlightConnectedPath: true,
dimOpacity: 0.1,
})
sankey.use(ApexSankey.plugins.pathTrace({ direction: 'downstream' }))
sankey.render(data)
| Option | Type | Default | Description |
|---|---|---|---|
trigger | 'click' | 'hover' | 'click' | What starts a trace |
direction | 'downstream' | 'upstream' | 'both' | 'downstream' | Which way flow is traced from the picked node |
color | string | '#ffffff' | Pulse color — pick one that contrasts with your background |
duration | number | 700 | Milliseconds for a pulse to cross one ribbon |
stagger | number | 220 | Milliseconds added per hop, so the trace cascades outward |
Click to focus and isolate
Hover highlighting is transient: move the pointer away and the diagram returns to normal. Clicking a node or a flow instead pins its full upstream and downstream path and dims the rest, so a path stays isolated while you read it or point at it.
Clicking again releases it. The traversal is cycle-guarded, so a diagram with circular links terminates rather than walking a loop forever.
This works alongside onNodeClick and the node:click event; both still fire, so pinning a path and running your own handler are not mutually exclusive.
Draggable nodes
draggableNodes lets a reader reposition nodes by dragging, with mouse, touch and pen support. Connected flows follow the node live:
const sankey = new ApexSankey(el, {
draggableNodes: true,
})
This is useful when the automatic layout puts two ranks in an order that fights the story, or when a reader wants to untangle a specific crossing to see it clearly.
A manual position holds until the next render() or update() recomputes the layout, so a dragged arrangement is a viewing aid rather than persisted state. Capture positions yourself if you need them to survive a data change.
Node click callback
onNodeClick fires when the user clicks a node. It receives a SankeyNode object describing the clicked node.
const sankey = new ApexSankey(el, {
onNodeClick: (node) => {
console.log('Clicked node:', node)
console.log('Node data:', node.data) // { id, title, color, ... }
console.log('Flow value:', node.value) // total value through the node
},
})
sankey.render(data)
SankeyNode fields
| Field | Type | Description |
|---|---|---|
data | NodeData | null | The node's user data: { id, title, color, direction, forceY } |
value | number | Total flow value passing through the node |
rank | number | Column index (0-based) of the node in the layout |
x, y | number | Computed position of the node |
dy | number | Computed height of the node rectangle |
direction | string | Flow direction for the node |
spaceAbove, spaceBelow | number | Whitespace above/below the node |
Building a drill-down interaction
Combine onNodeClick with your own state to drive a detail panel or filter:
const sankey = new ApexSankey(el, {
highlightConnectedPath: true,
dimOpacity: 0.15,
onNodeClick: (node) => {
const id = node.data?.id
if (!id) return
showDetailPanel(id, node.value)
},
})
sankey.render(data)
function showDetailPanel(id, value) {
document.getElementById('panel').innerHTML = `
<h3>${id}</h3>
<p>Total flow: ${value}</p>
`
}
React example
In React, use the top-level onNodeClick prop (not options.onNodeClick, which the wrapper omits):
import { useState } from 'react'
import { ApexSankey } from 'react-apexsankey'
import type { SankeyNode } from 'react-apexsankey'
export default function InteractiveSankey() {
const [selected, setSelected] = useState<string | null>(null)
return (
<div>
{selected && <p>Selected: {selected}</p>}
<ApexSankey
data={data}
options={{ highlightConnectedPath: true, dimOpacity: 0.15 }}
onNodeClick={(node: SankeyNode) => setSelected(node.data?.id ?? null)}
/>
</div>
)
}
Vue example
<template>
<div>
<p v-if="selected">Selected: {{ selected }}</p>
<ApexSankey :data="data" :options="options" @nodeClick="onNodeClick" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import ApexSankey from 'vue-apexsankey'
const selected = ref<string | null>(null)
const options = { highlightConnectedPath: true, dimOpacity: 0.15 }
const onNodeClick = (node) => {
selected.value = node.data?.id ?? null
}
</script>