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.
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.
<RadarChart {...props} niceMaxValue /><RadarChart :options="{ ...props, niceMaxValue: true }" /><div use:radarChart={{ ...props, niceMaxValue: true }}></div>applyRadarChartProps(this.c.nativeElement, { ...props, niceMaxValue: true });<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>truedùngrings(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:niceMaxValuechỉ 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().maxValuetrả 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ẽ.
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.
{ 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ỳ.
const ref = useRef<RadarChartHandle>(null);
<RadarChart ref={ref} {...props} timeline={{ speedMs: 1000, loop: true }} />;
// ref.current?.timeline() -> play() / pause() / seek(year) / seekIndex(i) / stepForward()<RadarChart :options="{ ...props, timeline: { speedMs: 1000, loop: true } }" /><div use:radarChart={{ ...props, timeline: { speedMs: 1000, loop: true } }}></div>applyRadarChartProps(this.c.nativeElement, { ...props, timeline: { speedMs: 1000, loop: true } });<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>speedMschỉnh nhịp chạy,loopquay vòng,autoplay: truetự 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ằngtweenMsvàeasing, hoặc đặtinterpolate: 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()choplay() / pause() / toggle() / seek(period) / seekIndex(i) / stepForward() / stepBack(), kèmonStepvàformatPeriodtrong config khi cần tự dựng UI. seek(period)tìm kỳ trước, so sánh dạng chữ, nênseek(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ó
periodvẫn hiển thị ở mọi giai đoạn. timelinethắngprogressiveDrawkhi 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:
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<RadarChart :options="{ ...props, progressiveDraw: { durationMs: 2000 } }" /><div use:radarChart={{ ...props, progressiveDraw: { durationMs: 2000 } }}></div>applyRadarChartProps(this.c.nativeElement, {
...props,
progressiveDraw: { durationMs: 2000 },
});<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>durationMsvàeasing("linear", "easeOutQuad", "easeInOutCubic", hoặc hàm(t) => ttự viết) quyết định nhịp vẽ.autoplay: falsehiển thị biểu đồ vẽ sẵn đầy đủ; gọireplay()(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: truechạ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):
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
import { RadarChart } from "@michi-vz/react";
export default () => <RadarChart {...props} />; // props = the chart options<script setup>
import { RadarChart } from "@michi-vz/vue";
</script>
<template>
<RadarChart :options="props" />
</template><script>
import { radarChart } from "@michi-vz/svelte";
</script>
<div use:radarChart={props}></div>// 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);<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>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.
