A portrait poster of where something comes from and where it goes, in three stacked bands painted in one colour: the producers as large labelled tiles, the leading producer's output fanning out as ribbons to an aligned column of destinations, and the destinations as bubbles on a scale of how much each depends on that producer.
Bubble scale
Places circles sized by one measure along a single value scale set by another, spread sideways so none overlap, with boxed scale marks instead of an axis.
- Use when
- A poster answer to "where does each one sit on this scale, and how big is it": 5 to about 40 named things with a rate or score (the scale) and a magnitude (the area), such as import dependency against share of demand.
- Avoid when
- Two measures that should be read against each other on two axes (use scatter chart); one measure only (use beeswarm chart or strip plot); precise comparison of the sizes (use bar chart); more than about 60 rows (use beeswarm chart).
Preview
import { BubbleScale } from "insightcharts";
const data = [
{ material: "Lithium", imported: 100, kt: 38 },
{ material: "Cobalt", imported: 86, kt: 21 },
{ material: "Graphite", imported: 98, kt: 96 },
{ material: "Nickel", imported: 71, kt: 164 },
{ material: "Manganese", imported: 96, kt: 52 },
{ material: "Copper", imported: 48, kt: 410 },
{ material: "Aluminium", imported: 59, kt: 290 },
{ material: "Rare earths", imported: 100, kt: 9 },
{ material: "Silicon metal", imported: 64, kt: 73 },
{ material: "Zinc", imported: 22, kt: 118 },
{ material: "Steel scrap", imported: 4, kt: 520 },
{ material: "Tin", imported: 78, kt: 14 },
];
export function Example() {
return (
<BubbleScale
data={data}
name="material"
value="imported"
size="kt"
format={"{value}%"}
sizeFormat={"{value} kt"}
domain={[0, 100]}
ticks={[0, 20, 40, 60, 80, 100]}
scaleLabel="Imported"
title="Imports supply between 4% and 100% of the region's demand for 12 materials"
subtitle="Share of each material imported (position) and annual demand in thousand tonnes (area), 12 battery and grid materials"
note="Illustrative figures for an invented region"
/>
);
}{
"type": "bubble-scale",
"data": [
{ "material": "Lithium", "imported": 100, "kt": 38 },
{ "material": "Cobalt", "imported": 86, "kt": 21 },
{ "material": "Graphite", "imported": 98, "kt": 96 },
{ "material": "Nickel", "imported": 71, "kt": 164 },
{ "material": "Manganese", "imported": 96, "kt": 52 },
{ "material": "Copper", "imported": 48, "kt": 410 },
{ "material": "Aluminium", "imported": 59, "kt": 290 },
{ "material": "Rare earths", "imported": 100, "kt": 9 },
{ "material": "Silicon metal", "imported": 64, "kt": 73 },
{ "material": "Zinc", "imported": 22, "kt": 118 },
{ "material": "Steel scrap", "imported": 4, "kt": 520 },
{ "material": "Tin", "imported": 78, "kt": 14 }
],
"name": "material",
"value": "imported",
"size": "kt",
"format": "{value}%",
"sizeFormat": "{value} kt",
"domain": [0, 100],
"ticks": [0, 20, 40, 60, 80, 100],
"scaleLabel": "Imported",
"title": "Imports supply between 4% and 100% of the region's demand for 12 materials",
"subtitle": "Share of each material imported (position) and annual demand in thousand tonnes (area), 12 battery and grid materials",
"note": "Illustrative figures for an invented region"
}Render with import { Insight } from "insightcharts" and <Insight spec={spec} />. Nested specs resolve by name; nothing else to import.
Composition
Used in
1 infographic uses bubble scale.
Infographics
1Installation
$ pnpm dlx shadcn@latest add @insightcharts/bubble-scaleThe CLI copies the source into your project and needs the @insightcharts registry in components.json; see installation. With the package, import it by name:
import { BubbleScale } from "insightcharts";Examples
Colour by series, horizontal
import { BubbleScale } from "insightcharts";
const data = [
{ district: "Harbourside", ring: "Inner", carFree: 71, thousand: 18 },
{ district: "Old Town", ring: "Inner", carFree: 64, thousand: 31 },
{ district: "Canal Quarter", ring: "Inner", carFree: 58, thousand: 22 },
{ district: "Station Fields", ring: "Inner", carFree: 49, thousand: 12 },
{ district: "Marketgate", ring: "Inner", carFree: 44, thousand: 27 },
{ district: "Northmoor", ring: "Outer", carFree: 37, thousand: 46 },
{ district: "Eastcliff", ring: "Outer", carFree: 33, thousand: 39 },
{ district: "Larkhill", ring: "Outer", carFree: 29, thousand: 52 },
{ district: "Westbrook", ring: "Outer", carFree: 26, thousand: 34 },
{ district: "Fenside", ring: "Outer", carFree: 22, thousand: 61 },
{ district: "Quarry Hill", ring: "Outer", carFree: 19, thousand: 28 },
{ district: "Ashby Green", ring: "Suburb", carFree: 14, thousand: 44 },
{ district: "Thornley", ring: "Suburb", carFree: 12, thousand: 57 },
{ district: "Upper Wold", ring: "Suburb", carFree: 9, thousand: 36 },
{ district: "Millbank", ring: "Suburb", carFree: 8, thousand: 49 },
{ district: "St Agnes", ring: "Suburb", carFree: 6, thousand: 23 },
{ district: "Coldharbour", ring: "Suburb", carFree: 5, thousand: 15 },
];
export function Example() {
return (
<BubbleScale
data={data}
name="district"
value="carFree"
size="thousand"
series="ring"
orientation="horizontal"
format={"{value}%"}
sizeFormat={"{value}k"}
scaleLabel="Households without a car"
title="71% of Harbourside households have no car"
subtitle="Share of households without a car (position) and population in thousands (area), 17 districts"
note="Illustrative figures for an invented city"
/>
);
}{
"type": "bubble-scale",
"data": [
{ "district": "Harbourside", "ring": "Inner", "carFree": 71, "thousand": 18 },
{ "district": "Old Town", "ring": "Inner", "carFree": 64, "thousand": 31 },
{ "district": "Canal Quarter", "ring": "Inner", "carFree": 58, "thousand": 22 },
{ "district": "Station Fields", "ring": "Inner", "carFree": 49, "thousand": 12 },
{ "district": "Marketgate", "ring": "Inner", "carFree": 44, "thousand": 27 },
{ "district": "Northmoor", "ring": "Outer", "carFree": 37, "thousand": 46 },
{ "district": "Eastcliff", "ring": "Outer", "carFree": 33, "thousand": 39 },
{ "district": "Larkhill", "ring": "Outer", "carFree": 29, "thousand": 52 },
{ "district": "Westbrook", "ring": "Outer", "carFree": 26, "thousand": 34 },
{ "district": "Fenside", "ring": "Outer", "carFree": 22, "thousand": 61 },
{ "district": "Quarry Hill", "ring": "Outer", "carFree": 19, "thousand": 28 },
{ "district": "Ashby Green", "ring": "Suburb", "carFree": 14, "thousand": 44 },
{ "district": "Thornley", "ring": "Suburb", "carFree": 12, "thousand": 57 },
{ "district": "Upper Wold", "ring": "Suburb", "carFree": 9, "thousand": 36 },
{ "district": "Millbank", "ring": "Suburb", "carFree": 8, "thousand": 49 },
{ "district": "St Agnes", "ring": "Suburb", "carFree": 6, "thousand": 23 },
{ "district": "Coldharbour", "ring": "Suburb", "carFree": 5, "thousand": 15 }
],
"name": "district",
"value": "carFree",
"size": "thousand",
"series": "ring",
"orientation": "horizontal",
"format": "{value}%",
"sizeFormat": "{value}k",
"scaleLabel": "Households without a car",
"title": "71% of Harbourside households have no car",
"subtitle": "Share of households without a car (position) and population in thousands (area), 17 districts",
"note": "Illustrative figures for an invented city"
}Render with import { Insight } from "insightcharts" and <Insight spec={spec} />. Nested specs resolve by name; nothing else to import.
Highlight one circle, click to select
import { BubbleScale } from "insightcharts";
const data = [
{ district: "Harbourside", ring: "Inner", carFree: 71, thousand: 18 },
{ district: "Old Town", ring: "Inner", carFree: 64, thousand: 31 },
{ district: "Canal Quarter", ring: "Inner", carFree: 58, thousand: 22 },
{ district: "Station Fields", ring: "Inner", carFree: 49, thousand: 12 },
{ district: "Marketgate", ring: "Inner", carFree: 44, thousand: 27 },
{ district: "Northmoor", ring: "Outer", carFree: 37, thousand: 46 },
{ district: "Eastcliff", ring: "Outer", carFree: 33, thousand: 39 },
{ district: "Larkhill", ring: "Outer", carFree: 29, thousand: 52 },
{ district: "Westbrook", ring: "Outer", carFree: 26, thousand: 34 },
{ district: "Fenside", ring: "Outer", carFree: 22, thousand: 61 },
{ district: "Quarry Hill", ring: "Outer", carFree: 19, thousand: 28 },
{ district: "Ashby Green", ring: "Suburb", carFree: 14, thousand: 44 },
{ district: "Thornley", ring: "Suburb", carFree: 12, thousand: 57 },
{ district: "Upper Wold", ring: "Suburb", carFree: 9, thousand: 36 },
{ district: "Millbank", ring: "Suburb", carFree: 8, thousand: 49 },
{ district: "St Agnes", ring: "Suburb", carFree: 6, thousand: 23 },
{ district: "Coldharbour", ring: "Suburb", carFree: 5, thousand: 15 },
];
export function Example() {
return (
<BubbleScale
data={data}
name="district"
value="carFree"
size="thousand"
highlight="Fenside"
select
format={"{value}%"}
sizeFormat={"{value}k"}
scaleLabel="Without a car"
title="22% of Fenside's households have no car"
subtitle="Share of households without a car (position) and population in thousands (area), 17 districts"
note="Illustrative figures for an invented city"
/>
);
}{
"type": "bubble-scale",
"data": [
{ "district": "Harbourside", "ring": "Inner", "carFree": 71, "thousand": 18 },
{ "district": "Old Town", "ring": "Inner", "carFree": 64, "thousand": 31 },
{ "district": "Canal Quarter", "ring": "Inner", "carFree": 58, "thousand": 22 },
{ "district": "Station Fields", "ring": "Inner", "carFree": 49, "thousand": 12 },
{ "district": "Marketgate", "ring": "Inner", "carFree": 44, "thousand": 27 },
{ "district": "Northmoor", "ring": "Outer", "carFree": 37, "thousand": 46 },
{ "district": "Eastcliff", "ring": "Outer", "carFree": 33, "thousand": 39 },
{ "district": "Larkhill", "ring": "Outer", "carFree": 29, "thousand": 52 },
{ "district": "Westbrook", "ring": "Outer", "carFree": 26, "thousand": 34 },
{ "district": "Fenside", "ring": "Outer", "carFree": 22, "thousand": 61 },
{ "district": "Quarry Hill", "ring": "Outer", "carFree": 19, "thousand": 28 },
{ "district": "Ashby Green", "ring": "Suburb", "carFree": 14, "thousand": 44 },
{ "district": "Thornley", "ring": "Suburb", "carFree": 12, "thousand": 57 },
{ "district": "Upper Wold", "ring": "Suburb", "carFree": 9, "thousand": 36 },
{ "district": "Millbank", "ring": "Suburb", "carFree": 8, "thousand": 49 },
{ "district": "St Agnes", "ring": "Suburb", "carFree": 6, "thousand": 23 },
{ "district": "Coldharbour", "ring": "Suburb", "carFree": 5, "thousand": 15 }
],
"name": "district",
"value": "carFree",
"size": "thousand",
"highlight": "Fenside",
"select": true,
"format": "{value}%",
"sizeFormat": "{value}k",
"scaleLabel": "Without a car",
"title": "22% of Fenside's households have no car",
"subtitle": "Share of households without a car (position) and population in thousands (area), 17 districts",
"note": "Illustrative figures for an invented city"
}Render with import { Insight } from "insightcharts" and <Insight spec={spec} />. Nested specs resolve by name; nothing else to import.
Stress: 40 circles crowded at one end, sizes from 1 to 900
import { BubbleScale } from "insightcharts";
const data = [
{ fund: "Fund A", fee: 1.15, bn: 900 },
{ fund: "Fund B", fee: 0.22, bn: 413 },
{ fund: "Fund C", fee: 0.11, bn: 226 },
{ fund: "Fund D", fee: 0.28, bn: 139 },
{ fund: "Fund E", fee: 0.17, bn: 87 },
{ fund: "Fund F", fee: 0.66, bn: 48 },
{ fund: "Fund G", fee: 0.23, bn: 33 },
{ fund: "Fund H", fee: 0.12, bn: 24 },
{ fund: "Fund I", fee: 0.29, bn: 18 },
{ fund: "Fund J", fee: 1.28, bn: 16 },
{ fund: "Fund K", fee: 0.07, bn: 2 },
{ fund: "Fund L", fee: 0.24, bn: 4 },
{ fund: "Fund M", fee: 0.13, bn: 906 },
{ fund: "Fund N", fee: 0.3, bn: 419 },
{ fund: "Fund O", fee: 0.19, bn: 232 },
{ fund: "Fund P", fee: 0.09, bn: 130 },
{ fund: "Fund Q", fee: 0.25, bn: 78 },
{ fund: "Fund R", fee: 0.15, bn: 54 },
{ fund: "Fund S", fee: 2.01, bn: 39 },
{ fund: "Fund T", fee: 0.21, bn: 30 },
{ fund: "Fund U", fee: 0.1, bn: 9 },
{ fund: "Fund V", fee: 0.27, bn: 7 },
{ fund: "Fund W", fee: 0.16, bn: 8 },
{ fund: "Fund X", fee: 0.05, bn: 10 },
{ fund: "Fund Y", fee: 0.22, bn: 912 },
{ fund: "Fund Z", fee: 0.11, bn: 410 },
{ fund: "Fund A2", fee: 0.28, bn: 223 },
{ fund: "Fund B2", fee: 1.27, bn: 136 },
{ fund: "Fund C2", fee: 0.06, bn: 84 },
{ fund: "Fund D2", fee: 0.23, bn: 60 },
{ fund: "Fund E2", fee: 0.12, bn: 30 },
{ fund: "Fund F2", fee: 0.89, bn: 21 },
{ fund: "Fund G2", fee: 0.18, bn: 15 },
{ fund: "Fund H2", fee: 0.07, bn: 13 },
{ fund: "Fund I2", fee: 0.24, bn: 14 },
{ fund: "Fund J2", fee: 0.13, bn: 1 },
{ fund: "Fund K2", fee: 1.4, bn: 903 },
{ fund: "Fund L2", fee: 0.19, bn: 416 },
{ fund: "Fund M2", fee: 0.09, bn: 229 },
{ fund: "Fund N2", fee: 0.25, bn: 142 },
];
export function Example() {
return (
<BubbleScale
data={data}
name="fund"
value="fee"
size="bn"
format={"{value}%"}
sizeFormat={"${value}bn"}
scaleLabel="Annual fee"
title="40 funds charge between 0.05% and 2.01% a year"
subtitle="Annual fee (position) and assets in $ billion (area), 40 index and active funds"
note="Illustrative figures for invented funds"
/>
);
}{
"type": "bubble-scale",
"data": [
{ "fund": "Fund A", "fee": 1.15, "bn": 900 },
{ "fund": "Fund B", "fee": 0.22, "bn": 413 },
{ "fund": "Fund C", "fee": 0.11, "bn": 226 },
{ "fund": "Fund D", "fee": 0.28, "bn": 139 },
{ "fund": "Fund E", "fee": 0.17, "bn": 87 },
{ "fund": "Fund F", "fee": 0.66, "bn": 48 },
{ "fund": "Fund G", "fee": 0.23, "bn": 33 },
{ "fund": "Fund H", "fee": 0.12, "bn": 24 },
{ "fund": "Fund I", "fee": 0.29, "bn": 18 },
{ "fund": "Fund J", "fee": 1.28, "bn": 16 },
{ "fund": "Fund K", "fee": 0.07, "bn": 2 },
{ "fund": "Fund L", "fee": 0.24, "bn": 4 },
{ "fund": "Fund M", "fee": 0.13, "bn": 906 },
{ "fund": "Fund N", "fee": 0.3, "bn": 419 },
{ "fund": "Fund O", "fee": 0.19, "bn": 232 },
{ "fund": "Fund P", "fee": 0.09, "bn": 130 },
{ "fund": "Fund Q", "fee": 0.25, "bn": 78 },
{ "fund": "Fund R", "fee": 0.15, "bn": 54 },
{ "fund": "Fund S", "fee": 2.01, "bn": 39 },
{ "fund": "Fund T", "fee": 0.21, "bn": 30 },
{ "fund": "Fund U", "fee": 0.1, "bn": 9 },
{ "fund": "Fund V", "fee": 0.27, "bn": 7 },
{ "fund": "Fund W", "fee": 0.16, "bn": 8 },
{ "fund": "Fund X", "fee": 0.05, "bn": 10 },
{ "fund": "Fund Y", "fee": 0.22, "bn": 912 },
{ "fund": "Fund Z", "fee": 0.11, "bn": 410 },
{ "fund": "Fund A2", "fee": 0.28, "bn": 223 },
{ "fund": "Fund B2", "fee": 1.27, "bn": 136 },
{ "fund": "Fund C2", "fee": 0.06, "bn": 84 },
{ "fund": "Fund D2", "fee": 0.23, "bn": 60 },
{ "fund": "Fund E2", "fee": 0.12, "bn": 30 },
{ "fund": "Fund F2", "fee": 0.89, "bn": 21 },
{ "fund": "Fund G2", "fee": 0.18, "bn": 15 },
{ "fund": "Fund H2", "fee": 0.07, "bn": 13 },
{ "fund": "Fund I2", "fee": 0.24, "bn": 14 },
{ "fund": "Fund J2", "fee": 0.13, "bn": 1 },
{ "fund": "Fund K2", "fee": 1.4, "bn": 903 },
{ "fund": "Fund L2", "fee": 0.19, "bn": 416 },
{ "fund": "Fund M2", "fee": 0.09, "bn": 229 },
{ "fund": "Fund N2", "fee": 0.25, "bn": 142 }
],
"name": "fund",
"value": "fee",
"size": "bn",
"format": "{value}%",
"sizeFormat": "${value}bn",
"scaleLabel": "Annual fee",
"title": "40 funds charge between 0.05% and 2.01% a year",
"subtitle": "Annual fee (position) and assets in $ billion (area), 40 index and active funds",
"note": "Illustrative figures for invented funds"
}Render with import { Insight } from "insightcharts" and <Insight spec={spec} />. Nested specs resolve by name; nothing else to import.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| namerequired | string | Name field: one circle per row. | |
| valuerequired | string | Numeric field that positions each circle along the scale. Printed with format. | |
| sizerequired | string | Numeric field for circle area. Printed inside or beside each circle with sizeFormat. | |
| series | string | Field that colours circles by category. Default: circles are shaded along the scale with the sequential ramp. | |
| orientation | "vertical" | "horizontal" | "vertical" | Direction of the scale. Narrow containers always use vertical. |
| domain | [number, number] | Scale range. Default: the data extent rounded out to the tick step. | |
| ticks | number[] | Values to mark on the scale. Default: about five round values. | |
| scaleLabel | string | Short name of the scale measure, printed at the head of the scale. | |
| sizeFormat | FormatSpec | How to print the size measure. | |
| guides | boolean | true | Thin dotted guide line at each scale mark. |
| names | Record<string, string> | Display names for series keys. |
Common chart props 29
Every chart built with defineChart accepts these on top of its own props.
| Prop | Type | Description |
|---|---|---|
| data | DataInput | Rows, CSV text, a URL, { url, path }, an async function, or an Arrow-style table. Omit inside <InsightData>. |
| transform | Transform | Transform[] | A pipeline of transforms applied before drawing. |
| title | string | The title. One plain statement of what the data shows, with the number and the period ("Monthly churn fell from 4.2% to 2.1% in 2024"), or a plain label ("Monthly churn, 2024"). One clause, no figures of speech. |
| subtitle | string | Measure, unit, population, period. |
| source | string | Data source, shown as "Source: ...". |
| note | string | Method notes or caveats. |
| height | number | string | CSS height of the plot. Default 360. |
| aspect | number | Width / height. Overrides height. |
| highlight | string | string[] | The entity (or entities) the title is about. Every other series is drawn in grey. |
| annotations | Annotation[] | Reference lines, shaded bands and callouts, in data coordinates. |
| format | FormatSpec | How to print values: "compact", "percent", "currency:USD", "{value} kg", Intl options, or a function. |
| legend | boolean | Show the legend. Default: shown when there are two or more series. |
| select | boolean | string | Make marks clickable to filter. Inside <InsightData> the selection filters every other chart. Pass a field name, or true to use the chart's category field. |
| onSelect | (values: string[], field: string) => void | Called with the selected values whenever the selection changes. |
| option | EChartsOption | ((option: EChartsOption, ctx: BuildContext) => EChartsOption) | Deep-merged over the generated ECharts option, or a function that receives and returns it. Use it for ECharts settings no prop covers. |
| theme | ThemeInput | A theme, or a partial set of tokens merged over the surrounding theme, for this chart only. |
| mode | ModeInput | "auto" (default) follows the page, "system" follows the OS, or force "light" / "dark". |
| renderer | RendererInput | "auto" (default) uses SVG for light charts and canvas for heavy ones. |
| animate | boolean | Set false to disable animation. Reduced-motion preferences are always respected. |
| lazy | boolean | Mount the chart only when it scrolls near the viewport. Default true. |
| group | string | Link tooltips with other charts sharing this group id. |
| exportable | boolean | ExportFormat[] | Show an export menu: true or a list of formats from png, jpeg, svg, pdf, csv, json. |
| table | boolean | Show a collapsible "View data" table under the chart. |
| alt | string | Accessible description: the chart type and measures, then the main pattern and the notable values. |
| id | string | Stable id for this chart's selection in the shared store. |
| onEvents | Record<string, ChartEventHandler> | ECharts event handlers, e.g. { click: (params, chart) => {} }. Prefix a name with zr: to bind to the canvas itself. |
| onReady | (chart: EChartsInstance) => void | Called with the ECharts instance once it is created. |
| className | string | Class name for the outer <figure>. |
| style | CSSProperties | Inline style for the outer <figure>. |