Combo Chart: Orders and Average Order Value
If you lead e-commerce, the monthly review comes down to one question: when orders went up, did the basket get smaller? A table of order counts and average order values holds both answers, but you read them in two columns and work out the relationship by eye, month after month.
A combo chart puts the two on one picture. Orders are bars, average order value is a line, and each has its own axis because one counts in thousands and the other in dollars. A month where the bars climb while the line dips is a promotion pulling in smaller baskets. The chart above is bound to the grid beneath it, so filtering the rows redraws the chart. Filter the Year column to 2025 and only those twelve months are left.
Why a combo chart and not two charts
Two charts stacked on top of each other make you carry a month across from one to the other. A combo chart draws both against the same months, so the dip in the line sits directly over the bar it belongs to.
The chart over a grid
The chart reads the grid’s rows, so there is nothing to keep in step. Each measure says which mark it draws and which axis it belongs to. Here is the example in plain JavaScript, with six inline months 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, month: 'Jan 25', year: 2025, orders: 2250, aov: 64.2 },
{ id: 2, month: 'Feb 25', year: 2025, orders: 2140, aov: 65.0 },
{ id: 3, month: 'Mar 25', year: 2025, orders: 2430, aov: 64.1 },
{ id: 4, month: 'Apr 25', year: 2025, orders: 2590, aov: 63.5 },
{ id: 5, month: 'May 25', year: 2025, orders: 2760, aov: 62.2 },
{ id: 6, month: 'Jun 25', year: 2025, orders: 2710, aov: 63.8 },
];
const gridEl = document.createElement('div');
const chartEl = document.createElement('div');
gridEl.style.height = '260px';
chartEl.style.height = '320px';
document.body.append(gridEl, chartEl);
const grid = createGrid(gridEl, {
rowKey: 'id',
rows,
columns: [
{ field: 'month', title: 'Month' },
{ field: 'orders', title: 'Orders', type: 'number' },
{ field: 'aov', title: 'Average order value', type: 'number' },
],
});
createChart({ grid, container: chartEl, type: 'combo', x: 'month', axis: { x: { scale: 'band' } },
measures: [
{ col: 'orders', fn: 'sum', type: 'bar', axis: 'left', title: 'Orders' },
{ col: 'aov', fn: 'avg', type: 'line', axis: 'right', title: 'Average order value ($)' },
] });
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, month: 'Jan 25', orders: 2250, aov: 64.2 },
{ id: 2, month: 'Feb 25', orders: 2140, aov: 65.0 },
{ id: 3, month: 'Mar 25', orders: 2430, aov: 64.1 },
{ id: 4, month: 'Apr 25', orders: 2590, aov: 63.5 },
];
const columns = [
{ field: 'month', title: 'Month' },
{ field: 'orders', title: 'Orders', type: 'number' },
{ field: 'aov', title: 'Average order value', type: 'number' },
];
export function OrdersAndBasket() {
const gridRef = useRef(null);
const chartRef = useRef(null);
useEffect(() => {
const chart = createChart({ grid: gridRef.current.grid, container: chartRef.current,
type: 'combo', x: 'month', axis: { x: { scale: 'band' } },
measures: [
{ col: 'orders', fn: 'sum', type: 'bar', axis: 'left', title: 'Orders' },
{ col: 'aov', fn: 'avg', type: 'line', axis: 'right', title: 'Average order value ($)' },
] });
return () => chart.destroy();
}, []);
return (<>
<LatticeGrid ref={gridRef} rowKey="id" columns={columns} rows={rows} style={{ height: 220 }} />
<div ref={chartRef} style={{ height: 280 }} />
</>);
}
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 | 'combo' here. |
x |
string | The column naming each category (the month). |
axis.x.scale |
'band', 'linear' or 'time' |
Pins the x scale. 'band' draws each month as a category, so a month label is not read as a date. |
measures |
object[] | Several measures at once, each an object of col, fn, type and axis. |
fn |
string | How a measure is reduced: sum, avg, min, max, count, countValues, first or last. Defaults to sum. |
type (on a measure) |
'bar', 'line' or 'area' |
The mark that measure draws on a combo. |
axis (on a measure) |
'left' or 'right' |
Which axis the measure reads against. |
title (on a measure) |
string | The series label the legend and axis titles use. |
title |
string | Drawn above the plot. |
Orders add up, a basket value does not
Each month here is one row, so sum and avg give the same number it holds. If your grid has one row per order, or one per day, keep sum on the order count and avg on the value: the chart then rolls the rows up by month itself, and filtering to one sales channel redraws both marks from the rows that are left. The charts reference also describes a cumulative flag on a measure, which turns a bar into a running total.
Next
See every chart 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.