Sankey
- +page.svelte
- ./_components/Sankey.svelte
- ./_data/sankey-data.js
<script>
import { LayerCake, Svg } from 'layercake';
import Sankey from './_components/Sankey.svelte';
import data from './_data/sankey-data.js';
</script>
<div class="chart-container">
<LayerCake {data}>
<Svg>
<Sankey colorNodes={d => '#00bbff'} colorLinks={d => '#00bbff35'} />
</Svg>
</LayerCake>
</div>
<style>
/*
The wrapper div needs to have an explicit width and height in CSS.
It can also be a flexbox child or CSS grid element.
The point being it needs dimensions since the <LayerCake> element will
expand to fill it.
*/
.chart-container {
width: 100%;
height: 250px;
}
</style>
<!--
@component
Generates an SVG Sankey chart using [d3-sankey](https://github.com/d3/d3-sankey).
-->
<script>
import { getContext } from 'svelte';
import * as Sankey from 'd3-sankey';
const { data, width, height } = getContext('LayerCake');
/**
* @typedef {(
* import('d3-sankey').sankeyLeft |
* import('d3-sankey').sankeyRight |
* import('d3-sankey').sankeyCenter |
* import('d3-sankey').sankeyJustify
* )} SankeyAlignment
*/
/** @typedef {import('d3-sankey').SankeyGraph} SankeyGraph */
/** @typedef {import('d3-sankey').SankeyNodeMinimal} SankeyNodeMinimal */
/** @typedef {import('d3-sankey').SankeyLinkMinimal} SankeyLink */
/**
* @typedef {((a: SankeyLink, b: SankeyLink) => (number | undefined | null))} LinkSortFunction
*/
/**
* @typedef {Object} Props
* @property {Function} [colorLinks=() => 'rgba(0, 0, 0, .2)'] - A function to return a color for the links.
* @property {Function} [colorNodes=() => '#333'] - A function to return a color for each node.
* @property {Function} [colorText=() => '#263238'] - A function to return a color for each text label.
* @property {number} [nodeWidth=5] - The width of each node, in pixels, passed to [`sankey.nodeWidth`](https://github.com/d3/d3-sankey#sankey_nodeWidth).
* @property {number} [nodePadding=10] - The padding between nodes, passed to [`sankey.nodePadding`](https://github.com/d3/d3-sankey#sankey_nodePadding).
* @property {LinkSortFunction|undefined} [linkSort] - How to sort the links, passed to [`sankey.linkSort`](https://github.com/d3/d3-sankey#sankey_linkSort).
* @property {(SankeyNodeMinimal) => number | string} [nodeId=(d) => d.id] - The ID field accessor, passed to [`sankey.nodeId`](https://github.com/d3/d3-sankey#sankey_nodeId).
* @property {SankeyAlignment} [nodeAlign=Sankey.sankeyLeft] - An alignment function to position the Sankey blocks. See the [d3-sankey documentation](https://github.com/d3/d3-sankey#alignments) for more.
*/
/** @type {Props} */
let {
colorLinks = () => 'rgba(0, 0, 0, .2)',
colorNodes = () => '#333',
colorText = () => '#263238',
nodeWidth = 5,
nodePadding = 10,
linkSort = undefined,
nodeId = d => d.id,
nodeAlign = Sankey.sankeyLeft
} = $props();
const link = Sankey.sankeyLinkHorizontal();
/** @type {SankeyGraph|{links: any, nodes: any}} sankeyData */
let sankeyData = $state({ links: undefined, nodes: undefined });
$effect(() => {
const sankey = Sankey.sankey()
.nodeAlign(nodeAlign)
.nodeWidth(nodeWidth)
.nodePadding(nodePadding)
.nodeId(nodeId)
.size([$width, $height])
.linkSort(linkSort);
sankeyData = sankey($data);
});
let fontSize = $derived($width <= 320 ? 8 : 12);
</script>
<g class="sankey-layer">
<g class="link-group">
{#each sankeyData.links as d}
<path
d={link(d)}
fill="none"
stroke={colorLinks(d)}
stroke-opacity="0.5"
stroke-width={d.width}
/>
{/each}
</g>
<g class="rect-group">
{#each sankeyData.nodes as d}
<rect x={d.x0} y={d.y0} height={d.y1 - d.y0} width={d.x1 - d.x0} fill={colorNodes(d)} />
<text
x={d.x0 < $width / 4 ? d.x1 + 6 : d.x0 - 6}
y={(d.y1 + d.y0) / 2}
dy={fontSize / 2 - 2}
style="fill: {colorText(d)};
font-size: {fontSize}px;
text-anchor: {d.x0 < $width / 4 ? 'start' : 'end'};"
>
{d.id}
</text>
{/each}
</g>
</g>
<style>
text {
pointer-events: none;
}
</style>
export default {
nodes: [
{ id: 'A1' },
{ id: 'A2' },
{ id: 'A3' },
{ id: 'B1' },
{ id: 'B2' },
{ id: 'B3' },
{ id: 'B4' },
{ id: 'C1' },
{ id: 'C2' },
{ id: 'C3' },
{ id: 'D1' },
{ id: 'D2' }
],
links: [
{ source: 'A1', target: 'B1', value: 27 },
{ source: 'A1', target: 'B2', value: 9 },
{ source: 'A2', target: 'B2', value: 5 },
{ source: 'A2', target: 'B3', value: 11 },
{ source: 'A3', target: 'B2', value: 12 },
{ source: 'A3', target: 'B4', value: 7 },
{ source: 'B1', target: 'C1', value: 13 },
{ source: 'B1', target: 'C2', value: 10 },
{ source: 'B4', target: 'C2', value: 5 },
{ source: 'B4', target: 'C3', value: 2 },
{ source: 'B1', target: 'D1', value: 4 },
{ source: 'C3', target: 'D1', value: 1 },
{ source: 'C3', target: 'D2', value: 1 }
]
};