Sankey diagram
Weighted flow diagram with nodes and links — node height equals flow.
Basic usage
Data is passed via props and rendered as pure SVG, with no third-party charting dependency.
Colors are drawn from the --viz-1..8 tokens and are colorblind-friendly.
背景 视口
<script setup lang="ts">
import { CfSankeyDiagram } from '@chufix-design/vue';
const nodes = [
{ id: 'organic', name: 'Organic', layer: 0 },
{ id: 'paid', name: 'Paid', layer: 0 },
{ id: 'referral', name: 'Referral', layer: 0 },
{ id: 'web', name: 'Web', layer: 1 },
{ id: 'mobile', name: 'Mobile', layer: 1 },
{ id: 'success', name: '2xx', layer: 2 },
{ id: 'redirect', name: '3xx', layer: 2 },
{ id: 'error', name: '4xx/5xx', layer: 2 },
];
const links = [
{ source: 'organic', target: 'web', value: 50 },
{ source: 'organic', target: 'mobile', value: 30 },
{ source: 'paid', target: 'web', value: 20 },
{ source: 'paid', target: 'mobile', value: 25 },
{ source: 'referral', target: 'web', value: 10 },
{ source: 'referral', target: 'mobile', value: 5 },
{ source: 'web', target: 'success', value: 60 },
{ source: 'web', target: 'redirect', value: 12 },
{ source: 'web', target: 'error', value: 8 },
{ source: 'mobile', target: 'success', value: 45 },
{ source: 'mobile', target: 'redirect', value: 8 },
{ source: 'mobile', target: 'error', value: 7 },
];
</script>
<template>
<CfSankeyDiagram :nodes="nodes" :links="links" />
</template> <script setup>
import { CfSankeyDiagram } from '@chufix-design/vue';
const nodes = [
{ id: 'organic', name: 'Organic', layer: 0 },
{ id: 'paid', name: 'Paid', layer: 0 },
{ id: 'referral', name: 'Referral', layer: 0 },
{ id: 'web', name: 'Web', layer: 1 },
{ id: 'mobile', name: 'Mobile', layer: 1 },
{ id: 'success', name: '2xx', layer: 2 },
{ id: 'redirect', name: '3xx', layer: 2 },
{ id: 'error', name: '4xx/5xx', layer: 2 },
];
const links = [
{ source: 'organic', target: 'web', value: 50 },
{ source: 'organic', target: 'mobile', value: 30 },
{ source: 'paid', target: 'web', value: 20 },
{ source: 'paid', target: 'mobile', value: 25 },
{ source: 'referral', target: 'web', value: 10 },
{ source: 'referral', target: 'mobile', value: 5 },
{ source: 'web', target: 'success', value: 60 },
{ source: 'web', target: 'redirect', value: 12 },
{ source: 'web', target: 'error', value: 8 },
{ source: 'mobile', target: 'success', value: 45 },
{ source: 'mobile', target: 'redirect', value: 8 },
{ source: 'mobile', target: 'error', value: 7 },
];
</script>
<template>
<CfSankeyDiagram :nodes="nodes" :links="links" />
</template> import { CfSankeyDiagram } from '@chufix-design/react';
export default function Demo() {
const nodes = [
{ id: 'organic', name: 'Organic', layer: 0 },
{ id: 'paid', name: 'Paid', layer: 0 },
{ id: 'referral', name: 'Referral', layer: 0 },
{ id: 'web', name: 'Web', layer: 1 },
{ id: 'mobile', name: 'Mobile', layer: 1 },
{ id: 'success', name: '2xx', layer: 2 },
{ id: 'redirect', name: '3xx', layer: 2 },
{ id: 'error', name: '4xx/5xx', layer: 2 },
];
const links = [
{ source: 'organic', target: 'web', value: 50 },
{ source: 'organic', target: 'mobile', value: 30 },
{ source: 'paid', target: 'web', value: 20 },
{ source: 'paid', target: 'mobile', value: 25 },
{ source: 'referral', target: 'web', value: 10 },
{ source: 'referral', target: 'mobile', value: 5 },
{ source: 'web', target: 'success', value: 60 },
{ source: 'web', target: 'redirect', value: 12 },
{ source: 'web', target: 'error', value: 8 },
{ source: 'mobile', target: 'success', value: 45 },
{ source: 'mobile', target: 'redirect', value: 8 },
{ source: 'mobile', target: 'error', value: 7 },
];
return (
<>
<CfSankeyDiagram nodes={nodes} links={links} />
</>
);
} import { CfSankeyDiagram } from '@chufix-design/react';
export default function Demo() {
const nodes = [
{ id: 'organic', name: 'Organic', layer: 0 },
{ id: 'paid', name: 'Paid', layer: 0 },
{ id: 'referral', name: 'Referral', layer: 0 },
{ id: 'web', name: 'Web', layer: 1 },
{ id: 'mobile', name: 'Mobile', layer: 1 },
{ id: 'success', name: '2xx', layer: 2 },
{ id: 'redirect', name: '3xx', layer: 2 },
{ id: 'error', name: '4xx/5xx', layer: 2 },
];
const links = [
{ source: 'organic', target: 'web', value: 50 },
{ source: 'organic', target: 'mobile', value: 30 },
{ source: 'paid', target: 'web', value: 20 },
{ source: 'paid', target: 'mobile', value: 25 },
{ source: 'referral', target: 'web', value: 10 },
{ source: 'referral', target: 'mobile', value: 5 },
{ source: 'web', target: 'success', value: 60 },
{ source: 'web', target: 'redirect', value: 12 },
{ source: 'web', target: 'error', value: 8 },
{ source: 'mobile', target: 'success', value: 45 },
{ source: 'mobile', target: 'redirect', value: 8 },
{ source: 'mobile', target: 'error', value: 7 },
];
return (
<>
<CfSankeyDiagram nodes={nodes} links={links} />
</>
);
} Three-layer purchase path
Search engine / direct visit → product list / detail → add-to-cart / checkout, in three layers.
背景 视口
<script setup lang="ts">
import { CfSankeyDiagram } from '@chufix-design/vue';
const nodes = [
{ id: 'a', name: '搜索引擎', layer: 0 },
{ id: 'b', name: '直接访问', layer: 0 },
{ id: 'c', name: '产品列表', layer: 1 },
{ id: 'd', name: '产品详情', layer: 1 },
{ id: 'e', name: '加入购物车', layer: 2 },
{ id: 'f', name: '完成支付', layer: 2 },
];
const links = [
{ source: 'a', target: 'c', value: 60 },
{ source: 'a', target: 'd', value: 25 },
{ source: 'b', target: 'c', value: 30 },
{ source: 'b', target: 'd', value: 40 },
{ source: 'c', target: 'e', value: 20 },
{ source: 'd', target: 'e', value: 18 },
{ source: 'e', target: 'f', value: 24 },
];
</script>
<template>
<CfSankeyDiagram :nodes="nodes" :links="links" :height="220" />
</template> <script setup>
import { CfSankeyDiagram } from '@chufix-design/vue';
const nodes = [
{ id: 'a', name: '搜索引擎', layer: 0 },
{ id: 'b', name: '直接访问', layer: 0 },
{ id: 'c', name: '产品列表', layer: 1 },
{ id: 'd', name: '产品详情', layer: 1 },
{ id: 'e', name: '加入购物车', layer: 2 },
{ id: 'f', name: '完成支付', layer: 2 },
];
const links = [
{ source: 'a', target: 'c', value: 60 },
{ source: 'a', target: 'd', value: 25 },
{ source: 'b', target: 'c', value: 30 },
{ source: 'b', target: 'd', value: 40 },
{ source: 'c', target: 'e', value: 20 },
{ source: 'd', target: 'e', value: 18 },
{ source: 'e', target: 'f', value: 24 },
];
</script>
<template>
<CfSankeyDiagram :nodes="nodes" :links="links" :height="220" />
</template> import { CfSankeyDiagram } from '@chufix-design/react';
export default function Demo() {
const nodes = [
{ id: 'a', name: '搜索引擎', layer: 0 },
{ id: 'b', name: '直接访问', layer: 0 },
{ id: 'c', name: '产品列表', layer: 1 },
{ id: 'd', name: '产品详情', layer: 1 },
{ id: 'e', name: '加入购物车', layer: 2 },
{ id: 'f', name: '完成支付', layer: 2 },
];
const links = [
{ source: 'a', target: 'c', value: 60 },
{ source: 'a', target: 'd', value: 25 },
{ source: 'b', target: 'c', value: 30 },
{ source: 'b', target: 'd', value: 40 },
{ source: 'c', target: 'e', value: 20 },
{ source: 'd', target: 'e', value: 18 },
{ source: 'e', target: 'f', value: 24 },
];
return (
<>
<CfSankeyDiagram nodes={nodes} links={links} height={220} />
</>
);
} import { CfSankeyDiagram } from '@chufix-design/react';
export default function Demo() {
const nodes = [
{ id: 'a', name: '搜索引擎', layer: 0 },
{ id: 'b', name: '直接访问', layer: 0 },
{ id: 'c', name: '产品列表', layer: 1 },
{ id: 'd', name: '产品详情', layer: 1 },
{ id: 'e', name: '加入购物车', layer: 2 },
{ id: 'f', name: '完成支付', layer: 2 },
];
const links = [
{ source: 'a', target: 'c', value: 60 },
{ source: 'a', target: 'd', value: 25 },
{ source: 'b', target: 'c', value: 30 },
{ source: 'b', target: 'd', value: 40 },
{ source: 'c', target: 'e', value: 20 },
{ source: 'd', target: 'e', value: 18 },
{ source: 'e', target: 'f', value: 24 },
];
return (
<>
<CfSankeyDiagram nodes={nodes} links={links} height={220} />
</>
);
} Node drag
draggable defaults to true. Press and drag any node vertically to manually reorder its layer; links update in real time. Subscribe to @node-drag / onNodeDrag for the drop-position delta. Set draggable={false} for a read-only view.
背景 视口
竖直拖拽 → 自动按落点 y 重排同层顺序;横向拖到另一列附近 → 节点跨层迁移,源层 / 目标层同时收紧。
last drag 尚未拖拽
<script setup lang="ts">
import { ref } from 'vue';
import { CfSankeyDiagram, CfTag } from '@chufix-design/vue';
const nodes = [
{ id: 'src-organic', name: '自然搜索', layer: 0, colorIndex: 0 },
{ id: 'src-paid', name: '广告投放', layer: 0, colorIndex: 1 },
{ id: 'src-social', name: '社交分享', layer: 0, colorIndex: 2 },
{ id: 'lp-home', name: '首页', layer: 1, colorIndex: 3 },
{ id: 'lp-product', name: '产品页', layer: 1, colorIndex: 4 },
{ id: 'lp-blog', name: '博客', layer: 1, colorIndex: 5 },
{ id: 'cv-trial', name: '试用注册', layer: 2, colorIndex: 6 },
{ id: 'cv-purchase', name: '直接购买', layer: 2, colorIndex: 7 },
];
const links = [
{ source: 'src-organic', target: 'lp-home', value: 240 },
{ source: 'src-organic', target: 'lp-product', value: 180 },
{ source: 'src-organic', target: 'lp-blog', value: 80 },
{ source: 'src-paid', target: 'lp-home', value: 140 },
{ source: 'src-paid', target: 'lp-product', value: 220 },
{ source: 'src-social', target: 'lp-blog', value: 120 },
{ source: 'src-social', target: 'lp-home', value: 60 },
{ source: 'lp-home', target: 'cv-trial', value: 220 },
{ source: 'lp-home', target: 'cv-purchase', value: 60 },
{ source: 'lp-product', target: 'cv-trial', value: 160 },
{ source: 'lp-product', target: 'cv-purchase', value: 200 },
{ source: 'lp-blog', target: 'cv-trial', value: 90 },
];
const lastDrag = ref<string>('');
function onDrag(p: { node: { name: string }; deltaY: number; layer: number; orderIndex: number; layerChanged: boolean }) {
const dirY = p.deltaY > 0 ? '↓' : p.deltaY < 0 ? '↑' : '=';
const layerNote = p.layerChanged ? ` → 层 ${p.layer}` : '';
lastDrag.value = `${p.node.name} ${dirY}${Math.abs(Math.round(p.deltaY))}px · 位置 #${p.orderIndex + 1}${layerNote}`;
}
</script>
<template>
<p style="margin: 0 0 8px; color: var(--fg-3); font-size: 12px;">
竖直拖拽 → 自动按落点 y 重排同层顺序;横向拖到另一列附近 → 节点跨层迁移,源层 / 目标层同时收紧。
</p>
<CfSankeyDiagram
:nodes="nodes"
:links="links"
:width="640"
:height="320"
:node-width="14"
@node-drag="onDrag"
/>
<p style="margin-top: 8px; font-size: 12px;">
<CfTag tone="info" size="sm">last drag</CfTag>
{{ lastDrag || '尚未拖拽' }}
</p>
</template> <script setup>
import { ref } from 'vue';
import { CfSankeyDiagram, CfTag } from '@chufix-design/vue';
const nodes = [
{ id: 'src-organic', name: '自然搜索', layer: 0, colorIndex: 0 },
{ id: 'src-paid', name: '广告投放', layer: 0, colorIndex: 1 },
{ id: 'src-social', name: '社交分享', layer: 0, colorIndex: 2 },
{ id: 'lp-home', name: '首页', layer: 1, colorIndex: 3 },
{ id: 'lp-product', name: '产品页', layer: 1, colorIndex: 4 },
{ id: 'lp-blog', name: '博客', layer: 1, colorIndex: 5 },
{ id: 'cv-trial', name: '试用注册', layer: 2, colorIndex: 6 },
{ id: 'cv-purchase', name: '直接购买', layer: 2, colorIndex: 7 },
];
const links = [
{ source: 'src-organic', target: 'lp-home', value: 240 },
{ source: 'src-organic', target: 'lp-product', value: 180 },
{ source: 'src-organic', target: 'lp-blog', value: 80 },
{ source: 'src-paid', target: 'lp-home', value: 140 },
{ source: 'src-paid', target: 'lp-product', value: 220 },
{ source: 'src-social', target: 'lp-blog', value: 120 },
{ source: 'src-social', target: 'lp-home', value: 60 },
{ source: 'lp-home', target: 'cv-trial', value: 220 },
{ source: 'lp-home', target: 'cv-purchase', value: 60 },
{ source: 'lp-product', target: 'cv-trial', value: 160 },
{ source: 'lp-product', target: 'cv-purchase', value: 200 },
{ source: 'lp-blog', target: 'cv-trial', value: 90 },
];
const lastDrag = ref<string>('');
function onDrag(p: { node: { name: string }; deltaY: number; layer: number; orderIndex: number; layerChanged: boolean }) {
const dirY = p.deltaY > 0 ? '↓' : p.deltaY < 0 ? '↑' : '=';
const layerNote = p.layerChanged ? ` → 层 ${p.layer}` : '';
lastDrag.value = `${p.node.name} ${dirY}${Math.abs(Math.round(p.deltaY))}px · 位置 #${p.orderIndex + 1}${layerNote}`;
}
</script>
<template>
<p style="margin: 0 0 8px; color: var(--fg-3); font-size: 12px;">
竖直拖拽 → 自动按落点 y 重排同层顺序;横向拖到另一列附近 → 节点跨层迁移,源层 / 目标层同时收紧。
</p>
<CfSankeyDiagram
:nodes="nodes"
:links="links"
:width="640"
:height="320"
:node-width="14"
@node-drag="onDrag"
/>
<p style="margin-top: 8px; font-size: 12px;">
<CfTag tone="info" size="sm">last drag</CfTag>
{{ lastDrag || '尚未拖拽' }}
</p>
</template> import { useState } from 'react';
import { CfSankeyDiagram, CfTag } from '@chufix-design/react';
export default function Demo() {
const nodes = [
{ id: 'src-organic', name: '自然搜索', layer: 0, colorIndex: 0 },
{ id: 'src-paid', name: '广告投放', layer: 0, colorIndex: 1 },
{ id: 'src-social', name: '社交分享', layer: 0, colorIndex: 2 },
{ id: 'lp-home', name: '首页', layer: 1, colorIndex: 3 },
{ id: 'lp-product', name: '产品页', layer: 1, colorIndex: 4 },
{ id: 'lp-blog', name: '博客', layer: 1, colorIndex: 5 },
{ id: 'cv-trial', name: '试用注册', layer: 2, colorIndex: 6 },
{ id: 'cv-purchase', name: '直接购买', layer: 2, colorIndex: 7 },
];
const links = [
{ source: 'src-organic', target: 'lp-home', value: 240 },
{ source: 'src-organic', target: 'lp-product', value: 180 },
{ source: 'src-organic', target: 'lp-blog', value: 80 },
{ source: 'src-paid', target: 'lp-home', value: 140 },
{ source: 'src-paid', target: 'lp-product', value: 220 },
{ source: 'src-social', target: 'lp-blog', value: 120 },
{ source: 'src-social', target: 'lp-home', value: 60 },
{ source: 'lp-home', target: 'cv-trial', value: 220 },
{ source: 'lp-home', target: 'cv-purchase', value: 60 },
{ source: 'lp-product', target: 'cv-trial', value: 160 },
{ source: 'lp-product', target: 'cv-purchase', value: 200 },
{ source: 'lp-blog', target: 'cv-trial', value: 90 },
];
const [lastDrag, setLastDrag] = useState<string>('');
function onDrag(p: { node: { name: string }; deltaY: number; layer: number; orderIndex: number; layerChanged: boolean }) {
const dirY = p.deltaY > 0 ? '↓' : p.deltaY < 0 ? '↑' : '=';
const layerNote = p.layerChanged ? ` → 层 ${p.layer}` : '';
setLastDrag(`${p.node.name} ${dirY}${Math.abs(Math.round(p.deltaY))}px · 位置 #${p.orderIndex + 1}${layerNote}`);
}
return (
<>
<p style={{ margin: "0 0 8px", color: "var(--fg-3)", fontSize: 12 }}>
竖直拖拽 → 自动按落点 y 重排同层顺序;横向拖到另一列附近 → 节点跨层迁移,源层 / 目标层同时收紧。
</p>
<CfSankeyDiagram nodes={nodes} links={links} width={640} height={320} nodeWidth={14} onNodeDrag={onDrag} />
<p style={{ marginTop: 8, fontSize: 12 }}>
<CfTag tone="info" size="sm">last drag</CfTag>
{lastDrag || '尚未拖拽'}
</p>
</>
);
} import { useState } from 'react';
import { CfSankeyDiagram, CfTag } from '@chufix-design/react';
export default function Demo() {
const nodes = [
{ id: 'src-organic', name: '自然搜索', layer: 0, colorIndex: 0 },
{ id: 'src-paid', name: '广告投放', layer: 0, colorIndex: 1 },
{ id: 'src-social', name: '社交分享', layer: 0, colorIndex: 2 },
{ id: 'lp-home', name: '首页', layer: 1, colorIndex: 3 },
{ id: 'lp-product', name: '产品页', layer: 1, colorIndex: 4 },
{ id: 'lp-blog', name: '博客', layer: 1, colorIndex: 5 },
{ id: 'cv-trial', name: '试用注册', layer: 2, colorIndex: 6 },
{ id: 'cv-purchase', name: '直接购买', layer: 2, colorIndex: 7 },
];
const links = [
{ source: 'src-organic', target: 'lp-home', value: 240 },
{ source: 'src-organic', target: 'lp-product', value: 180 },
{ source: 'src-organic', target: 'lp-blog', value: 80 },
{ source: 'src-paid', target: 'lp-home', value: 140 },
{ source: 'src-paid', target: 'lp-product', value: 220 },
{ source: 'src-social', target: 'lp-blog', value: 120 },
{ source: 'src-social', target: 'lp-home', value: 60 },
{ source: 'lp-home', target: 'cv-trial', value: 220 },
{ source: 'lp-home', target: 'cv-purchase', value: 60 },
{ source: 'lp-product', target: 'cv-trial', value: 160 },
{ source: 'lp-product', target: 'cv-purchase', value: 200 },
{ source: 'lp-blog', target: 'cv-trial', value: 90 },
];
const [lastDrag, setLastDrag] = useState<string>('');
function onDrag(p: { node: { name: string }; deltaY: number; layer: number; orderIndex: number; layerChanged: boolean }) {
const dirY = p.deltaY > 0 ? '↓' : p.deltaY < 0 ? '↑' : '=';
const layerNote = p.layerChanged ? ` → 层 ${p.layer}` : '';
setLastDrag(`${p.node.name} ${dirY}${Math.abs(Math.round(p.deltaY))}px · 位置 #${p.orderIndex + 1}${layerNote}`);
}
return (
<>
<p style={{ margin: "0 0 8px", color: "var(--fg-3)", fontSize: 12 }}>
竖直拖拽 → 自动按落点 y 重排同层顺序;横向拖到另一列附近 → 节点跨层迁移,源层 / 目标层同时收紧。
</p>
<CfSankeyDiagram nodes={nodes} links={links} width={640} height={320} nodeWidth={14} onNodeDrag={onDrag} />
<p style={{ marginTop: 8, fontSize: 12 }}>
<CfTag tone="info" size="sm">last drag</CfTag>
{lastDrag || '尚未拖拽'}
</p>
</>
);
} API
| Prop | Type | Default | Description |
|---|---|---|---|
nodes | SankeyNode[] | — | { id, name, layer?, colorIndex? }[] |
links | SankeyLink[] | — | { source, target, value }[] |
nodeWidth | number | 12 | Node bar width |
draggable | boolean | true | Allow vertical drag to reorder nodes within a layer |
反馈与讨论
Sankey diagram · Discussion