Skip to content

Sankey ​

Dòng chảy

"Tất cả rồi sẽ đi đâu?" Một biểu đồ Sankey theo dõi dòng chảy qua một hệ thống: các nút được bố trí theo cột, và độ dày của mỗi dải chính là giá trị dòng chảy. Đây là hình ảnh phù hợp cho thương mại giữa các nhà xuất khẩu và thị trường, ngân sách từ nguồn đến nơi sử dụng, lưu lượng truy cập từ nguồn giới thiệu đến các trang - bất cứ nơi nào một đại lượng tách ra rồi hợp lại trên đường đi của nó.

Example
canvas · responsive
Tìm hiểu thêm: Hướng dẫn Insights·Hướng dẫn DevTools

Bố cục được tính toán bằng d3-sankey: các nút được gán vào các cột từ cấu trúc topology của đồ thị, sắp xếp theo chiều dọc, và các liên kết được vẽ thành các dải ngang mượt. Di chuột vào một nút hoặc một dòng chảy để xem con số.

Khi nào nên dùng ​

  • Dòng chảy có cấu trúc. Thương mại giữa nước xuất khẩu và thị trường, ngân sách từ nguồn đến khoản chi, người dùng đi qua phễu: lượng đến từ đâu, đi về đâu, tách nhập ra sao trên đường đi.
  • Tìm tuyến chủ đạo và chỗ rò rỉ. Độ dày của dải chính là giá trị, nên tuyến lớn và những thất thoát nhỏ đều hiện rõ tức thì: góc nhìn kiểm toán cho mọi hệ thống.
  • Chỉ có một chặng? Nếu không có gì chảy xuyên qua, biểu đồ cột so sánh xếp hạng các nguồn gọn gàng hơn một Sankey hai cột.

Làm nổi bật khi rê chuột ​

Rê chuột luôn hiện tooltip. Đặt hoverHighlight: true thì biểu đồ còn làm nổi bật thứ bạn đang trỏ vào: rê chuột lên một nút thì nút đó, mọi dòng chảy vào hoặc ra khỏi nó và các nút ở đầu bên kia vẫn giữ nguyên độ đậm trong khi phần còn lại mờ đi; rê chuột lên một dòng chảy thì chỉ dòng chảy đó và hai nút ở hai đầu của nó còn sáng. Mặc định tắt.

Example
canvas · responsive
Tìm hiểu thêm: Hướng dẫn Insights·Hướng dẫn DevTools
tsx
<SankeyChart {...props} hoverHighlight />
vue
<SankeyChart :options="{ ...props, hoverHighlight: true }" />
svelte
<div use:sankeyChart={{ ...props, hoverHighlight: true }}></div>
ts
applySankeyChartProps(this.c.nativeElement, { ...props, hoverHighlight: true });
html
<michi-vz-sankey-chart hover-highlight></michi-vz-sankey-chart>
  • Hiệu ứng được áp dụng tại chỗ: svg đổi độ mờ của các phần tử đã vẽ sẵn, canvas và webgpu vẽ lại. Rê chuột không bao giờ khiến biểu đồ dựng lại, nên không cần đưa onHighlightItem ngược vào highlightItems để có hiệu ứng này. onHighlightItem vẫn được gọi như trước, cho giao diện riêng của bạn.
  • Mức làm mờ giống hệt highlightItems (dòng chảy ở một phần tư linkOpacity, nút ở 0,25). Trong lúc rê chuột, nó được ưu tiên hơn highlightItems; khi rời đi, trạng thái highlightItems được khôi phục.
  • Click để ghim tooltip thì phần nổi bật cũng được giữ theo cho đến khi bỏ ghim (click vào tooltip, hoặc bất kỳ đâu bên ngoài biểu đồ).
  • Với trình kết xuất svg, hiệu ứng mờ dần tuân theo enableTransitions; canvas và webgpu chuyển ngay lập tức.

Dữ liệu lớn trên WebGPU Thử nghiệm ​

renderer="webgpu" tùy chọn của biểu đồ này vẽ các mark trên GPU trong khi trục/nhãn/tooltip vẫn nằm trên SVG; được kiểm soát theo khả năng phần cứng với tự động chuyển về canvas.

⚗️ Experimental - not yet stable. WebGPU rendering is an opt-in preview. It needs a WebGPU-capable browser (Chrome / Edge, or Safari 26+); everywhere else it falls back to canvas automatically. Axes, labels and tooltips stay on the SVG layer - only the data marks are painted on the GPU.
Heavy-data demo · ~150 liên kết… detecting

Xem dữ liệu chạy theo năm ​

Gắn date cho từng liên kết rồi bật timeline: snapshot của một năm là các liên kết chia sẻ date đó - các nút dùng chung qua mọi năm, chỉ có dòng chảy đến rồi đi - và độ dày dải tự biến hình mượt giữa các năm. Mặc định tắt - không bật thì biểu đồ giữ nguyên. Đây là kiểu chạy từng năm có tương tác, không phải hiệu ứng vào cảnh một lần bên dưới.

