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.
No split needed? Drop partial for a clean proportional cloud, one colour per category:
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.
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.
const ref = useRef<BubbleChartHandle>(null);
<BubbleChart ref={ref} {...props} timeline={{ speedMs: 1000, loop: true }} />;
// ref.current?.timeline() -> play() / pause() / seek(year) / stepForward()<BubbleChart :options="{ ...props, timeline: { speedMs: 1000, loop: true } }" /><div use:bubbleChart={{ ...props, timeline: { speedMs: 1000, loop: true } }}></div>applyBubbleChartProps(this.c.nativeElement, { ...props, timeline: { speedMs: 1000, loop: true } });<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>speedMssets the pace,loopwraps around,autoplay: truestarts on mount,showControl: falsehides the built-in bar.- Values glide between periods by default (
interpolate); tune the motion withtweenMsandeasing, or setinterpolate: falsefor hard cuts. Reduced motion always gets the hard cut. - The headless controller is always available:
chart.timeline()exposesplay() / pause() / toggle() / seek(period) / stepForward() / stepBack(), plusonStepandformatPeriodin 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
datestay visible in every period.
Usage
import { BubbleChart } from "@michi-vz/react";
export default () => <BubbleChart {...props} />; // props = the chart options<script setup>
import { BubbleChart } from "@michi-vz/vue";
</script>
<template>
<BubbleChart :options="props" />
</template><script>
import { bubbleChart } from "@michi-vz/svelte";
</script>
<div use:bubbleChart={props}></div>// 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);<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>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.
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.
