Skip to content

Cây tỏa tròn

Cấu trúc

Biểu đồ #21 - biểu đồ mới cuối cùng trong quá trình di trú sdg-trade: một sơ đồ cụm/dendrogram tỏa tròn. Mục tiêu di trú cho TreeRadial cũ của sdg-trade - các nhóm tỏa ra từ một điểm trung tâm, các lá của mỗi nhóm nằm trên "nan hoa" tỏa tròn riêng, mỗi lá đều nằm ở CÙNG khoảng cách với tâm (một dendrogram thực thụ - xem ghi chú hành vi bên dưới).

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

Càng nhiều lá càng vượt qua các ngưỡng mật độ nhãn thích ứng - nhãn được viết tắt và xoay theo hướng tỏa tròn, rồi biến mất hoàn toàn khi cây trở nên rất dày đặc:

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

Biểu đồ trên là cùng một engine trên mọi framework - chỉ có mã tích hợp bên dưới là khác nhau.

Hình dạng phản chiếu Treemap

RadialTreeNode cố tình phản chiếu hình dạng của TreemapNode - label / code / value / color / children - để nhất quán API giữa hai biểu đồ phân cấp. Nhóm màu của một nút là label của tổ tiên CẤP CAO NHẤT của nó, giống hệt Treemap: một lá kế thừa màu của nhóm trừ khi chính nó (hoặc nhóm) đặt color riêng.

ts
import { RadialTreeChart } from "@michi-vz/react";

<RadialTreeChart
  centerLabel="Tổng giá trị thương mại"
  dataSet={[
    {
      label: "Nông nghiệp",
      children: [
        { label: "Cà phê", value: 8 },
        { label: "Trà", value: 5 },
        // ...
      ],
    },
    // ...
  ]}
/>

Giá trị riêng của một nhóm LUÔN LUÔN là tổng các con của nó (một value được khai báo rõ ràng trên một nút có children sẽ bị bỏ qua) - vì vậy bạn chỉ cần cung cấp giá trị cho các lá.

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

Gắn date cho từng nút cấp gốc rồi bật timeline: snapshot của một năm là các nút gốc chia sẻ date đó - các nút con không cần gắn ngày riêng - và các vòng tròn tự biến hình mượt giữa các năm khi đổi kích thước. 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<RadialTreeChartHandle>(null);

<RadialTreeChart ref={ref} {...props} timeline={{ speedMs: 1000, loop: true }} />;
// ref.current?.timeline() -> play() / pause() / seek(year) / stepForward()
vue
<RadialTreeChart :options="{ ...props, timeline: { speedMs: 1000, loop: true } }" />
svelte
<div use:radialTreeChart={{ ...props, timeline: { speedMs: 1000, loop: true } }}></div>
ts
applyRadialTreeChartProps(this.c.nativeElement, { ...props, timeline: { speedMs: 1000, loop: true } });
html
<michi-vz-radial-tree-chart id="c"></michi-vz-radial-tree-chart>
<script>
  const el = document.getElementById("c");
  el.timeline = { speedMs: 1000, loop: true };
  // el.getTimeline() -> play() / pause() / seek(year)
</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 tweenMseasing, 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) / stepForward() / stepBack(), kèm onStepformatPeriod trong config khi cần tự dựng UI.
  • Nút gốc 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<RadialTreeChartHandle>(null);

<RadialTreeChart
  ref={ref}
  {...props}
  progressiveDraw={{ durationMs: 2000 }}
/>;
// ref.current?.replay() chạy lại hiệu ứng khi cần
vue
<RadialTreeChart :options="{ ...props, progressiveDraw: { durationMs: 2000 } }" />
svelte
<div use:radialTreeChart={{ ...props, progressiveDraw: { durationMs: 2000 } }}></div>
ts
applyRadialTreeChartProps(this.c.nativeElement, {
  ...props,
  progressiveDraw: { durationMs: 2000 },
});
html
<michi-vz-radial-tree-chart id="c"></michi-vz-radial-tree-chart>
<script>
  const el = document.getElementById("c");
  el.progressiveDraw = { durationMs: 2000 };
  // el.replay() chạy lại hiệu ứng
