Charts & Statschartsvgzero-dependencybubblescatterportfoliothree-variables
Bubble Chart
Three-variable scatter where bubble area encodes a third measure such as revenue.
Live Preview
Growth vs margin, bubble = revenue
- Core
- Mature
- Emerging
View data
| Item | Group | YoY growth (%) | Gross margin (%) | Revenue ($M) |
|---|---|---|---|---|
| Analytics | Core | 34 | 62 | 120 |
| Payments | Core | 18 | 41 | 260 |
| CRM | Mature | 9 | 55 | 180 |
| Hosting | Mature | 22 | 28 | 90 |
| AI tools | Emerging | 71 | 48 | 70 |
| Security | Emerging | 27 | 72 | 140 |
| Mature | 6 | 66 | 60 | |
| Design | Core | 41 | 58 | 85 |
Usage
Copy and paste this code into your React component:
tsx
import { BubbleChart } from 'react-component-kit-ui/charts';
<BubbleChart groups={['Core', 'Emerging']} items={[
{ label: 'Analytics', x: 34, y: 62, z: 120, group: 0 },
{ label: 'AI tools', x: 71, y: 48, z: 70, group: 1 },
]} />Manual Installation
If you are not using the npm package, create a new file and paste the following code: src/components/BubbleChart.tsx + BubbleChart.css:
BubbleChart.tsx
'use client';
import ChartFrame from './ChartFrame';
import { scaleLinear, niceTicks, colorAt, formatCompact } from './chart-utils';
/**
* BubbleChart โ Three-variable scatter: x, y and bubble area (z). Larger
* bubbles are drawn first so small ones stay visible on top.
*
* @param {Array<{label: string, x: number, y: number, z: number, group?: number}>} items - Bubbles.
* @param {string[]} groups - Group names (max 3) mapped to color slots.
* @param {string} xLabel - X-axis title.
* @param {string} yLabel - Y-axis title.
* @param {string} zLabel - Bubble size meaning.
* @param {string[]} colors - Optional per-group colors.
* @param {string} variant - Card style (glass|solid|ghost).
* @param {string} size - Density (sm|md|lg).
*/
export interface BubbleChartProps {
title?: string;
subtitle?: string;
items?: Array<{ label: string; x: number; y: number; z: number; group?: number }>;
groups?: string[];
xLabel?: string;
yLabel?: string;
zLabel?: string;
height?: number;
colors?: string[];
variant?: 'glass' | 'solid' | 'ghost';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
export function BubbleChart({
title = 'Market opportunity',
subtitle = 'Growth vs margin, bubble = revenue',
items = [
{ label: 'Analytics', x: 34, y: 62, z: 120, group: 0 },
{ label: 'Payments', x: 18, y: 41, z: 260, group: 0 },
{ label: 'CRM', x: 9, y: 55, z: 180, group: 1 },
{ label: 'Hosting', x: 22, y: 28, z: 90, group: 1 },
{ label: 'AI tools', x: 71, y: 48, z: 70, group: 2 },
{ label: 'Security', x: 27, y: 72, z: 140, group: 2 },
{ label: 'Email', x: 6, y: 66, z: 60, group: 1 },
{ label: 'Design', x: 41, y: 58, z: 85, group: 0 },
],
groups = ['Core', 'Mature', 'Emerging'],
xLabel = 'YoY growth (%)',
yLabel = 'Gross margin (%)',
zLabel = 'Revenue ($M)',
height = 320,
colors,
variant = 'glass',
size = 'md',
className = '',
}: BubbleChartProps) {
const W = 640;
const H = height;
const m = { t: 20, r: 24, b: 40, l: 48 };
const xt = niceTicks(0, Math.max(...items.map((i) => i.x)), 5);
const yt = niceTicks(0, Math.max(...items.map((i) => i.y)), 4);
const x = scaleLinear(xt[0], xt[xt.length - 1], m.l, W - m.r);
const y = scaleLinear(yt[0], yt[yt.length - 1], H - m.b, m.t);
const zMax = Math.max(...items.map((i) => i.z)) || 1;
const r = (z: number) => 6 + Math.sqrt(z / zMax) * 30; // area-proportional radius
const sorted = [...items].sort((a, b) => b.z - a.z);
return (
<>
<ChartFrame title={title} subtitle={subtitle} variant={variant} size={size} className={className} legend={groups.slice(0, 3).map((g, i) => ({ label: g, color: colorAt(i, colors) }))} table={{ headers: ['Item', 'Group', xLabel, yLabel, zLabel], rows: items.map((i) => [i.label, groups[i.group ?? 0] ?? 'โ', i.x, i.y, i.z]) }}>
<svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: bubble chart of ${items.length} items`}>
{yt.map((t, __i: number) => (
<g key={__i}>
<line className="cf-grid" x1={m.l} x2={W - m.r} y1={y(t)} y2={y(t)} />
<text className="cf-label" x={m.l - 8} y={y(t) + 4} textAnchor="end">{formatCompact(t)}</text>
</g>
))}
{xt.map((t, __i: number) => <text key={__i} className="cf-label" x={x(t)} y={H - m.b + 16} textAnchor="middle">{formatCompact(t)}</text>)}
<text className="cf-label" x={(m.l + W - m.r) / 2} y={H - 4} textAnchor="middle">{xLabel}</text>
<text className="cf-label" transform={`translate(12 ${(m.t + H - m.b) / 2}) rotate(-90)`} textAnchor="middle">{yLabel}</text>
{sorted.map((it) => {
const col = colorAt(it.group ?? 0, colors);
const rad = r(it.z);
return (
<g className="cf-mark cf-dim" tabIndex={0} role="img" aria-label={`${it.label}: ${xLabel} ${it.x}, ${yLabel} ${it.y}, ${zLabel} ${it.z}`} data-tip-title={it.label} data-tip={`${col}::${groups[it.group ?? 0] ?? ''}|${xLabel}: ${it.x}|${yLabel}: ${it.y}|${zLabel}: ${it.z}`}>
<circle cx={x(it.x)} cy={y(it.y)} r={rad} fill={col} fillOpacity="0.55" stroke={col} strokeWidth="1.5" />
{rad > 18 && <text x={x(it.x)} y={y(it.y) + 4} textAnchor="middle" fill="#fff" fontSize="11" fontWeight="600">{it.label}</text>}
</g>
);
})}
</svg>
</ChartFrame>
</>
);
}
export default BubbleChart;Quick Info
- Category
- Charts & Stats
- Filename
BubbleChart.tsx- Dependencies
- None โ pure React + CSS
- Tags
- chartsvgzero-dependencybubblescatterportfoliothree-variables