Bấm nút play dưới biểu đồ: dữ liệu chạy qua từng năm, mỗi lần một snapshot. Kéo thanh tua để nhảy đến năm bất kỳ.

tsx
const ref = useRef<SankeyChartHandle>(null);

<SankeyChart ref={ref} {...props} timeline={{ speedMs: 1000, loop: true }} />;
// ref.current?.timeline() -> play() / pause() / seek(year) / seekIndex(i) / stepForward()
vue
<SankeyChart :options="{ ...props, timeline: { speedMs: 1000, loop: true } }" />
svelte
<div use:sankeyChart={{ ...props, timeline: { speedMs: 1000, loop: true } }}></div>
ts
applySankeyChartProps(this.c.nativeElement, { ...props, timeline: { speedMs: 1000, loop: true } });
html
<michi-vz-sankey-chart id="c"></michi-vz-sankey-chart>
<script>
  const el = document.getElementById("c");
  el.timeline = { speedMs: 1000, loop: true };
  // el.getTimeline() -> play() / pause() / seek(year) / seekIndex(i)
</script>
  • speedMs chỉnh nhịp chạy, loop quay vòng, autoplay: true tự chạy khi mount, showControl: false ẩn thanh điều khiển có sẵn.
  • Giá trị trượt mượt giữa các giai đoạn theo mặc định (interpolate); chỉnh chuyển động bằng tweenMs và easing, hoặc đặt interpolate: false để cắt thẳng. Khi bật reduced motion, biểu đồ luôn cắt thẳng.
  • Controller headless luôn sẵn sàng: chart.timeline() cho play() / pause() / toggle() / seek(period) / seekIndex(i) / stepForward() / stepBack(), kèm onStep và formatPeriod trong config khi cần tự dựng UI.
  • seek(period) tìm kỳ trước, so sánh dạng chữ, nên seek(2021) và seek("2021") đều tới 2021 dù ngày trong dữ liệu là số hay chuỗi. Số chỉ được hiểu là vị trí (0 = kỳ đầu) khi không có kỳ nào khớp, còn chuỗi không khớp kỳ nào thì không làm gì. seekIndex(i) luôn đi theo vị trí, giống thanh tua có sẵn.
  • Liên kết không có date vẫn hiển thị ở mọi giai đoạn.
  • timeline thắng progressiveDraw khi cả hai cùng đặt - hiệu ứng vẽ dần bên dưới đứng yên khi timeline đang điều khiển.

Hiệu ứng vẽ dần ​

Biểu đồ tự vẽ dần từ trái sang phải ngay khi mount, hiện lần lượt các nét vẽ trước khi ổn định vào vị trí. Mặc định tắt - không bật thì biểu đồ giữ nguyên như cũ.

Mỗi đường lớn dần từ năm đầu đến năm cuối; nhãn bám theo ngọn đường rồi dừng ở điểm cuối. Khi hệ điều hành bật reduced motion, biểu đồ hiển thị đầy đủ ngay lập tức.

progressiveDraw: true dùng cấu hình mặc định (1200 ms, easeInOutCubic). Truyền object để tinh chỉnh:

tsx
const ref = useRef<SankeyChartHandle>(null);

<SankeyChart
  ref={ref}
  {...props}
  progressiveDraw={{ durationMs: 2000 }}
/>;
// ref.current?.replay() chạy lại hiệu ứng khi cần
vue
<SankeyChart :options="{ ...props, progressiveDraw: { durationMs: 2000 } }" />
svelte
<div use:sankeyChart={{ ...props, progressiveDraw: { durationMs: 2000 } }}></div>
ts
applySankeyChartProps(this.c.nativeElement, {
  ...props,
  progressiveDraw: { durationMs: 2000 },
});
html
<michi-vz-sankey-chart id="c"></michi-vz-sankey-chart>
<script>
  const el = document.getElementById("c");
  el.progressiveDraw = { durationMs: 2000 };
  // el.replay() chạy lại hiệu ứng
</script>
  • durationMs và easing ("linear", "easeOutQuad", "easeInOutCubic", hoặc hàm (t) => t tự viết) quyết định nhịp vẽ.
  • autoplay: false hiển thị biểu đồ vẽ sẵn đầy đủ; gọi replay() (ref handle bên React, method của web component, hoặc instance của core) để chạy hiệu ứng khi cần. replayOnUpdate: true chạy lại mỗi lần dữ liệu thay đổi.
  • Tôn trọng prefers-reduced-motion: biểu đồ hiển thị đầy đủ ngay, không animation.
  • Hiệu ứng vẽ dần chỉ chạy một lần khi mount; mục xem dữ liệu chạy theo năm ở trên chạy từng năm một theo yêu cầu.

Cách dùng ​

tsx
import { SankeyChart } from "@michi-vz/react";

export default () => <SankeyChart {...props} />; // props = the chart options
vue
<script setup>
import { SankeyChart } from "@michi-vz/vue";
</script>

<template>
  <SankeyChart :options="props" />
</template>
svelte
<script>
  import { sankeyChart } from "@michi-vz/svelte";
