sankey_diagram_flutter
A highly customizable Sankey diagram widget for Flutter. Visualize flow-based data — broker distribution, fund flows, supply chains, survey results, and more — with smooth Bézier curves, animated entries, hover effects, and full interactivity.
Features
- Smooth Bézier links rendered with a
CustomPainter— no web view, no canvas bridging - Animated entry with a configurable duration and easing curve
- Interactive — tap nodes or links to select, hover to highlight related flows
- Tooltip shown on tap, with a built-in default style or a fully custom builder
- Automatic layout driven by node type (
buyer→ left,neutral→ middle,seller→ right), or fully topology-driven via BFS longest-path (autoLayout: true) - Circular link support — backward flows rendered as upward arcs, with automatic detection
- Node sorting by value, by label, or in insertion order
- Legend with four position options (top / bottom / left / right) or hidden; supports tap-to-highlight
- Programmatic control via
SankeyController— highlight nodes from external UI - Value formatter that auto-scales to K / M / B / T
- Dark-mode aware — label and tooltip colours adapt to
ThemeData.brightness - Pure Dart/Flutter — no platform channels, no native dependencies
Screenshots
| Example | Dark mode | Light mode |
|---|---|---|
| Broker Distribution 2 columns |
![]() |
![]() |
| Financing 2 columns |
![]() |
![]() |
| E-Commerce Journey 4 columns |
![]() |
![]() |
| Survey 2 columns |
![]() |
![]() |
| Export / Import 2 columns |
![]() |
![]() |
| APBN 2024 3 columns |
![]() |
![]() |
| Auto Layout topology-driven + circular link |
![]() |
![]() |
Installation
dependencies:
sankey_diagram_flutter: ^1.1.0
Then run:
flutter pub get
Quick start
import 'package:sankey_diagram_flutter/sankey_diagram_flutter.dart';
SankeyDiagram(
data: SankeyData(
nodes: const [
SankeyNode(id: 'a', label: 'Source A', color: Color(0xFF7C3AED), type: SankeyNodeType.buyer),
SankeyNode(id: 'b', label: 'Source B', color: Color(0xFF3B82F6), type: SankeyNodeType.buyer),
SankeyNode(id: 'x', label: 'Target X', color: Color(0xFF10B981), type: SankeyNodeType.seller),
SankeyNode(id: 'y', label: 'Target Y', color: Color(0xFFEF4444), type: SankeyNodeType.seller),
],
links: const [
SankeyLink(sourceId: 'a', targetId: 'x', value: 300),
SankeyLink(sourceId: 'a', targetId: 'y', value: 150),
SankeyLink(sourceId: 'b', targetId: 'x', value: 200),
SankeyLink(sourceId: 'b', targetId: 'y', value: 350),
],
),
height: 360,
)
Data model
SankeyNode
Represents a single node (bar) in the diagram.
| Property | Type | Required | Description |
|---|---|---|---|
id |
String |
yes | Unique identifier referenced by links |
label |
String |
yes | Display text rendered on the node |
color |
Color |
yes | Fill colour of the node bar and its outgoing links |
type |
SankeyNodeType |
no | buyer (left), seller (right), neutral (middle). Default: neutral |
column |
int? |
no | Explicit zero-based column index. Overrides type. Enables 4+ column layouts. |
subLabel |
String? |
no | Secondary text shown below the label |
metadata |
Map<String, dynamic>? |
no | Arbitrary data passed through to tap/hover callbacks |
const SankeyNode(
id: 'bank',
label: 'Bank',
color: Color(0xFF3B82F6),
type: SankeyNodeType.buyer,
subLabel: 'Commercial',
)
SankeyLink
Represents a directed flow between two nodes.
| Property | Type | Required | Description |
|---|---|---|---|
sourceId |
String |
yes | id of the source node |
targetId |
String |
yes | id of the target node |
value |
double |
yes | Flow magnitude — determines link thickness |
color |
Color? |
no | Override link colour (defaults to source node colour) |
label |
String? |
no | Optional label attached to this link |
SankeyData
Container holding all nodes and links. Provides helpers for querying the graph.
final data = SankeyData(nodes: [...], links: [...]);
data.nodeById('bank'); // SankeyNode?
data.outgoingLinks('bank'); // List<SankeyLink>
data.incomingLinks('mortgage'); // List<SankeyLink>
Column layout
Automatic layout (2–3 columns)
Column assignment via SankeyNodeType — no coordinates needed.
SankeyNodeType |
Column |
|---|---|
buyer |
Left |
neutral |
Middle (triggers a three-column layout) |
seller |
Right |
// Two-column: Buyer → Seller
SankeyNode(id: 'src', label: 'Source', color: ..., type: SankeyNodeType.buyer)
SankeyNode(id: 'dst', label: 'Dest', color: ..., type: SankeyNodeType.seller)
// Three-column: Buyer → Neutral → Seller
SankeyNode(id: 'src', label: 'Source', color: ..., type: SankeyNodeType.buyer)
SankeyNode(id: 'mid', label: 'Middle', color: ..., type: SankeyNodeType.neutral)
SankeyNode(id: 'dst', label: 'Dest', color: ..., type: SankeyNodeType.seller)
Auto-layout (topology-driven)
Set autoLayout: true on SankeyStyle to derive column positions directly from
link topology via BFS, instead of SankeyNodeType or column. Node vertical
positions are then refined with iterative relaxation (like d3-sankey) to
minimise link crossings. SankeyNode.column and SankeyNode.type are ignored
in this mode.
SankeyDiagram(
data: data,
style: const SankeyStyle(
autoLayout: true,
layoutIterations: 6,
nodeAlignment: NodeAlignment.justify,
),
)
NodeAlignment controls how source/sink nodes are placed:
| Value | Behaviour |
|---|---|
left |
Source nodes at column 0; columns increase by link depth |
right |
Sink nodes at the rightmost column; columns propagate right to left |
center |
Nodes positioned at the midpoint of forward/reverse depth |
justify |
Like left, but also pushes sink nodes to the rightmost column (default — matches d3-sankey) |
Backward flows (where a link's source column is at or after its target column — e.g. a feedback loop) are automatically detected and rendered as upward arcs instead of the usual Bézier ribbon.
Explicit column placement (4+ columns)
Set column on each node to place them in any column. This unlocks diagrams
with four or more steps — energy flows, supply chains, multi-stage budget
allocations, and more.
// Four-column energy flow: Source → Conversion → Grid → End Use
const nodes = [
SankeyNode(id: 'coal', label: 'Coal', color: Color(0xFF78716C), column: 0),
SankeyNode(id: 'gas', label: 'Natural Gas', color: Color(0xFF3B82F6), column: 0),
SankeyNode(id: 'solar', label: 'Solar', color: Color(0xFFFBBF24), column: 0),
SankeyNode(id: 'plant', label: 'Power Plant', color: Color(0xFF8B5CF6), column: 1),
SankeyNode(id: 'grid', label: 'Grid', color: Color(0xFF6B7280), column: 2),
SankeyNode(id: 'home', label: 'Residential', color: Color(0xFF10B981), column: 3),
SankeyNode(id: 'industry',label: 'Industry', color: Color(0xFFEF4444), column: 3),
];
When any node has
columnset, all nodes must use explicitcolumnvalues. Mixingcolumnandtype-based assignment in the same diagram is not supported.
Styling
Pass a SankeyStyle to the style parameter to control every visual aspect of the diagram.
SankeyDiagram(
data: data,
height: 400,
style: const SankeyStyle(
nodeWidth: 10,
nodePadding: 14,
nodeRadius: 6,
linkOpacity: 0.40,
hoverOpacity: 0.90,
dimOpacity: 0.08,
sortOrder: SortOrder.byValue,
legendPosition: LegendPosition.bottom,
animationDuration: Duration(milliseconds: 800),
),
)
SankeyStyle reference
| Property | Type | Default | Description |
|---|---|---|---|
nodeWidth |
double |
8.0 |
Width of each node bar in logical pixels |
nodePadding |
double |
12.0 |
Vertical gap between nodes in the same column |
nodeRadius |
double |
4.0 |
Corner radius of node bars |
linkOpacity |
double |
0.45 |
Default link fill opacity |
hoverOpacity |
double |
0.85 |
Link opacity when the linked node or link is focused |
dimOpacity |
double |
0.12 |
Opacity of unrelated links when a node or link is focused |
showNodeLabels |
bool |
true |
Whether to draw labels on nodes |
showTooltip |
bool |
true |
Whether to show a tooltip on tap |
labelStyle |
TextStyle? |
— | Style for node label text |
valueStyle |
TextStyle? |
— | Style for the value text below the label |
labelBackgroundColor |
Color? |
— | Pill background behind each node label |
labelPadding |
EdgeInsets |
h:6 v:3 |
Padding inside the label pill |
labelBorderRadius |
double |
4.0 |
Corner radius of label pills |
animationDuration |
Duration |
600 ms |
Duration of the entry animation |
tooltipStyle |
TooltipStyle? |
— | Colours, padding, and border for the default tooltip |
sortOrder |
SortOrder |
byValue |
byValue, byLabel, or none |
legendPosition |
LegendPosition |
bottom |
top, bottom, left, right, or hidden |
backgroundColor |
Color |
transparent |
Canvas background colour |
horizontalPadding |
double |
0.0 |
Padding added on both horizontal edges |
verticalPadding |
double |
0.0 |
Padding added on both vertical edges |
autoLayout |
bool |
false |
Derive columns from link topology via BFS instead of type/column |
layoutIterations |
int |
6 |
Relaxation passes run when autoLayout is true |
nodeAlignment |
NodeAlignment |
justify |
Source/sink placement strategy during auto-layout: left, right, center, or justify |
Tooltip style
SankeyDiagram(
data: data,
style: const SankeyStyle(
tooltipStyle: TooltipStyle(
backgroundColor: Color(0xFF1E293B),
borderColor: Color(0xFF334155),
borderWidth: 1.5,
borderRadius: 10,
elevation: 6,
textStyle: TextStyle(color: Colors.white, fontSize: 13),
padding: EdgeInsets.symmetric(horizontal: 14, vertical: 10),
),
),
)
Column header labels
SankeyDiagram(
data: data,
buyerLabel: 'Source',
sellerLabel: 'Destination',
buyerLabelStyle: const TextStyle(
color: Color(0xFF4ADE80),
fontWeight: FontWeight.w700,
),
sellerLabelStyle: const TextStyle(
color: Color(0xFFF87171),
fontWeight: FontWeight.w700,
),
)
Interaction callbacks
SankeyDiagram(
data: data,
onNodeTap: (SankeyNode node) {
print('Tapped: ${node.label}');
// node.metadata holds any domain data you attached
},
onLinkTap: (SankeyLayoutLink link) {
print('Flow: ${link.link.sourceId} → ${link.link.targetId}');
print('Value: ${link.link.value}');
},
onNodeHover: (SankeyNode? node) {
// node is null when the cursor leaves all nodes
setState(() => _hovered = node?.label);
},
)
Programmatic control — SankeyController
Use SankeyController to highlight a node from outside the diagram, for example from a list or a chip group.
class _MyPageState extends State<MyPage> {
final _controller = SankeyController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
SankeyDiagram(data: data, controller: _controller),
Row(
children: [
ElevatedButton(
onPressed: () => _controller.highlight('bank'),
child: const Text('Highlight Bank'),
),
ElevatedButton(
onPressed: _controller.clearHighlight,
child: const Text('Clear'),
),
],
),
],
);
}
}
Custom tooltip
Replace the built-in tooltip with any widget using tooltipBuilder.
SankeyDiagram(
data: data,
tooltipBuilder: (BuildContext context, SankeyTooltipData data) {
return Card(
color: Colors.black87,
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
data.label,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
Text(
ValueFormatter.format(data.value),
style: const TextStyle(color: Colors.amber),
),
if (data.percentage != null)
Text(
'${data.percentage!.toStringAsFixed(1)} %',
style: const TextStyle(color: Colors.white54, fontSize: 11),
),
],
),
),
);
},
)
SankeyTooltipData fields:
| Field | Type | Description |
|---|---|---|
isNode |
bool |
true for a node tap, false for a link tap |
label |
String |
Node label, or "sourceId → targetId" for a link |
value |
double |
Aggregated flow value |
color |
Color |
Node or link colour |
percentage |
double? |
Percentage of total flow (when calculable) |
Custom legend
Override the entire legend widget with legendBuilder:
SankeyDiagram(
data: data,
legendBuilder: (BuildContext context, List<SankeyLegendItem> items) {
return Wrap(
spacing: 12,
children: items.map((item) => Chip(
avatar: CircleAvatar(backgroundColor: item.color),
label: Text(item.label),
)).toList(),
);
},
)
Or supply a hand-crafted legendItems list instead of deriving them automatically:
SankeyDiagram(
data: data,
legendItems: const [
SankeyLegendItem(color: Color(0xFF7C3AED), label: 'Domestic'),
SankeyLegendItem(color: Color(0xFF10B981), label: 'BUMN'),
SankeyLegendItem(color: Color(0xFFEF4444), label: 'Foreign'),
],
)
By default, legend items are derived automatically from nodes via
deriveLegendItems(nodes), which groups bysubLabelwhen present, otherwise bytypename.
Utilities
ValueFormatter
Formats large numbers with SI suffixes:
ValueFormatter.format(1500); // "1.50 K"
ValueFormatter.format(2_500_000); // "2.50 M"
ValueFormatter.format(3_750_000_000); // "3.75 B"
ValueFormatter.format(1e12); // "1.00 T"
ValueFormatter.format(500); // "500.00"
// Custom decimal places:
ValueFormatter.format(1234, decimals: 0); // "1 K"
// Suffix only:
ValueFormatter.suffix(1_000_000); // "M"
ColorUtils
HSL-based colour manipulation helpers and a seed-based colour generator:
ColorUtils.darken(color, 0.15); // darken by 15 %
ColorUtils.lighten(color, 0.20); // lighten by 20 %
ColorUtils.withSaturation(color, 0.5); // set saturation to 50 %
// Generate a deterministic colour from any string (node id, category name, etc.)
// Uses golden-angle hue distribution — consecutive seeds produce well-spaced colours.
ColorUtils.fromSeed('revenue'); // always the same colour for the same string
ColorUtils.fromSeed('expense'); // a different, perceptually-distinct colour
fromSeed is useful when mapping API data where colours are not part of the response:
SankeyNode(
id: node['id'],
label: node['name'],
color: ColorUtils.fromSeed(node['category']), // no manual palette needed
column: node['step'],
metadata: node,
)
SankeyDiagram — full parameter reference
| Parameter | Type | Default | Description |
|---|---|---|---|
data |
SankeyData |
required | Nodes and links to render |
height |
double |
400 |
Fixed canvas height |
style |
SankeyStyle |
SankeyStyle() |
Visual configuration |
controller |
SankeyController? |
— | Programmatic node highlight |
buyerLabel |
String |
'Buyer' |
Header above the left column |
sellerLabel |
String |
'Seller' |
Header above the right column |
buyerLabelStyle |
TextStyle? |
— | Style for buyerLabel |
sellerLabelStyle |
TextStyle? |
— | Style for sellerLabel |
legendItems |
List<SankeyLegendItem>? |
auto-derived | Override the legend items list |
onNodeTap |
void Function(SankeyNode)? |
— | Called when a node is tapped |
onLinkTap |
void Function(SankeyLayoutLink)? |
— | Called when a link is tapped |
onNodeHover |
void Function(SankeyNode?)? |
— | Called on pointer hover; null on pointer exit |
tooltipBuilder |
Widget Function(BuildContext, SankeyTooltipData)? |
— | Custom tooltip widget |
legendBuilder |
Widget Function(BuildContext, List<SankeyLegendItem>)? |
— | Custom legend widget |
Platform support
| Android | iOS | Web | macOS | Windows | Linux |
|---|---|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
The widget uses CustomPainter and standard Flutter gesture detectors with no platform-specific code.
Example app
The example/ directory contains a full demo with seven real-world datasets:
- Broker Distribution — equity transaction flows between brokers (2 columns)
- Financing — lending sources vs. loan purposes (2 columns)
- E-Commerce Journey — 4-column customer journey: traffic source → product category → payment method → delivery service
- Survey — generational media consumption by platform (2 columns)
- Export / Import — Indonesian export commodities vs. destination countries (2 columns)
- APBN 2024 — 3-column Indonesian state budget: revenue sources → spending functions → beneficiaries (~Rp 3.000 T)
- Auto Layout — national energy flow with topology-driven column placement (BFS) and a circular link representing industrial cogeneration
Run it with:
cd example
flutter run
Issues and contributions
Found a bug or have a feature request? Please open an issue. Pull requests are welcome.
License
MIT — see LICENSE for details.
Libraries
- sankey_diagram_flutter
- A highly customizable Sankey diagram widget for Flutter.













