开发预览 更新于 2026-05-10

SankeyDiagram 流向图

节点 + 加权边的流向图,节点高度 = 流量。

基础用法

数据通过 props 传入,纯 SVG 渲染,无第三方图表库依赖。 配色取自 --viz-1..8 token,色盲友好。

背景 视口
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} />
    </>
  );
}

购物路径 3 层

搜索引擎 / 直接访问 → 产品列表 / 详情 → 加购 / 支付 三层流向。

背景 视口
搜索引擎直接访问产品列表产品详情加入购物车完成支付
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} />
    </>
  );
}

节点拖拽 + 跨层迁移

draggable 默认 true。按住任意节点:

  • 竖直拖: 落手后组件按 y 中心重排同层顺序,内部 orderOverrides 记忆这个顺序。
  • 横向拖到另一列: 节点跨层迁移到最近的 layer,源层 / 目标层都会自动收紧 — 不需要外部重算。
  • 落手后清掉漂浮 offset,下次再拖从新的基础位置开始。
  • 监听 @node-drag 拿到完整 payload { node, y, deltaY, layer, orderIndex, layerChanged },可以反写到外部数据源做持久化。

draggable={false} 退回只读视图。

背景 视口

竖直拖拽 → 自动按落点 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

属性类型默认值说明
nodesSankeyNode[]{ id, name, layer?, colorIndex? }[]
linksSankeyLink[]{ source, target, value }[]
widthnumber480SVG 宽度
heightnumber280SVG 高度
nodeWidthnumber12节点条宽
draggablebooleantrue允许竖直拖拽节点重排顺序
ariaLabelstring透传给根 <svg>aria-label

Events

Vue 事件React 回调载荷类型说明
node-enteronNodeEnterSankeyNodeInteractionPayload鼠标进入节点
node-leaveonNodeLeaveSankeyNodeInteractionPayload鼠标离开节点
link-enteronLinkEnterSankeyLinkInteractionPayload鼠标进入连接边
link-leaveonLinkLeaveSankeyLinkInteractionPayload鼠标离开连接边
node-dragonNodeDragSankeyDragPayload拖拽落手后,载荷含 node / y / deltaY / layer / orderIndex / layerChanged

类型

interface SankeyNodeInteractionPayload {
  node: SankeyNode;
  nodeIndex: number;
  nativeEvent?: PointerEvent;
}

interface SankeyLinkInteractionPayload {
  link: SankeyLink;
  linkIndex: number;
  nativeEvent?: PointerEvent;
}

反馈与讨论

SankeyDiagram 流向图 的讨论

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