Board report template with a chart
A board report is the page a director reads before the meeting. It answers three questions: is the company on plan, why not, and what does the board have to decide. This one puts the quarter on one A4 page. Two sentences from the managing director open it, then four figures against plan and the year’s revenue by month beside the plan. A financial summary gives each line’s variance for the quarter and the year to date. Highlights, risks and the decisions for the meeting close the page. This is a ready-made quario board report — copy it, point it at your accounts, and render to PDF or HTML.
What it renders
This isn’t a screenshot. quario compiles the definition and data below, runs chart.ts in your
browser, and @quario/viewer paints the result right here. The PDF button hands you the same report
as real @quario/pdf bytes, where the chart is vector paths rather than pixels. There’s no Word
button: @quario/docx refuses an SVG image.
The template
The whole report is a definition — data you can version and review. This is the exact file the preview above renders:
{
"data": "$.board.lines[*]",
"aggregates": {
"profit": "sum:[email protected]",
"profitPlan": "sum:[email protected]",
"profitYtd": "sum:[email protected]",
"profitYtdPlan": "sum:[email protected]",
"revenue": "sum:[email protected] == 'revenue' ? @.actual : 0",
"revenuePlan": "sum:[email protected] == 'revenue' ? @.plan : 0",
"gross": "sum:[email protected] != 'overhead' ? @.actual : 0",
"grossPlan": "sum:[email protected] != 'overhead' ? @.plan : 0"
},
"style": { "family": "sans", "size": 10 },
"header": [
{
"type": "split",
"slots": [
{ "type": "text", "value": "Board report, Q{{ $.input.board.quarter }} {{ $.input.board.year }}", "width": 60,
"style": { "family": "serif", "size": 19, "bold": true, "color": "#1e1f22" } },
{ "type": "text", "value": [{ "value": "Board meeting " }, { "value": "{{ $.input.board.meeting }}" }],
"style": { "size": 8, "bold": false, "color": "#989a97", "align": "right", "format": "date" } }
]
},
{
"type": "split",
"slots": [
{ "type": "text", "value": "{{ $.input.board.company }}", "width": 60,
"style": { "size": 8, "bold": false, "color": "#989a97" } },
{ "type": "text", "value": [{ "value": "Prepared " }, { "value": "{{ $.input.board.prepared }}" }],
"style": { "size": 8, "bold": false, "color": "#989a97", "align": "right", "format": "date" } }
]
},
{
"type": "text",
"value": "{{ $.input.board.summary }}",
"style": { "family": "serif", "size": 12, "bold": false, "color": "#1e1f22", "spaceBefore": 20, "spaceAfter": 18 }
},
{
"type": "split",
"slots": [
{ "type": "text", "value": "Revenue", "width": 25,
"style": { "family": "mono", "size": 7, "bold": false, "uppercase": true, "color": "#989a97" } },
{ "type": "text", "value": "Gross margin", "width": 25,
"style": { "family": "mono", "size": 7, "bold": false, "uppercase": true, "color": "#989a97" } },
{ "type": "text", "value": "Operating profit", "width": 25,
"style": { "family": "mono", "size": 7, "bold": false, "uppercase": true, "color": "#989a97" } },
{ "type": "text", "value": "Cash at quarter end",
"style": { "family": "mono", "size": 7, "bold": false, "uppercase": true, "color": "#989a97" } }
]
},
{
"type": "split",
"slots": [
{ "type": "text", "value": "{{ round($.revenue, 2) }}", "width": 25,
"style": { "family": "serif", "size": 16, "bold": false, "color": "#1e1f22", "format": { "kind": "currency", "digits": 0 } } },
{ "type": "text", "value": "{{ round($.gross / $.revenue, 3) }}", "width": 25,
"style": { "family": "serif", "size": 16, "bold": false, "color": "#1e1f22", "format": { "kind": "percent", "digits": 1 } } },
{ "type": "text", "value": "{{ round($.profit, 2) }}", "width": 25,
"style": { "family": "serif", "size": 16, "bold": false, "color": "#1e1f22", "format": { "kind": "currency", "digits": 0 } } },
{ "type": "text", "value": "{{ $.input.board.cash.actual }}",
"style": { "family": "serif", "size": 16, "bold": false, "color": "#1e1f22", "format": { "kind": "currency", "digits": 0 } } }
]
},
{
"type": "split",
"style": { "spaceAfter": 20 },
"slots": [
{ "type": "text", "width": 25, "style": { "size": 8, "bold": false, "color": "#5e6062", "format": { "kind": "currency", "digits": 0 } },
"value": [{ "value": "Plan " }, { "value": "{{ round($.revenuePlan, 2) }}" }, { "value": " · {{ $.revenue >= $.revenuePlan ? '+' : '' }}" }, { "value": "{{ round($.revenue - $.revenuePlan, 2) }}" }] },
{ "type": "text", "width": 25, "style": { "size": 8, "bold": false, "color": "#5e6062" },
"value": [{ "value": "Plan " }, { "value": "{{ round($.grossPlan / $.revenuePlan, 3) }}", "style": { "format": { "kind": "percent", "digits": 1 } } }, { "value": " · {{ $.gross / $.revenue >= $.grossPlan / $.revenuePlan ? '+' : '' }}" }, { "value": "{{ round(($.gross / $.revenue - $.grossPlan / $.revenuePlan) * 100, 1) }}", "style": { "format": { "kind": "number", "digits": 1 } } }, { "value": " pts" }] },
{ "type": "text", "width": 25, "style": { "size": 8, "bold": false, "color": "#5e6062", "format": { "kind": "currency", "digits": 0 } },
"value": [{ "value": "Plan " }, { "value": "{{ round($.profitPlan, 2) }}", "style": { "format": { "kind": "currency", "digits": 0, "negative": "parens" } } }, { "value": " · {{ $.profit >= $.profitPlan ? '+' : '' }}" }, { "value": "{{ round($.profit - $.profitPlan, 2) }}", "style": { "format": { "kind": "currency", "digits": 0, "negative": "parens" } } }] },
{ "type": "text", "style": { "size": 8, "bold": false, "color": "#5e6062", "format": { "kind": "currency", "digits": 0 } },
"value": [{ "value": "Plan " }, { "value": "{{ $.input.board.cash.plan }}" }, { "value": " · {{ $.input.board.cash.actual >= $.input.board.cash.plan ? '+' : '' }}" }, { "value": "{{ round($.input.board.cash.actual - $.input.board.cash.plan, 2) }}" }] }
]
},
{
"type": "image",
"source": "=chart($.input.board.months, $.input.board.quarter)",
"fit": "width",
"alt": "Revenue per month against plan, {{ $.input.board.year }}",
"style": { "spaceAfter": 16 }
},
{
"type": "split",
"style": { "family": "mono", "size": 7, "bold": false, "uppercase": true, "color": "#989a97", "paddingBottom": 4 },
"slots": [
{ "type": "text", "value": "Financial summary", "width": 22 },
{ "type": "text", "value": "Q{{ $.input.board.quarter }}", "width": 39, "style": { "align": "center" } },
{ "type": "text", "value": "Year to date", "style": { "align": "center" } }
]
}
],
"detail": {
"header": {
"style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97",
"borderBottomWidth": 0.5, "borderBottomStyle": "solid", "borderBottomColor": "#1e1f22" }
},
"row": { "style": { "size": 9, "color": "#5e6062" } },
"columns": [
{ "header": "", "value": "{{ @.line }}", "width": 22, "style": { "color": "#1e1f22" } },
{ "header": { "value": "Actual", "style": { "align": "right" } }, "value": "{{ @.actual }}", "width": 13, "style": { "align": "right", "format": { "kind": "currency", "digits": 0, "negative": "parens" } } },
{ "header": { "value": "Plan", "style": { "align": "right" } }, "value": "{{ @.plan }}", "width": 13, "style": { "align": "right", "format": { "kind": "currency", "digits": 0, "negative": "parens" } } },
{ "header": { "value": "Variance", "style": { "align": "right" } }, "value": "{{ round(@.actual - @.plan, 2) }}", "width": 13, "style": { "align": "right", "format": { "kind": "currency", "digits": 0, "negative": "parens", "zero": "dash" } } },
{ "header": { "value": "Actual", "style": { "align": "right" } }, "value": "{{ @.ytd }}", "width": 13, "style": { "align": "right", "format": { "kind": "currency", "digits": 0, "negative": "parens" } } },
{ "header": { "value": "Plan", "style": { "align": "right" } }, "value": "{{ @.ytdPlan }}", "width": 13, "style": { "align": "right", "format": { "kind": "currency", "digits": 0, "negative": "parens" } } },
{ "header": { "value": "Variance", "style": { "align": "right" } }, "value": "{{ round(@.ytd - @.ytdPlan, 2) }}", "width": 13, "style": { "align": "right", "format": { "kind": "currency", "digits": 0, "negative": "parens", "zero": "dash" } } }
],
"total": {
"style": { "size": 9, "color": "#1e1f22", "align": "right" },
"rows": [
{
"style": { "bold": true, "paddingTop": 4, "borderTopWidth": 0.5, "borderTopStyle": "solid", "borderTopColor": "#1e1f22" },
"cells": [
{ "value": "Operating profit", "style": { "align": "left" } },
{ "value": "{{ round($.profit, 2) }}", "style": { "format": { "kind": "currency", "digits": 0, "negative": "parens" } } },
{ "value": "{{ round($.profitPlan, 2) }}", "style": { "format": { "kind": "currency", "digits": 0, "negative": "parens" } } },
{ "value": "{{ round($.profit - $.profitPlan, 2) }}", "style": { "format": { "kind": "currency", "digits": 0, "negative": "parens" } } },
{ "value": "{{ round($.profitYtd, 2) }}", "style": { "format": { "kind": "currency", "digits": 0, "negative": "parens" } } },
{ "value": "{{ round($.profitYtdPlan, 2) }}", "style": { "format": { "kind": "currency", "digits": 0, "negative": "parens" } } },
{ "value": "{{ round($.profitYtd - $.profitYtdPlan, 2) }}", "style": { "format": { "kind": "currency", "digits": 0, "negative": "parens" } } }
]
}
]
}
},
"footer": [
{
"type": "grid",
"columns": [{ "width": 48 }, { "width": 4 }, {}],
"rows": [
{ "style": { "family": "mono", "size": 7, "bold": false, "uppercase": true, "color": "#989a97", "paddingTop": 20, "paddingBottom": 4 },
"slots": [{ "type": "text", "value": "Highlights" }, { "type": "text", "value": "" }, { "type": "text", "value": "Risks" }] },
{ "style": { "size": 9, "color": "#1e1f22" },
"slots": [{ "type": "text", "value": "{{ bullets($.input.board.highlights) }}" }, { "type": "text", "value": "" }, { "type": "text", "value": "{{ bullets($.input.board.risks) }}" }] }
]
},
{
"type": "text",
"value": "For decision",
"style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97", "spaceBefore": 14, "paddingBottom": 4 }
},
{
"type": "text",
"value": "{{ bullets($.input.board.decisions) }}",
"style": { "size": 9, "color": "#1e1f22" }
}
],
"page": {
"header": [
{
"type": "text",
"value": "Board report, Q{{ $.input.board.quarter }} {{ $.input.board.year }} · continued",
"visible": "=page.number > 1",
"style": { "size": 8, "color": "#989a97", "paddingBottom": 6, "spaceAfter": 18,
"borderBottomWidth": 0.5, "borderBottomStyle": "solid", "borderBottomColor": "#1e1f22" }
}
],
"footer": [
{
"type": "split",
"style": { "paddingTop": 10,
"borderTopWidth": 0.5, "borderTopStyle": "solid", "borderTopColor": "#1e1f22" },
"slots": [
{ "type": "text", "value": "{{ $.input.board.company }} · board confidential. Unaudited management accounts. Costs are in brackets, and so is a variance against the company.", "width": 80,
"style": { "size": 8, "color": "#989a97" } },
{ "type": "text", "value": "Page {{ page.number }} of {{ page.total }}",
"style": { "size": 8, "color": "#989a97", "align": "right" } }
]
}
]
}
}The rows are the lines of the profit and loss account, with costs stored as negative amounts. So
operating profit is no line of its own: it’s detail.total, the sum of every line. The table shows it for
the quarter and for the year to date. The figures at the top are aggregates over the same rows. Revenue
is sum:[email protected] == 'revenue' ? @.actual : 0, and gross profit is the same sum over every line
that isn’t an overhead. The figures and the table can’t disagree, because one set of rows feeds
both.
Each figure carries its plan and its distance from it as one cell of styled runs. The distance is
round($.revenue - $.revenuePlan, 2) under the currency kind, with a + written in front when
it’s ahead. The gross margin’s distance is in percentage points. The table’s amounts declare
"negative": "parens", so a cost reads (€27,840), and its variances add "zero": "dash", so a
line exactly on plan reads as a dash rather than €0.
Highlights, risks, and decisions are lists in the data. bullets() sets each one as bulleted lines,
and the cell wraps them.
The chart
d3’s scales place twelve months across the width. A bar is the month’s revenue, and the plan is the dark tick across it. The months still to come have a tick and no bar, so the board sees the plan the rest of the year has to meet. A pale band marks the quarter the report covers:
import { scaleBand, scaleLinear } from "d3-scale";
type Month = { month: string; actual: number | null; plan: number };
const ACTUAL = "#2e708f";
const PLAN = "#1e1f22";
const QUARTER = "#eef3f6";
const money = new Intl.NumberFormat("en-IE", {
style: "currency",
currency: "EUR",
notation: "compact",
});
/** A year of revenue against plan, with the reported quarter shaded, as the SVG's bytes. */
export function chart(months: Month[], quarter: number): Uint8Array {
const width = 720;
const height = 200;
const left = 52;
const top = 30;
const bottom = height - 26;
const x = scaleBand(
months.map((m) => m.month),
[left, width],
).padding(0.3);
const y = scaleLinear(
[0, Math.max(...months.flatMap((m) => [m.actual ?? 0, m.plan]))],
[bottom, top],
).nice();
const first = x(months[(quarter - 1) * 3].month) ?? 0;
const step = x.step();
const shade = `
<rect x="${first - (step - x.bandwidth()) / 2}" y="${top - 12}" width="${step * 3}" height="${bottom - top + 12}" fill="${QUARTER}" />
<text x="${first - (step - x.bandwidth()) / 2 + 6}" y="${top - 2}" fill="#1e1f22">Q${quarter}</text>`;
const gridlines = y.ticks(4).map(
(tick) => `
<line x1="${left}" x2="${width}" y1="${y(tick)}" y2="${y(tick)}" stroke="#e3e8eb" />
<text x="${left - 8}" y="${y(tick) + 4}" text-anchor="end">${money.format(tick)}</text>`,
);
// A month still to come has a plan and no actual: its tick stands alone.
const bars = months.map((m) => {
const at = x(m.month) ?? 0;
const bar =
m.actual === null
? ""
: `<rect x="${at}" y="${y(m.actual)}" width="${x.bandwidth()}" height="${bottom - y(m.actual)}" fill="${ACTUAL}" />`;
return `
${bar}
<line x1="${at - 4}" x2="${at + x.bandwidth() + 4}" y1="${y(m.plan)}" y2="${y(m.plan)}" stroke="${PLAN}" stroke-width="2" />
<text x="${at + x.bandwidth() / 2}" y="${height - 6}" text-anchor="middle">${m.month.slice(0, 3)}</text>`;
});
const legend = `
<rect x="${width - 120}" y="2" width="10" height="10" fill="${ACTUAL}" />
<text x="${width - 104}" y="11">Actual</text>
<line x1="${width - 56}" x2="${width - 42}" y1="7" y2="7" stroke="${PLAN}" stroke-width="2" />
<text x="${width - 36}" y="11">Plan</text>`;
// A width and height on the root: the image item reads its size from the bytes.
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"
font-family="Helvetica, Arial, sans-serif" font-size="11" fill="#627179">
${shade}
${gridlines.join("")}
${bars.join("")}
${legend}
</svg>`;
return new TextEncoder().encode(svg);
}
/** A list as bulleted lines, one paragraph per item. */
export function bullets(list: string[]): string {
return list.map((item) => `• ${item}`).join("\n");
}The plan is a solid tick rather than a dashed line. The PDF target draws a stroke’s colour and width, but not a dash pattern. A chart that reads one way on screen and another on paper is worse than either.
The data
lines is the financial summary, one row per line, with section marking revenue, direct costs,
and overheads. months feeds the chart, with null for a month that hasn’t happened. The cash position
is a balance, not a line of the account, so it stands on its own:
{
"board": {
"company": "Quire Press Ltd.",
"quarter": 3,
"year": 2026,
"meeting": "2026-10-28",
"prepared": "2026-10-21",
"preparedBy": "M. de Graaf, managing director",
"summary": "A record September carried the quarter. Revenue closed 5.3% over plan and operating profit well ahead of it, though paper ran over budget again. Cash stands above plan.",
"cash": { "actual": 48200, "plan": 40000 },
"months": [
{"month": "January", "actual": 18420, "plan": 17500},
{"month": "February", "actual": 17261, "plan": 17500},
{"month": "March", "actual": 24310, "plan": 21000},
{"month": "April", "actual": 21980, "plan": 21000},
{"month": "May", "actual": 26140, "plan": 23500},
{"month": "June", "actual": 20875, "plan": 22000},
{"month": "July", "actual": 16930, "plan": 17000},
{"month": "August", "actual": 14616, "plan": 15000},
{"month": "September", "actual": 28460, "plan": 25000},
{"month": "October", "actual": null, "plan": 24000},
{"month": "November", "actual": null, "plan": 26500},
{"month": "December", "actual": null, "plan": 21000}
],
"lines": [
{"line": "Revenue", "section": "revenue", "actual": 60006, "plan": 57000, "ytd": 188992, "ytdPlan": 179500},
{"line": "Cost of sales", "section": "direct", "actual": -27840, "plan": -25650, "ytd": -84210, "ytdPlan": -80780},
{"line": "Staff", "section": "overhead", "actual": -18450, "plan": -18600, "ytd": -55020, "ytdPlan": -55800},
{"line": "Rent and premises", "section": "overhead", "actual": -7650, "plan": -7650, "ytd": -22950, "ytdPlan": -22950},
{"line": "Marketing and fairs", "section": "overhead", "actual": -2310, "plan": -2800, "ytd": -6480, "ytdPlan": -8400},
{"line": "Other overheads", "section": "overhead", "actual": -1620, "plan": -1500, "ytd": -4870, "ytdPlan": -4500}
],
"highlights": [
"September was the best month on record, on two catalogue runs.",
"Two new publisher accounts, in Antwerp and Cologne, with Q4 runs booked.",
"Marketing came in under plan: a partner press shared the fair stand."
],
"risks": [
"Paper rose 7% in July, and cost of sales is over plan for a third quarter.",
"August is still the weakest month. The press sat idle for nine days.",
"One customer is 18% of revenue this year."
],
"decisions": [
"Approve a fixed-price paper contract for 2027, at up to €96,000.",
"Note the Q4 forecast, which holds the full-year plan."
]
}
}Use it
npm install quario @quario/pdf d3-scaleimport { writeFileSync } from 'node:fs'
import { quario } from 'quario'
import { pdf } from '@quario/pdf'
import board from './board-report.report.json' with { type: 'json' }
// `chart` and `bullets` are the functions from chart.ts
const q = quario({ locale: 'en-IE', currency: 'EUR' }) // what `format` presents through
const report = q.report(board, { chart, bullets })
const data = await getBoardReport('2026-Q3') // your management accounts
const bytes = await report.render(pdf({ page: { size: 'A4', margin: 54 } }), data)
writeFileSync('board-report.pdf', bytes)Make it yours
A monthly board report is the same rows over a month, with a title that names the month. Its chart
shades one month rather than three. Another figure at the top is one more slot in each of the three
rows. A line the board asks about is one more row in lines. Operating profit picks it up, because
the total is a sum. The chart is your code. A cumulative line against the full-year plan is a different
function under the same name, and the definition doesn’t change.
FAQ
Can I export the board report to Word or Excel?
Not with an SVG chart in it. A worksheet drawing holds a raster picture only, and Word shows an SVG
only beside a raster copy. So @quario/xlsx and @quario/docx refuse one, with a render error that
names the item. For those renders, register a chart that returns PNG bytes. The definition stays
the same, and every figure in the workbook is still a number.
Why are costs in brackets?
Because a board reads a profit and loss account that way. A cost stored as a negative amount makes operating profit a plain sum. It also makes every variance read the same way: in brackets is against the company, whether the line is revenue or cost.
What if the report runs past one page?
The financial summary breaks across pages like any table, and every page after the first opens with
a “continued” line naming the report. The fine print sits in page.footer, so it’s on the baseline
of every page.
Where are the other templates?
Browse the template gallery → — revenue reports, expense reports, invoices, and more.