Lattice Grid

blog

Population Pyramid for Customer Age by Plan

Building…
Loading a live grid…

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.

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