Skip to content

Biểu đồ radar ​

So sánh

Lựa chọn nào thắng, và thắng ở đâu? Đặt vài ứng viên chồng lên cùng một tập tiêu chí, mỗi ứng viên thành một đa giác bạn đọc được chỉ trong nháy mắt: đỉnh nhô ra cho thấy mọi điểm mạnh, chỗ lõm vào cho thấy mọi điểm yếu, và phần chồng lấn cho thấy chính xác nơi họ đổi vị trí cho nhau.

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.

Khi nào nên dùng ​

  • Bảng chấm điểm. Đánh giá nhà cung cấp, ứng viên, sản phẩm: vài lựa chọn trên cùng một bộ tiêu chí, mỗi lựa chọn là một đa giác mà đỉnh nhọn là điểm mạnh, chỗ lõm là điểm yếu.
  • Cân bằng hay chuyên biệt. Đa giác càng tròn đều càng toàn diện; đa giác nhọn hoắt đặt cược tất cả vào vài trục. Câu chuyện hình dáng ấy là thứ bảng số liệu không kể được.
  • Giữ ở mức vài thực thể và 5-12 trục. Cần so sánh chính xác trên một tiêu chí, biểu đồ cột so sánh cho đọc đúng từng giá trị; radar dành cho cái nhìn tổng thể.

Làm tròn vòng ngoài cùng thành một giá trị đẹp ​

Khi không có maxValue, vòng ngoài cùng nằm ở giá trị lớn nhất trong dữ liệu, nên với giá trị lớn nhất là 73 các vòng được ghi nhãn 18, 37, 55 và 73. Đặt niceMaxValue thì vòng ngoài cùng được làm tròn lên vạch "đẹp" kế tiếp, theo cùng các bước 1, 2 và 5 × 10ⁿ mà một trục d3 sử dụng, nên các vòng hiển thị 20, 40, 60 và 80.

Example
canvas · responsive
Tìm hiểu thêm: Hướng dẫn Insights·Hướng dẫn DevTools
tsx
<RadarChart {...props} niceMaxValue />
vue
<RadarChart :options="{ ...props, niceMaxValue: true }" />
svelte
<div use:radarChart={{ ...props, niceMaxValue: true }}></div>
ts
applyRadarChartProps(this.c.nativeElement, { ...props, niceMaxValue: true });
html
<michi-vz-radar-chart id="c"></michi-vz-radar-chart>
<script>
  // là property, không phải attribute: nhận true hoặc một số vạch chia
  document.getElementById("c").niceMaxValue = true;
</script>
  • true dùng rings (mặc định 4) làm số vạch chia. Một con số sẽ tự đặt số vạch chia, ví dụ niceMaxValue: 5 để khớp với một trục năm vạch ở chỗ khác trên trang.
  • maxValue được đặt tường minh luôn được ưu tiên: niceMaxValue chỉ làm tròn vòng ngoài cùng mà biểu đồ vốn lấy từ dữ liệu. Giá trị lớn nhất đã nằm đúng trên một vạch thì giữ nguyên (100 vẫn là 100), và dữ liệu toàn số 0 hoặc số âm giữ vòng ngoài cùng mặc định là 1.
  • Chỉ vòng ngoài cùng được bảo đảm là số tròn. Các vòng bên trong là các phần bằng nhau của nó, nên chúng chỉ tròn khi giá trị lớn nhất đã làm tròn chia hết cho rings. 80 chia cho 4 vòng cho 20, 40, 60, 80, còn 50 chia cho 4 vòng cho 12,5, 25, 37,5, 50.
  • getContext().maxValue trả về giá trị đã làm tròn. Phép làm tròn cũng được export dưới dạng hàm thuần, niceRadarMax(dataMax, tickCount) từ @michi-vz/core.

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

RadarChart có renderer="webgpu" tùy chọn, vẽ phần tô đa giác và các marker ở cực bằng các mark do GPU dựng hàng loạt trong khi trục, nhãn và tooltip vẫn nằm trên lớp SVG. Tính năng này được kiểm soát theo khả năng phần cứng: trên trình duyệt không có WebGPU, nó tự động hạ cấp về canvas, và getContext().renderer báo cáo bộ dựng thực sự đã vẽ.

⚗️ 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 · 12 trục × 4 chuỗi… detecting

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

