Preview Updated 2026-05-10

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.

背景 视口
OrganicPaidReferralWebMobile2xx3xx4xx/5xx
src/App.vue
<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.

背景 视口
搜索引擎直接访问产品列表产品详情加入购物车完成支付
src/App.vue
<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 尚未拖拽

src/App.vue
<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

PropTypeDefaultDescription
nodesSankeyNode[]{ id, name, layer?, colorIndex? }[]
linksSankeyLink[]{ source, target, value }[]
nodeWidthnumber12Node bar width
draggablebooleantrueAllow vertical drag to reorder nodes within a layer

反馈与讨论

Sankey diagram · Discussion

0
0 / 600
正在加载评论...