Bullet Chart for KPI Targets by Region
If you direct sales, the monthly review asks one thing of every region: did it make its number, and if not, how far short did it fall? A table of revenue and target gives you both figures, but you still do the subtraction in your head, one region at a time, and “close” and “badly off” look the same in a column of digits.
A bullet chart answers it in one glance per row. Each region is a bar of revenue, a marker across it for the target, and shaded poor, fair and good bands behind. A bar that stops inside the poor band is a region to talk about; a bar that reaches the marker is not. The chart above is bound to the grid beneath it, so filtering the rows redraws the chart. Filter the Territory column to EMEA and only those regions are left.
Why a bullet chart and not a gauge
A gauge gives one KPI a whole dial, so twenty-four regions would be twenty-four dials you cannot compare. A bullet chart is a gauge laid flat: every region shares one scale, so the bars compare against each other as well as against their own targets.
The chart over a grid
The chart reads the grid’s rows, so there is nothing to keep in step. The bands are columns too, so each region can have its own. Here is the example in plain JavaScript, with six inline regions 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, region: 'North East', territory: 'Americas', actual: 620, target: 700, poor: 420, fair: 595, good: 840 },
{ id: 2, region: 'Midwest', territory: 'Americas', actual: 710, target: 680, poor: 408, fair: 578, good: 816 },
{ id: 3, region: 'DACH', territory: 'EMEA', actual: 820, target: 780, poor: 468, fair: 663, good: 936 },
{ id: 4, region: 'Iberia', territory: 'EMEA', actual: 290, target: 360, poor: 216, fair: 306, good: 432 },
{ id: 5, region: 'Japan', territory: 'APAC', actual: 640, target: 660, poor: 396, fair: 561, good: 792 },
{ id: 6, region: 'Greater China', territory: 'APAC', actual: 560, target: 700, poor: 420, fair: 595, good: 840 },
];
const gridEl = document.createElement('div');
const chartEl = document.createElement('div');
gridEl.style.height = '260px';
chartEl.style.height = '300px';
document.body.append(gridEl, chartEl);
const grid = createGrid(gridEl, {
rowKey: 'id',
rows,
columns: [
{ field: 'region', title: 'Region' },
{ field: 'territory', title: 'Territory', filter: { type: 'set' } },
{ field: 'actual', title: 'Revenue', type: 'number' },
{ field: 'target', title: 'Target', type: 'number' },
],
});
createChart({ grid, container: chartEl, type: 'bullet', x: 'region', y: 'actual', target: 'target',
ranges: ['poor', 'fair', 'good'], min: 0, title: 'Revenue against target by region ($k)' });
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, region: 'North East', territory: 'Americas', actual: 620, target: 700 },
{ id: 2, region: 'DACH', territory: 'EMEA', actual: 820, target: 780 },
{ id: 3, region: 'Iberia', territory: 'EMEA', actual: 290, target: 360 },
{ id: 4, region: 'Japan', territory: 'APAC', actual: 640, target: 660 },
];
const columns = [
{ field: 'region', title: 'Region' },
{ field: 'territory', title: 'Territory', filter: { type: 'set' } },
{ field: 'actual', title: 'Revenue', type: 'number' },
{ field: 'target', title: 'Target', type: 'number' },
];
export function RegionTargets() {
const gridRef = useRef(null);
const chartRef = useRef(null);
useEffect(() => {
const chart = createChart({ grid: gridRef.current.grid, container: chartRef.current,
type: 'bullet', x: 'region', y: 'actual', target: 'target', ranges: [400, 600, 900], min: 0 });
return () => chart.destroy();
}, []);
return (<>
<LatticeGrid ref={gridRef} rowKey="id" columns={columns} rows={rows} style={{ height: 240 }} />
<div ref={chartRef} style={{ height: 260 }} />
</>);
}
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 | 'bullet' here. |
x / y |
string | The column naming each row (the region) and the column drawn as the bar (revenue). |
target |
string | On a bullet, the column holding each row’s target, drawn as a marker across the bar. |
ranges |
(number or string)[] | The bands’ upper edges, ascending. A number is the same for every row; a column name is read per row. [400, 600, 900] shades the scale up to 400, 400 to 600 and 600 to 900. |
min / max |
number | Pin the ends of the scale. |
title |
string | Drawn above the plot. |
Keep one scale, or give each row its own
By default every region is read against one scale and one axis, so a long bar means more revenue. If your regions differ so much in size that the small ones vanish, the charts reference describes a bullet option whose scale can be 'row', which gives each row its own scale, as a bullet in a grid cell has, with no shared axis. Its orientation can also be 'vertical'.
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.