---
title: "Board report template with a chart for JavaScript — quario"
description: "A ready-made quario board report. The quarter on one A4 page: four figures against plan, revenue by month against plan as a chart, a financial summary with variances, then highlights, risks and the decisions asked for. Copy the definition and the chart function, point them at your accounts, and render to PDF or HTML."
url: "https://getquario.com/templates/board-report"
---

# 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.

*Preview: the page renders this definition and its sample data live, with downloads — [see it rendered](https://getquario.com/templates/board-report).*

## The template

The whole report is a definition — data you can version and review. This is the exact file the
preview above renders:

```json title="board-report.report.json"
{
  "data": "$.board.lines[*]",
  "aggregates": {
    "profit": "sum:=@.actual",
    "profitPlan": "sum:=@.plan",
    "profitYtd": "sum:=@.ytd",
    "profitYtdPlan": "sum:=@.ytdPlan",
    "revenue": "sum:=@.section == 'revenue' ? @.actual : 0",
    "revenuePlan": "sum:=@.section == 'revenue' ? @.plan : 0",
    "gross": "sum:=@.section != 'overhead' ? @.actual : 0",
    "grossPlan": "sum:=@.section != '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:=@.section == '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:

```ts title="chart.ts"
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:

```json title="board-report.data.json"
{
  "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

```bash title="shell"
npm install quario @quario/pdf d3-scale
```

```ts title="board-report.ts"
import { 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 →](https://getquario.com/templates.md) — revenue reports, expense reports, invoices, and more.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://getquario.com/"},{"@type":"ListItem","position":2,"name":"Templates","item":"https://getquario.com/templates"},{"@type":"ListItem","position":3,"name":"Board report","item":"https://getquario.com/templates/board-report"}]}
```
