Build a rolling 12-month KPI dashboard
Auto-shift the time window so the Northwind dashboard always shows the last 12 months — never a stale snapshot.
Published Aug 6, 2025
Northwind’s Dashboard tab is meant to show the last twelve months of
revenue at a glance. The trouble with a hand-built dashboard is the window:
the month columns are fixed, so as the year rolls on the chart drifts out of
date until someone remembers to shuffle everything along. A dashboard that
needs maintenance is a dashboard people stop trusting.
This script removes the maintenance. Every night it works out the trailing twelve months from today, totals invoice revenue into each one, and writes the labels and figures into named ranges the dashboard charts read from. The window slides itself, so the dashboard is always current without anyone touching it.
What you’ll need
- An
Invoicessheet with a header row and at least these columns:date,amount, andclient. - A
Dashboardtab (in its own spreadsheet here) containing two named ranges:MonthLabels, a single row of twelve cells for the month names, andRevenue, a single row of twelve cells for the totals. - Whatever charts you want, pointed at those two named ranges so they redraw automatically when the values change.
The script
// The spreadsheet holding the raw invoice rows.
const INVOICES_SHEET_ID = '1abcInvoicesSheetId';
// The spreadsheet holding the Dashboard tab and its named ranges.
const DASHBOARD_SHEET_ID = '1abcDashboardSheetId';
// How many months the rolling window covers.
const WINDOW_MONTHS = 12;
/**
* Rebuilds the rolling 12-month revenue rollup: works out the trailing
* months, totals invoices into each, and writes labels and figures into
* the Dashboard's named ranges.
*/
function rebuildRolling12() {
// 1. Load every invoice as a plain object keyed by column name.
const invoices = readSheet(INVOICES_SHEET_ID);
if (!invoices.length) {
Logger.log('No invoices found — nothing to roll up.');
return;
}
// 2. Build the list of trailing months and their display labels.
const months = lastNMonths(WINDOW_MONTHS);
const labels = months.map((m) => Utilities.formatDate(m, 'GMT', 'MMM yy'));
// 3. Total each month's revenue: invoices on or after the month start
// and before the next month start.
const totals = months.map((monthStart) => {
const nextMonth = new Date(monthStart);
nextMonth.setMonth(nextMonth.getMonth() + 1);
return invoices
.filter((inv) => inv.date >= monthStart && inv.date < nextMonth)
.reduce((sum, inv) => sum + inv.amount, 0);
});
// 4. Write the labels and totals into the dashboard's named ranges.
const dash = SpreadsheetApp.openById(DASHBOARD_SHEET_ID);
dash.getRangeByName('MonthLabels').setValues([labels]);
dash.getRangeByName('Revenue').setValues([totals]);
Logger.log(`Rebuilt ${WINDOW_MONTHS}-month rollup ending ${labels.at(-1)}.`);
}
/**
* Returns an array of Date objects, one per month, each set to the 1st,
* running from (n-1) months ago up to the current month.
*/
function lastNMonths(n) {
const out = [];
const cursor = new Date();
cursor.setDate(1);
for (let i = n - 1; i >= 0; i--) {
const month = new Date(cursor);
month.setMonth(month.getMonth() - i);
out.push(month);
}
return out;
}
/**
* Reads a sheet and returns its rows as objects keyed by the header row.
*/
function readSheet(id) {
const [header, ...rows] = SpreadsheetApp.openById(id)
.getSheets()[0]
.getDataRange()
.getValues();
return rows.map((row) =>
Object.fromEntries(header.map((key, i) => [key, row[i]]))
);
}
How it works
rebuildRolling12callsreadSheetto load every invoice as an object, so the rest of the code usesinv.dateandinv.amountrather than column numbers, then stops early if the sheet is empty.lastNMonthsbuilds twelveDateobjects, each pinned to the 1st of its month, running from eleven months ago up to the current month.- Those dates are formatted into short labels like
Jul 25for theMonthLabelsrange. - For each month it computes the next month’s start and sums every invoice
whose
datefalls in that half-open window — on or after the month start, strictly before the next. - It opens the dashboard spreadsheet and writes the twelve labels and twelve
totals into the
MonthLabelsandRevenuenamed ranges in one call each. - Because the charts point at those named ranges, they redraw with the fresh numbers the moment the values change.
Example run
On a 2025-08-06 run, lastNMonths(12) produces a window from September 2024
to August 2025. After totalling the Invoices sheet, the named ranges hold:
| Range | Values |
|---|---|
| MonthLabels | Sep 24, Oct 24, Nov 24, … Jul 25, Aug 25 |
| Revenue | 18400, 21250, 19900, … 24100, 9300 |
When the script runs again on 1 September, the window shifts one month: Sep 24 drops off the front, Sep 25 appears on the end, and every chart bound to
the ranges follows along — no column shuffling required.
Trigger it
Run this nightly so the dashboard is fresh each morning:
- In the Apps Script editor open Triggers (the clock icon).
- Add a trigger for
rebuildRolling12, time-driven, Day timer, set to the 1am–2am slot. - Save. Each night the window slides and the totals refresh.
Watch out for
- The current month is partial. The last column always counts an in-progress month, so its figure climbs through the month and is not comparable to the eleven complete months beside it.
- The
datecolumn must hold real date values, not text. A date typed as a string will fail the>=and<comparisons and that invoice will be silently excluded from every total. - The named ranges must be exactly twelve cells wide. If
MonthLabelsorRevenueis sized differently,setValueswill throw a range-mismatch error. - It totals every invoice regardless of status. Draft or cancelled invoices
count too unless you filter them out before the
reduce. - All months are bucketed in GMT. If Northwind’s invoices straddle a very different timezone, an invoice dated late on a month boundary could land in the neighbouring bucket.
Related
Build a linear-projection forecasting model
Extend a Northwind revenue trend into the next quarter with a simple linear regression.
Updated Aug 30, 2025
Build a subscription churn dashboard
Track active, churned, and reactivated accounts month over month from a Subscriptions sheet.
Updated Aug 27, 2025
Build a cohort-retention analysis tab
Compute month-over-month retention curves from a signups log — built monthly cohorts, automatic.
Updated Aug 23, 2025
Build a budget-vs-actual variance tracker
Compute and colour-code variances between budgeted and actual spend per project category.
Updated Aug 20, 2025
Generate a project burndown chart
Visualise remaining work against an ideal line, refreshed nightly from the Tasks sheet.
Updated Aug 16, 2025