Lattice Grid

blog

Bubble Chart for Campaign Reach and Cost

Building…
Loading a live grid…

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.

Read next

  • Pie Chart of Revenue Share by Channel

    Bind a pie chart to a grid of revenue by channel: show each channel as a share of the quarter, and filter the rows to redraw the slices.

  • Heatmap of Sign-ups by Hour and Weekday

    Bind a heatmap to a grid of sign-ups: shade every weekday and hour by volume to time a campaign send, and filter the rows to redraw it.

  • Violin Chart of Salary Bands by Level

    Bind a violin chart to a grid of salaries: see the shape of pay at each level, including two bands inside one, and filter the rows to redraw it.

All posts RSS