Bubble Chart for Campaign Reach and Cost
If you run marketing, the quarterly review always has the same question in it: which campaign cost a lot and gave us little? A table of campaigns holds every figure you need, but reach, cost and leads are three columns, and you can only compare two of them by eye at a time.
A bubble chart puts all three on one plot. Each campaign is placed by its reach across and its cost up, and the size of the bubble is the leads it produced. The expensive campaign that produced little shows up as a high, small bubble. The chart above is bound to the grid beneath it, so filtering the rows redraws the bubbles. Filter the Channel column to Display and only the display campaigns are left on the plot.
Why a bubble chart and not a scatter plot
A scatter plot places each campaign by two measures and stops there, so leads would need a second chart or a column you read separately. A bubble chart is a scatter plot with a third measure drawn as size, which is what lets you judge cost against return without leaving the plot. Use a scatter for two measures, and a bubble when a third decides whether a point is a problem.
The chart over a grid
The chart reads the grid’s rows, so there is nothing to keep in step. Here is the whole example in plain JavaScript, with eight inline campaigns standing in for your own rows.
import { createGrid } from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import { createChart } from '@toclocoinc/lattice-grid/modules/charts';
const rows = [
{ id: 1, campaign: 'Spring Newsletter', channel: 'Email', reach: 41000, cost: 3200, leads: 410 },
{ id: 2, campaign: 'Brand Terms', channel: 'Search', reach: 24800, cost: 5200, leads: 640 },
{ id: 3, campaign: 'Homepage Takeover', channel: 'Display', reach: 245000, cost: 38000, leads: 130 },
{ id: 4, campaign: 'Short Video Series', channel: 'Social', reach: 148000, cost: 9800, leads: 340 },
{ id: 5, campaign: 'Trade Show Booth', channel: 'Events', reach: 9800, cost: 26000, leads: 310 },
{ id: 6, campaign: 'Hosted Webinar', channel: 'Events', reach: 21700, cost: 4800, leads: 480 },
{ id: 7, campaign: 'Template Library', channel: 'Content', reach: 72500, cost: 2900, leads: 780 },
{ id: 8, campaign: 'Influencer Week', channel: 'Social', reach: 205000, cost: 31500, leads: 155 },
];
const gridEl = document.createElement('div');
const chartEl = document.createElement('div');
gridEl.style.height = chartEl.style.height = '300px';
document.body.append(gridEl, chartEl);
const grid = createGrid(gridEl, {
rowKey: 'id',
rows,
columns: [
{ field: 'campaign', title: 'Campaign' },
{ field: 'channel', title: 'Channel', filter: { type: 'set' } },
{ field: 'reach', title: 'Reach', type: 'number' },
{ field: 'cost', title: 'Cost', type: 'number' },
{ field: 'leads', title: 'Leads', type: 'number' },
],
});
createChart({ grid, container: chartEl, type: 'bubble', x: 'reach', y: 'cost', size: 'leads',
maxRadius: 22, title: 'Campaigns by reach and cost, sized by leads' });
The same thing in React
The React adapter is a factory, so you build the component once at module scope. The ref’s grid is the real grid, which is what the chart binds to.
import React, { useEffect, useRef } from 'react';
import { createGrid } from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import { createLatticeGrid } from '@toclocoinc/lattice-grid/modules/react';
import { createChart } from '@toclocoinc/lattice-grid/modules/charts';
const LatticeGrid = createLatticeGrid({ React, createGrid });
const rows = [
{ id: 1, campaign: 'Spring Newsletter', channel: 'Email', reach: 41000, cost: 3200, leads: 410 },
{ id: 2, campaign: 'Brand Terms', channel: 'Search', reach: 24800, cost: 5200, leads: 640 },
{ id: 3, campaign: 'Homepage Takeover', channel: 'Display', reach: 245000, cost: 38000, leads: 130 },
{ id: 4, campaign: 'Short Video Series', channel: 'Social', reach: 148000, cost: 9800, leads: 340 },
];
const columns = [
{ field: 'campaign', title: 'Campaign' },
{ field: 'channel', title: 'Channel', filter: { type: 'set' } },
{ field: 'reach', title: 'Reach', type: 'number' },
{ field: 'cost', title: 'Cost', type: 'number' },
{ field: 'leads', title: 'Leads', type: 'number' },
];
export function CampaignBubbles() {
const gridRef = useRef(null);
const chartRef = useRef(null);
useEffect(() => {
const chart = createChart({ grid: gridRef.current.grid, container: chartRef.current,
type: 'bubble', x: 'reach', y: 'cost', size: 'leads', maxRadius: 22 });
return () => chart.destroy();
}, []);
return (<>
<LatticeGrid ref={gridRef} rowKey="id" columns={columns} rows={rows} style={{ height: 300 }} />
<div ref={chartRef} style={{ height: 300 }} />
</>);
}
The options used
These are the rows of the charts reference for the options in the listings above.
| Option | Type | What it does |
|---|---|---|
grid |
Grid | Required. The grid to read. |
container |
Element or string | Required. Where to draw. |
type |
string | 'bubble' here. |
x / y |
string | The columns that place each bubble: reach across, cost up. |
size / maxRadius |
string / number | Bubble charts: the column driving the radius (leads here), and the largest it may be drawn. The largest is also capped by the plot’s own size, so a bubble never outgrows the plot. |
title |
string | Drawn above the plot. |
Give every campaign its own reach
A bubble chart binds one mark per row where every row’s x is distinct. Rows that tie on reach share one point, reduced by the measure’s aggregation, so two campaigns with the same reach would draw as one bubble. If your reach figures can tie, the charts reference describes a perRow option that keeps a mark for every row.
Next
See every Cartesian type on one dataset in the Cartesian chart demo, read the full list of chart types on the charts page, or follow the how-to for a chart that follows the grid.