Population Pyramid for Customer Age by Plan
You are a product manager, and you want to know which plan skews young. Two plans, six age bands, men and women counted separately: the numbers are all in a table, but the question is about shape. Does the Starter plan weigh toward the young bands while the Team plan bulges in the middle? A population pyramid answers that at a glance. It draws each age band as a pair of bars back to back, men to the left and women to the right, so the outline of the whole customer base is the answer.
The example below starts on the Starter plan. The chart is bound to the grid, so it draws exactly the rows the grid is showing. Change the box to Team, or open the filter on the Plan column, and the pyramid redraws as the other plan’s profile on the next frame. Clear the box and both plans are counted together.
Why a population pyramid and not a grouped bar
A grouped bar puts two bars side by side, one baseline, and the eye has to hop across to compare them. A pyramid mirrors the two halves about a central axis on one shared scale, so equal bars are equal lengths and a lopsided age band shows as a lopsided pair. Stack the age bands up the side and the silhouette tells you the age profile before you have read a number.
The chart is a view of the grid
There is no second data set to keep in step. createChart takes the grid itself, reads its filtered rows, and redraws whenever they change. x names the age band, y the number of customers, and series is the column that holds the two halves. The first value it meets, here Men, is drawn on the left. Sort, filter or edit the grid and the chart follows, because a chart of rows you cannot see would be describing a different customer base.
The code, in plain JavaScript
import { createGrid } from '@toclocoinc/lattice-grid';
import { createChart } from '@toclocoinc/lattice-grid/modules/charts';
import '@toclocoinc/lattice-grid/css';
const bands = ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'];
const counts = {
Starter: { Men: [310, 420, 260, 150, 80, 40], Women: [290, 450, 240, 130, 70, 30] },
Team: { Men: [60, 180, 330, 380, 290, 160], Women: [50, 170, 310, 360, 280, 170] },
};
const rows = [];
for (const plan of Object.keys(counts))
for (const gender of ['Men', 'Women'])
bands.forEach((age, i) =>
rows.push({ id: rows.length + 1, plan, age, gender, customers: counts[plan][gender][i] }));
const grid = createGrid(document.querySelector('#grid'), {
rowKey: 'id',
columns: [
{ field: 'plan', title: 'Plan', filter: { type: 'set' } },
{ field: 'age', title: 'Age band' },
{ field: 'gender', title: 'Gender', filter: { type: 'set' } },
{ field: 'customers', title: 'Customers', type: 'number' },
],
rows,
});
grid.filters.quick('Starter');
createChart({
grid, container: '#chart', type: 'populationPyramid',
x: 'age', y: 'customers', series: 'gender',
title: 'Customers by age band, Starter plan',
});
Give #chart and #grid a height in your page and that is the whole screen.
The same thing in React
The React adapter is a factory: you pass it React and createGrid, and it returns the component. The component forwards a ref whose .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 { createChart } from '@toclocoinc/lattice-grid/modules/charts';
import { createLatticeGrid } from '@toclocoinc/lattice-grid/modules/react';
import '@toclocoinc/lattice-grid/css';
const LatticeGrid = createLatticeGrid({ React, createGrid });
const columns = [
{ field: 'plan', title: 'Plan', filter: { type: 'set' } },
{ field: 'age', title: 'Age band' },
{ field: 'gender', title: 'Gender', filter: { type: 'set' } },
{ field: 'customers', title: 'Customers', type: 'number' },
];
export function AgeByPlan({ rows }) {
const gridRef = useRef(null);
const chartEl = useRef(null);
useEffect(() => {
const chart = createChart({
grid: gridRef.current.grid, container: chartEl.current,
type: 'populationPyramid', x: 'age', y: 'customers', series: 'gender',
title: 'Customers by age band',
});
return () => chart.destroy();
}, []);
return (
<>
<div ref={chartEl} style={{ height: 300 }} />
<LatticeGrid ref={gridRef} rowKey="id" columns={columns} rows={rows} style={{ height: 300 }} />
</>
);
}
Pass it the same twenty-four rows as the first listing, with plan, age, gender and customers on each.
The options used
| Option | Type | What it does |
|---|---|---|
grid |
Grid | Required. The grid to read. |
container |
Element or string | Required. Where to draw. |
type |
string | 'populationPyramid' here. |
x / y |
string | The age band column, and the measure to total per band. |
series |
string | Splits the measure into one series per distinct value. A pyramid takes two, and the first is drawn on the left. |
title |
string | Drawn above the plot. |
A pyramid also takes pyramid: { scale: 'independent' }, which scales each half to its own largest bar, and pyramid: { percent: true }, which draws each bar as a share of the whole.
Go further
The charts page lists every chart type the same grid can feed, and the bar, line and pie family demo draws the pyramid beside the rest of the Cartesian types. To see how a chart keeps following the grid when you filter by clicking, read how to draw a chart that follows the grid.