Skip to content

Bubble Chart

Composition

"How big is each one, and how much of it is already realized?" A bubble cloud answers magnitude at a glance: every circle is sized by value (area, not radius), and a gravity simulation pulls them into a tidy cluster so the big ones obviously dominate. Like the treemap, each bubble can carry a two-part split - a solid realized core inside a lighter untapped ring - so you read size and progress together.

Example
canvas · responsive
Go deeper: Insights guide·DevTools guide

No split needed? Drop partial for a clean proportional cloud, one colour per category:

Example
canvas · responsive
Go deeper: Insights guide·DevTools guide

The cluster is laid out with d3-force: bubbles fall toward the centre (gravity) and push apart so they never overlap (collision). The simulation is settled synchronously, so SVG and canvas render the identical, reproducible layout.

When to reach for it

  • Magnitude at a glance. Products, markets, keywords as a sized cloud - when "which ones are big?" matters more than an exact ranking, the cluster answers instantly.
  • Opportunity maps. With the split, a big bubble with a thin realized core is money on the table - the scan-for-upside view for portfolio reviews.
  • If position should mean something (two numeric axes, correlation), that is a Scatter plot; the bubble cloud trades position for compactness.

Heavy data on WebGPU Experimental

BubbleChart has an opt-in renderer="webgpu" that paints the bubble cloud as GPU-instanced circles while labels and tooltips stay on the SVG layer. It is capability-gated: on a browser without WebGPU it downgrades to canvas automatically, and getContext().renderer reports whichever actually painted.

Like the scatter page, the demo below borrows from particle physics: ~1,500 reconstructed energy clusters from one simulated collision event, one bubble per cluster, coloured by subdetector. The handful of hard deposits tower over thousands of soft ones, and gravity packing turns the whole event's energy budget into a single readable cloud.

⚗️ 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 simulated energy clusters… detecting

Play through the years

Give every bubble a date and flip on timeline: the cloud becomes a year-by-year story with its own play button and scrubber, snapshotting one period's sizes at a time. Off by default - nothing changes until a chart opts in.

Press the play button under the chart: it steps through the years, one snapshot at a time. Drag the scrubber to jump to any year.

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

<BubbleChart ref={ref} {...props} timeline={{ speedMs: 1000, loop: true }} />;
// ref.current?.timeline() -> play() / pause() / seek(year) / 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)
</script>
  • speedMs sets the pace, loop wraps around, autoplay: true starts on mount, showControl: false hides the built-in bar.
  • Values glide between periods by default (interpolate); tune the motion with tweenMs and easing, or set interpolate: false for hard cuts. Reduced motion always gets the hard cut.
  • The headless controller is always available: chart.timeline() exposes play() / pause() / toggle() / seek(period) / stepForward() / stepBack(), plus onStep and formatPeriod in the config for custom UI.
  • A filter (top-N, sorting) still applies inside each period, so only the top 5 bubbles per year make the cut.
  • Bubbles without a date stay visible in every period.

Usage

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

Data shape

Each dataSet item is one bubble: a label, a value (area), an optional partial (the realized sub-portion), and an optional color.

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 },
  ],
};

Gravity & the split

gravity sets how strongly bubbles are pulled toward the centre (higher = tighter), padding the gap between them, and fillRatio how much of the plot the cloud fills. The split mirrors the treemap: partial carves an area-true realized core (radius r·√(partial/value)), and the rest reads as a lighter tint of the same hue - a solid colour under a white veil, so it works on light and dark backgrounds. Name the parts with splitLabels.

API

Props are typed as BubbleChartProps in @michi-vz/core. Shared across all charts: width, height, margin, colors / colorsMapping, renderer ("svg", "canvas", or experimental "webgpu"), highlightItems, disabledItems, and the on* callbacks. onChartDataProcessed / getContext() return the renderer-agnostic ChartContext. Full reference: Bubble API.