Skip to content

Biểu đồ bong bóng ​

Cấu thành

"Mỗi cái lớn cỡ nào, và bao nhiêu phần trong đó đã được hiện thực hóa?" Một đám mây bong bóng trả lời về độ lớn chỉ trong nháy mắt: mỗi vòng tròn có kích thước theo giá trị (diện tích, không phải bán kính), và một mô phỏng trọng lực kéo chúng vào một cụm gọn gàng để những cái lớn rõ ràng nổi bật. Giống như treemap, mỗi bong bóng có thể mang một phần tách hai phần - một lõi đã hiện thực hóa đặc bên trong một vành chưa khai thác nhạt hơn - để bạn đọc được cả quy mô lẫn tiến độ cùng lúc.

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

Không cần tách? Bỏ partial để có một đám mây tỷ lệ gọn gàng, mỗi danh mục một màu:

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

Cụm được bố trí bằng d3-force: các bong bóng rơi về phía trung tâm (gravity) và đẩy nhau ra để không bao giờ chồng lấn (va chạm). Mô phỏng được ổn định đồng bộ, nên SVG và canvas dựng ra cùng một bố cục giống hệt nhau, có thể tái tạo được.

Khi nào nên dùng ​

  • Độ lớn trong nháy mắt. Sản phẩm, thị trường, từ khóa hiện thành một đám mây bong bóng có kích thước: khi "cái nào lớn?" quan trọng hơn thứ hạng chính xác, cụm bong bóng trả lời tức thì.
  • Bản đồ cơ hội. Với phần chia, một bong bóng to mà lõi đã hiện thực hóa còn mỏng nghĩa là tiền vẫn đang nằm trên bàn: góc nhìn quét tìm dư địa cho các buổi rà soát danh mục.
  • Nếu vị trí cần mang ý nghĩa (hai trục số, mối tương quan), đó là việc của biểu đồ phân tán; đám mây bong bóng đánh đổi vị trí lấy sự gọn gàng.

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

BubbleChart có tùy chọn renderer="webgpu" để vẽ đám mây bong bóng dưới dạng các vòng tròn theo instance trên GPU trong khi nhãn và tooltip vẫn ở lớp SVG. Tính năng này được kiểm soát theo khả năng: trên trình duyệt không có WebGPU, nó sẽ tự động hạ cấp xuống canvas, và getContext().renderer báo cáo bất kỳ renderer nào thực sự đã vẽ.

Giống trang biểu đồ phân tán, demo bên dưới cũng mượn cảm hứng từ vật lý hạt: ~1.500 cụm năng lượng tái dựng từ một sự kiện va chạm mô phỏng, mỗi cụm một bong bóng, tô màu theo hệ đo. Vài cú va đập mạnh vượt hẳn hàng nghìn cụm mềm, và cách xếp trọng lực biến toàn bộ ngân sách năng lượng của sự kiện thành một đám mây dễ đọc.

⚗️ 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 · ~1.500 cụm năng lượng mô phỏng… detecting

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

Gắn date cho từng bong bóng rồi bật timeline: đám mây bong bóng thành câu chuyện theo từng năm với nút play và thanh tua riêng, mỗi lần hiển thị kích thước của một giai đoạn. Mặc định tắt - không bật thì biểu đồ giữ nguyên.

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

<BubbleChart ref={ref} {...props} timeline={{ speedMs: 1000, loop: true }} />;
// ref.current?.timeline() -> play() / pause() / seek(year) / seekIndex(i) / stepForward()
vue
<BubbleChart :options="{ ...props, timeline: { speedMs: 1000, loop: true } }" />
svelte
<div use:bubbleChart={{ ...props, timeline: { speedMs: 1000, loop: true } }}></div>
ts
applyBubbleChartProps(this.c.nativeElement, { ...props, timeline: { speedMs: 1000, loop: true } });
html
<michi-vz-bubble-chart id="c"></michi-vz-bubble-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.
  • filter (top-N, sắp xếp) vẫn áp dụng bên trong từng giai đoạn, nên mỗi năm chỉ giữ lại top 5 bong bóng.
  • Bong bóng không có date hiển thị ở mọi giai đoạn.

Cách dùng ​

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

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

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

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

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

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

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

Mỗi phần tử dataSet là một bong bóng: một label, một value (diện tích), một partial tùy chọn (phần đã hiện thực hóa), và một color tùy chọn.

ts
const props = {
  splitLabels: ["Realized", "Untapped"],
  showLegend: true,
  gravity: 0.09, // higher = tighter cluster
  dataSet: [
    { label: "Germany", value: 120, partial: 64 }, // 53% realized
    { label: "United States", value: 152, partial: 88 },
    { label: "China", value: 168, partial: 51 },
  ],
};

Mã ngắn cho bong bóng nhỏ. Gán code cho một mục (ví dụ mã ISO) và mã được vẽ bên trong bong bóng khi nhãn đầy đủ không vừa nhưng mã thì vừa; bong bóng quá nhỏ cho cả hai sẽ hiện nhãn bị cắt bằng dấu ba chấm, và bong bóng có bán kính dưới 16px không hiện gì. Mã cũng đi theo trong context và tooltip.

Trọng lực & phần tách ​

gravity quy định các bong bóng bị kéo về trung tâm mạnh đến đâu (càng cao = càng chặt), padding là khoảng cách giữa chúng, và fillRatio là đám mây lấp bao nhiêu phần của khu vực vẽ. Phần tách phản chiếu treemap: partial khắc ra một lõi đã hiện thực hóa chuẩn theo diện tích (bán kính r·√(partial/value)), và phần còn lại hiện lên như một sắc thái nhạt hơn của cùng tông màu - một màu đặc dưới một lớp phủ trắng, để nó hoạt động tốt trên cả nền sáng và nền tối. Đặt tên cho các phần bằng splitLabels.

Kéo và bật về. draggable: true cho phép người xem nhấc một bong bóng lên: nó đi theo con trỏ, lực va chạm đẩy các bong bóng lân cận ra, và khi thả ra trọng lực của cụm kéo mọi thứ về chỗ cũ. Bố cục đã tính không thay đổi, nên lần cập nhật dữ liệu hoặc kích thước tiếp theo sẽ xếp lại cụm từ đầu.

Nhấp chuột. onBubbleClick(bubble, event) được gọi khi nhấp vào một bong bóng, 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; một thao tác kéo không bao giờ được tính là nhấp.

ts
const props = { draggable: true, onBubbleClick: (b) => go(b.code ?? b.label) };

API ​

Các prop được định kiểu là BubbleChartProps 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 độc lập với renderer. Tài liệu tham khảo đầy đủ: Bubble API.