</script>
  • durationMseasing ("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 { RadialTreeChart } from "@michi-vz/react";

export default () => <RadialTreeChart {...props} />; // props = tùy chọn biểu đồ
vue
<script setup>
import { RadialTreeChart } from "@michi-vz/vue";
</script>

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

<div use:radialTreeChart={props}></div>
ts
// main.ts - đăng ký các phần tử một lần
import "@michi-vz/angular";
import { applyRadialTreeChartProps } from "@michi-vz/angular";

// component (dùng CUSTOM_ELEMENTS_SCHEMA)
// template: <michi-vz-radial-tree-chart #c></michi-vz-radial-tree-chart>
applyRadialTreeChartProps(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-radial-tree-chart id="c"></michi-vz-radial-tree-chart>
<script>
  Object.assign(document.getElementById("c"), props); // dataSet, title, …
</script>
ts
import { mountRadialTreeChart } from "@michi-vz/core";

const chart = mountRadialTreeChart(el, props);
chart.update(next);
chart.getContext(); // không phụ thuộc renderer, sẵn sàng cho LLM
chart.destroy();

API

Props được định kiểu là RadialTreeChartProps 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 renderer.

Ghi chú hành vi

cluster(), không phải tree()

Bố cục được xây dựng bằng cluster() của d3-hierarchy - đã được xác minh khớp với lệnh gọi chính xác của biểu đồ cũ - KHÔNG PHẢI tree(). cluster() đặt mọi LÁ ở cùng khoảng cách tỏa tròn tính từ tâm bất kể nhánh của nó có bao nhiêu cấp, điều này khiến đây là một dendrogram thực thụ; tree() thay vào đó sẽ định kích thước mỗi nhánh theo độ sâu cây con riêng của nó, khiến các lá ở độ sâu khác nhau nằm ở bán kính khác nhau.

Vòng tròn hai cấp, một thang tuyến tính

Cả vòng tròn NHÓM (thu phóng theo tổng của nhóm) và mỗi vòng tròn LÁ (thu phóng theo giá trị riêng) đều được vẽ từ CÙNG một thang tuyến tính (đã xác minh khớp với scaleLinear của biểu đồ cũ - không phải thang căn bậc hai) trên miền kết hợp giá trị của mọi nhóm VÀ mọi lá. radiusRange (mặc định [2, 32], circleRange cũ) đặt phạm vi đầu ra của thang đo.

Mật độ nhãn thích ứng

Nhãn phản ứng theo tổng số LÁ thông qua labelDensityThresholds:

  • Dưới rotateAbove (mặc định 20): mọi nút hiển thị tên đầy đủ; ở mật độ thấp đến trung bình, tên của một nhóm cấp cao nhất còn bị cắt ngắn còn 10 ký tự khi số lượng vượt quá một nửa rotateAbove (một đặc điểm cũ được giữ lại - lá không bao giờ bị cắt ngắn theo cách này).
  • Vượt qua rotateAbove: mọi nhãn được viết tắt còn 3 chữ cái + "." và xoay theo hướng tỏa tròn thay vì giữ nằm ngang.
  • Vượt qua hideAbove (mặc định 100): không có nhãn nào được vẽ.

Tự động xuống dòng cho centerLabel

centerLabel (titleCenter cũ) vẽ một vòng tròn nhỏ ở giữa (bằng một phần tư bán kính ngoài) với văn bản tự động xuống dòng sau khoảng 10 ký tự - một phiên bản đơn giản hóa, tất định của thuật toán xuống dòng dựa trên độ rộng pixel của biểu đồ cũ.

Liên kết

Mỗi nút vẽ một liên kết cong (đường cong Bézier bậc ba) trở về nút cha - các "nan hoa" tỏa tròn của dendrogram - được chuyển thể từ công thức điểm điều khiển của biểu đồ cũ. Các liên kết được vẽ thành một lớp nền duy nhất, vì vậy liên kết không bao giờ che khuất vòng tròn về mặt hình ảnh (một đơn giản hóa thuần túy về mặt thẩm mỹ, được ghi chú, so với cách lồng DOM theo từng nút của biểu đồ cũ).

Lồng sâu hơn 2 cấp

Hợp đồng với người dùng là 2 cấp (nhóm + lá), nhưng lồng sâu hơn vẫn được chấp nhận: mỗi cấp bổ sung vẫn có vòng tròn được thu phóng và một liên kết, onDataWarning sẽ báo (excess-depth), và các quy tắc mật độ nhãn vẫn được áp dụng (quy tắc cắt ngắn chỉ dành cho cấp 1 sẽ ngừng áp dụng dưới cấp cao nhất).

Kết xuất: SVG, canvas, và WebGPU ủy quyền

renderer="svg" (mặc định) vẽ một <circle class="radial-tree-node-circle"> cho mỗi nút. renderer="canvas" vẽ cùng các dấu đó lên canvas 2D, phân giải màu tô qua cùng cơ chế dò CSS của người dùng mà mọi biểu đồ một-dấu sử dụng. renderer="webgpu" ủy quyền cho bộ kết xuất canvas-2D, cùng lý do như Choropleth Map / Symbol Map: các liên kết của dendrogram là đường cong Bézier, nên việc tam giác hóa GPU chính xác là công việc không tương xứng ở đây.

Đang tải / không có dữ liệu

isLoadingisNodata điều khiển lớp phủ (React: isLoadingComponent / isNodataComponent), giống hệt mọi biểu đồ khác trong thư viện.

Cơ chế màu do người dùng kiểm soát: context mang theo legendData ({ label, color, dataLabelSafe }, một dòng cho mỗi nhóm cấp cao nhất) để hệ thống màu tiêm CSS có thể nhắm mục tiêu quy tắc theo từng label; onChartDataProcessed chỉ được phát ra khi context thay đổi.