RadarChart đã dùng date cho hình dạng { date, value } theo từng trục kiểu cũ, nên tag của timeline được đặt tên là period thay vì date. Gắn period cho từng dòng chuỗi rồi bật timeline: snapshot của một năm là các dòng chia sẻ period đó, và mỗi đa giác 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.

ts
{ label: "Vienna", period: "2021", values: [72, 65, 40, 88 /* … */] }

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<RadarChartHandle>(null);

<RadarChart ref={ref} {...props} timeline={{ speedMs: 1000, loop: true }} />;
// ref.current?.timeline() -> play() / pause() / seek(year) / seekIndex(i) / stepForward()
vue
<RadarChart :options="{ ...props, timeline: { speedMs: 1000, loop: true } }" />
svelte
<div use:radarChart={{ ...props, timeline: { speedMs: 1000, loop: true } }}></div>
ts
applyRadarChartProps(this.c.nativeElement, { ...props, timeline: { speedMs: 1000, loop: true } });
html
<michi-vz-radar-chart id="c"></michi-vz-radar-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.
  • Chuỗi không có period 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<RadarChartHandle>(null);

<RadarChart
  ref={ref}
  {...props}
  progressiveDraw={{ durationMs: 2000 }}
/>;
// ref.current?.replay() chạy lại hiệu ứng khi cần
vue
<RadarChart :options="{ ...props, progressiveDraw: { durationMs: 2000 } }" />
svelte
<div use:radarChart={{ ...props, progressiveDraw: { durationMs: 2000 } }}></div>
ts
applyRadarChartProps(this.c.nativeElement, {
  ...props,
  progressiveDraw: { durationMs: 2000 },
});
html
<michi-vz-radar-chart id="c"></michi-vz-radar-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.

Hover và tooltip ​

Hover vào một cực sẽ gọi tooltipFormatter với dữ liệu của cực đó cộng thêm date - nhãn trục của cực đang hover - và giờ hành vi này nhất quán trên mọi bộ dựng (svg, canvas, webgpu):

ts
tooltipFormatter: (item) => `${item.date}: ${item.value}`,

Markup tooltip mặc định (khi không truyền tooltipFormatter) không đổi. Hover bám theo con trỏ khi di chuyển trên đa giác thay vì chỉ bắn một lần lúc vừa chạm vào, và highlightItems làm mờ mọi chuỗi khác - các chuỗi bị làm mờ không được hit-test, nên chỉ chuỗi đang active mới phản hồi hover, giống hệt canvas và webgpu. disabledItems là một cơ chế khác: nó loại hẳn một chuỗi khỏi biểu đồ thay vì làm mờ, nên một chuỗi bị disable chưa bao giờ có mặt trong render model để mà hit-test. Click giờ chỉ ghim tooltip khi click đó xác định được đúng một cực; một click trượt (vào khoảng trống, hoặc vào một chuỗi bị làm mờ - vốn không được hit-test) sẽ không còn ghim tooltip nữa, trong khi trước đây trên svg, click vào bất kỳ đâu trên một đa giác - kể cả đa giác bị làm mờ - đều ghim tooltip.

Trên svg, onHighlightItem giờ bắn ở mỗi mousemove xác định được một hit, và bắn [] ở mỗi lần di chuyển không xác định được hit nào - không hề dedupe hay throttle. Nếu handler onHighlightItem của bạn ghi nhãn đang hover vào store hoặc state khác rồi phản hồi ngược lại vào props của biểu đồ, bạn cần tự debounce hoặc dedupe, vì mỗi lần dispatch chạm tới props đều kích hoạt re-render. Đây không phải điều mới của thư viện: canvas và webgpu vốn đã bắn với tần suất này trên radar, và các biểu đồ khác cũng vậy (ví dụ Area, Pie) - hover svg của radar giờ chỉ đơn giản là khớp theo chúng.

Cách dùng ​

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

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

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

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

// component (uses CUSTOM_ELEMENTS_SCHEMA)
// template: <michi-vz-radar-chart #c></michi-vz-radar-chart>
applyRadarChartProps(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-radar-chart id="c"></michi-vz-radar-chart>
<script>
  Object.assign(document.getElementById("c"), props); // dataSet/series, title, …
</script>
ts
import { mountRadarChart } from "@michi-vz/core";

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

API ​

Các prop được định kiểu là RadarChartProps 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.