</script>

<div use:sankeyChart={props}></div>
ts
// main.ts - register the elements once
import "@michi-vz/angular";
import { applySankeyChartProps } from "@michi-vz/angular";

// component (uses CUSTOM_ELEMENTS_SCHEMA)
// template: <michi-vz-sankey-chart #c></michi-vz-sankey-chart>
applySankeyChartProps(this.c.nativeElement, props);
html
<script type="module" src="https://cdn.jsdelivr.net/npm/@michi-vz/wc/dist/michi-vz-wc.bundle.js"></script>

<michi-vz-sankey-chart id="c"></michi-vz-sankey-chart>
<script>
  Object.assign(document.getElementById("c"), props); // nodes, links, …
</script>
ts
import { mountSankeyChart } from "@michi-vz/core";

const chart = mountSankeyChart(el, props);
chart.update(next);
chart.getContext(); // renderer-agnostic, LLM-ready
chart.destroy();

Cấu trúc dữ liệu ​

Khác với các biểu đồ khác, một Sankey nhận hai mảng: nodes (mỗi nút có một id duy nhất, một label và color tùy chọn) và links (source → target theo id, kèm một value).

ts
const props = {
  linkColorMode: "source", // colour links by their source (or "target")
  nodes: [
    { id: "France" }, { id: "Germany" },
    { id: "EU" }, { id: "Asia" },
  ],
  links: [
    { source: "France", target: "EU", value: 40 },
    { source: "France", target: "Asia", value: 22 },
    { source: "Germany", target: "EU", value: 55 },
    { source: "Germany", target: "Asia", value: 35 },
  ],
};

Một liên kết trỏ tới một id nút không xác định (hoặc một nút nằm trong disabledItems) sẽ bị bỏ kèm một datawarning; vô hiệu hóa một nút cũng bỏ luôn các liên kết của nó.

Các tùy chỉnh bố cục ​

nodeWidth đặt chiều rộng hình chữ nhật của nút, nodePadding khoảng cách dọc giữa các nút trong một cột, và linkOpacity độ trong suốt của các dải. linkColorMode tô màu mỗi dải theo nút source (mặc định) hoặc target của nó. Bản sao a11y và getContext() hiển thị các liên kết dưới dạng bảng "Nguồn → Đích: giá trị" có thể đọc được.

Nút bo tròn. nodeRadius (px, mặc định 2) bo tròn các góc hình chữ nhật của nút - tăng lên để có kiểu viên thuốc, hoặc đặt 0 cho góc vuông. Giá trị này bị giới hạn ở nửa cạnh ngắn hơn của nút, nên không bao giờ làm biến dạng một nút mỏng.

Dòng chảy bo tròn. Các dòng chảy được vẽ dưới dạng dải tô đặc; linkRadius (px, mặc định 2) bo tròn các góc của chúng nơi gặp các nút, tạo kết nối mềm mại hơn (bị giới hạn ở nửa độ dày của dải; 0 = sắc cạnh). linkColorMode tô màu mỗi dòng chảy theo nút source hoặc target của nó, ở mức linkOpacity:

ts
const props = { nodeRadius: 4, linkRadius: 4, /* …nodes, links */ };

Thứ tự nút. Mặc định d3-sankey sắp xếp các nút trong một cột để giảm giao cắt. nodeSort: "input" giữ nguyên thứ tự của mảng nodes (và thứ tự của links cho các dải rời khỏi một nút), nên một nút "Phần còn lại của thế giới" hoặc "Khác" sẽ ở đúng chỗ bạn liệt kê, thường là cuối cùng.

Dòng chảy mỏng. Một dòng nhỏ bên cạnh một dòng lớn có thể chỉ là dải rộng một pixel, không thể rê chuột tới. minLinkValue (theo đơn vị giá trị) bố trí những liên kết đó như thể chúng có ít nhất giá trị này; chỉ hình học thay đổi, còn tooltip, context và các tổng giữ giá trị thật. Hãy chọn từ dữ liệu của bạn, ví dụ 15% của liên kết lớn nhất.

Nhấp chuột. onNodeClick(node, event) và onLinkClick(link, event) được gọi khi nhấp vào một nút hoặc một dòng chảy, trong svg cũng như qua kiểm tra va chạm trên canvas, với cùng context mà trình định dạng tooltip nhận. Nhấp chuột vẫn ghim tooltip; dùng các callback để điều hướng hoặc chọn ngay từ phần tử.

ts
const props = { nodeSort: "input", minLinkValue: 0.15 * maxLinkValue, onNodeClick: (n) => go(n.id) };

API ​

Các prop được định kiểu là SankeyChartProps trong @michi-vz/core. Dùng chung cho mọi biểu đồ: width, height, margin, colors / colorsMapping, renderer ("svg", "canvas", hoặc "webgpu" thử nghiệm), highlightItems, disabledItems, và các callback on*. onChartDataProcessed / getContext() trả về ChartContext không phụ thuộc bộ dựng. Tham chiếu đầy đủ: API Sankey.