---
title: "Revenue report template with a chart for JavaScript — quario"
description: "A ready-made quario revenue report with a chart. This year against last, month by month, as a d3 bar chart above the figures. Copy the definition and the chart function, point them at your ledger, and render to PDF or HTML."
url: "https://getquario.com/templates/revenue-report"
---

# Revenue report template with a chart

A revenue report answers whether this year is ahead of the last. A reader sees that faster in a
chart than in a column of figures, so this one puts both on the page. Four headline figures sit at
the top, the two years stand as bars month by month, and the table underneath gives the change per
month. The chart
isn’t a picture someone pasted in. Your code draws it from the same rows the table lists, every
time the report renders. This is a ready-made quario revenue report — copy it, point it at your
ledger, and render to PDF or HTML.

## The template

The whole report is a definition — data you can version and review. The headline figures, the
chart’s place on the page, the table and the totals are all in here. This is the exact file the
preview below renders:

```json title="revenue-report.report.json"
{
  "data": "$.sales.months[*]",
  "aggregates": {
    "revenue": "sum:=@.revenue",
    "previous": "sum:=@.previous",
    "orders": "sum:=@.orders",
    "best": "max:=@.revenue"
  },
  "style": { "family": "sans", "size": 10 },
  "header": [
    {
      "type": "split",
      "slots": [
        { "type": "text", "value": "Revenue report, {{ $.input.sales.year }}", "width": 60,
          "style": { "family": "serif", "size": 19, "bold": true, "color": "#1e1f22" } },
        { "type": "text", "value": "{{ $.input.sales.period }} {{ $.input.sales.year }}",
          "style": { "size": 8, "bold": false, "color": "#989a97", "align": "right" } }
      ]
    },
    {
      "type": "split",
      "slots": [
        { "type": "text", "value": "{{ $.input.sales.company }}", "width": 60,
          "style": { "size": 8, "bold": false, "color": "#989a97" } },
        { "type": "text", "value": [{ "value": "Prepared " }, { "value": "{{ $.input.sales.prepared }}" }],
          "style": { "size": 8, "bold": false, "color": "#989a97", "align": "right", "format": "date" } }
      ]
    },
    {
      "type": "split",
      "style": { "spaceBefore": 28 },
      "slots": [
        { "type": "text", "value": "Revenue", "width": 25,
          "style": { "family": "mono", "size": 7, "bold": false, "uppercase": true, "color": "#989a97" } },
        { "type": "text", "value": "Against {{ $.input.sales.year - 1 }}", "width": 25,
          "style": { "family": "mono", "size": 7, "bold": false, "uppercase": true, "color": "#989a97" } },
        { "type": "text", "value": "Orders", "width": 25,
          "style": { "family": "mono", "size": 7, "bold": false, "uppercase": true, "color": "#989a97" } },
        { "type": "text", "value": "Best month",
          "style": { "family": "mono", "size": 7, "bold": false, "uppercase": true, "color": "#989a97" } }
      ]
    },
    {
      "type": "split",
      "style": { "spaceAfter": 22 },
      "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($.revenue / $.previous - 1, 3) }}", "width": 25,
          "style": { "family": "serif", "size": 16, "bold": false, "color": "#1e1f22", "format": { "kind": "percent", "digits": 1 } } },
        { "type": "text", "value": "{{ $.orders }}", "width": 25,
          "style": { "family": "serif", "size": 16, "bold": false, "color": "#1e1f22" } },
        { "type": "text", "value": "{{ $.best }}",
          "style": { "family": "serif", "size": 16, "bold": false, "color": "#1e1f22", "format": { "kind": "currency", "digits": 0 } } }
      ]
    },
    {
      "type": "image",
      "source": "=chart($.input.sales.months, $.input.sales.year)",
      "fit": "width",
      "alt": "Revenue per month, {{ $.input.sales.year }} beside {{ $.input.sales.year - 1 }}",
      "style": { "spaceAfter": 28 }
    }
  ],
  "detail": {
    "header": {
      "style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97",
        "borderBottomWidth": 0.5, "borderBottomStyle": "solid", "borderBottomColor": "#1e1f22" }
    },
    "row": { "style": { "color": "#5e6062" } },
    "columns": [
      { "header": "Month", "value": "{{ @.month }}", "width": 28 },
      { "header": { "value": "Orders", "style": { "align": "right" } }, "value": "{{ @.orders }}", "width": 12, "style": { "align": "right" } },
      { "header": { "value": "{{ $.input.sales.year - 1 }}", "style": { "align": "right" } }, "value": "{{ @.previous }}", "width": 20, "style": { "align": "right", "format": "currency" } },
      { "header": { "value": "{{ $.input.sales.year }}", "style": { "align": "right" } }, "value": "{{ @.revenue }}", "width": 20, "style": { "align": "right", "color": "#1e1f22", "format": "currency" } },
      { "header": { "value": "Change", "style": { "align": "right" } }, "value": "{{ round(@.revenue / @.previous - 1, 3) }}", "width": 20, "style": { "align": "right", "format": { "kind": "percent", "digits": 1 } } }
    ],
    "total": {
      "style": { "bold": true, "color": "#1e1f22", "align": "right", "paddingTop": 4,
        "borderTopWidth": 0.5, "borderTopStyle": "solid", "borderTopColor": "#1e1f22" },
      "rows": [
        {
          "cells": [
            { "value": "Total", "style": { "align": "left" } },
            { "value": "{{ $.orders }}" },
            { "value": "{{ round($.previous, 2) }}", "style": { "format": "currency" } },
            { "value": "{{ round($.revenue, 2) }}", "style": { "format": "currency" } },
            { "value": "{{ round($.revenue / $.previous - 1, 3) }}", "style": { "format": { "kind": "percent", "digits": 1 } } }
          ]
        }
      ]
    }
  },
  "footer": [
    { "type": "text", "value": "Amounts exclude VAT. Source: {{ $.input.sales.source }}.",
      "style": { "size": 8, "color": "#989a97", "spaceBefore": 10 } }
  ],
  "page": {
    "footer": [
      {
        "type": "split",
        "style": { "paddingTop": 10,
          "borderTopWidth": 0.5, "borderTopStyle": "solid", "borderTopColor": "#1e1f22" },
        "slots": [
          { "type": "text", "value": "{{ $.input.sales.company }} · internal", "width": 70,
            "style": { "size": 8, "color": "#989a97" } },
          { "type": "text", "value": "Page {{ page.number }} of {{ page.total }}",
            "style": { "size": 8, "color": "#989a97", "align": "right" } }
        ]
      }
    ]
  }
}
```

The headline figures are report aggregates — three `sum`s and a `max` — read from `$` in the
header. The change against last year is one expression,
`round($.revenue / $.previous - 1, 3)`. The `percent` kind reads that ratio and prints `10.6%`, with
`"digits": 1` for the one decimal. The `round` keeps the stored ratio to the precision it presents,
so a workbook cell holds `0.106` rather than a float’s tail.

The chart is an `image` item whose `source` is `=chart($.input.sales.months, $.input.sales.year)`.
`chart` is a function your code registers. The definition says where the chart goes and which rows
it draws, so the chart and the table can’t disagree.

## The chart

The function is ordinary host code. d3’s scales place the months and the two bars inside each one,
and the rest is a string of SVG. It needs no DOM, so it runs in Node, in a worker, or in the browser,
which is where the preview below runs it:

```ts title="chart.ts"
import { scaleBand, scaleLinear } from "d3-scale";

type Month = { month: string; revenue: number; previous: number };

const THIS_YEAR = "#2e708f";
const LAST_YEAR = "#bfd0da";

const money = new Intl.NumberFormat("en-IE", {
  style: "currency",
  currency: "EUR",
  notation: "compact",
});

/** This year's revenue beside last year's, per month, as the SVG's bytes. */
export function chart(months: Month[], year: number): Uint8Array {
  const width = 720;
  const height = 330;
  const left = 52;
  const top = 34;
  const bottom = height - 26;

  const x = scaleBand(
    months.map((m) => m.month),
    [left, width],
  ).padding(0.28);
  const pair = scaleBand(["previous", "revenue"], [0, x.bandwidth()]).padding(
    0.08,
  );
  const y = scaleLinear(
    [0, Math.max(...months.flatMap((m) => [m.revenue, m.previous]))],
    [bottom, top],
  ).nice();

  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>`,
  );

  const bars = months.map((m) => {
    const at = x(m.month) ?? 0;
    const bar = (key: "previous" | "revenue", fill: string) =>
      `<rect x="${at + (pair(key) ?? 0)}" y="${y(m[key])}" width="${pair.bandwidth()}" height="${bottom - y(m[key])}" fill="${fill}" />`;
    return `
    ${bar("previous", LAST_YEAR)}${bar("revenue", THIS_YEAR)}
    <text x="${at + x.bandwidth() / 2}" y="${height - 6}" text-anchor="middle">${m.month.slice(0, 3)}</text>`;
  });

  const legend = `
    <rect x="${width - 112}" y="4" width="10" height="10" fill="${THIS_YEAR}" />
    <text x="${width - 96}" y="13">${year}</text>
    <rect x="${width - 56}" y="4" width="10" height="10" fill="${LAST_YEAR}" />
    <text x="${width - 40}" y="13">${year - 1}</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">
  ${gridlines.join("")}
  ${bars.join("")}
  ${legend}
</svg>`;
  return new TextEncoder().encode(svg);
}
```

The root states its own `width` and `height`, because quario reads the picture’s size from the
bytes, and the function returns those bytes as a `Uint8Array`. The labels stay in Helvetica’s
Western European set, which is what the PDF target draws SVG text in.

## The data

A definition renders nothing on its own — you hand it a document. Each month carries this year’s
revenue and last year’s, so one row feeds a pair of bars and a line of the table:

```json title="revenue-report.data.json"
{
  "sales": {
    "company": "Quire Press Ltd.",
    "year": 2026,
    "period": "January to September",
    "prepared": "2026-10-02",
    "source": "sales ledger, invoices issued in the period",
    "months": [
      {"month": "January", "orders": 41, "revenue": 18420, "previous": 16950.5},
      {"month": "February", "orders": 38, "revenue": 17260.75, "previous": 17880},
      {"month": "March", "orders": 52, "revenue": 24310, "previous": 20140.25},
      {"month": "April", "orders": 47, "revenue": 21980.5, "previous": 19325},
      {"month": "May", "orders": 55, "revenue": 26140, "previous": 21760},
      {"month": "June", "orders": 44, "revenue": 20875.25, "previous": 22410.5},
      {"month": "July", "orders": 36, "revenue": 16930, "previous": 15280},
      {"month": "August", "orders": 31, "revenue": 14615.5, "previous": 14090.75},
      {"month": "September", "orders": 58, "revenue": 28460, "previous": 23115}
    ]
  }
}
```

## What it renders

This isn’t a screenshot. quario compiles the definition and data above, 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 on this one: a Word document shows an SVG only beside a raster copy, so `@quario/docx`
refuses it.

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

## Use it

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

```ts title="revenue-report.ts"
import { writeFileSync } from 'node:fs'
import { quario } from 'quario'
import { pdf } from '@quario/pdf'
import revenue from './revenue-report.report.json' with { type: 'json' }

// `chart` is the function from chart.ts
const q = quario({ locale: 'en-IE', currency: 'EUR' })   // what `format` presents through
const report = q.report(revenue, { chart })

const data = await getSales('2026')                   // your ledger
const bytes = await report.render(pdf({ page: { size: 'A4', margin: 54 } }), data)

writeFileSync('revenue-report.pdf', bytes)
```

The [Charts in a PDF](https://getquario.com/guides/charts-in-pdf.md) guide draws the same kind of chart with ECharts and
Vega-Lite. It also shows which of them can be a registered function.

## Make it yours

The chart is your code, so it’s whatever your reader asks for. A line for the trend is a d3-shape
`line()` path beside the bars. A target is one more `<line>` across the plot. Another kind of chart
is another function, registered under its own name, and the definition calls each where it belongs.
The definition diffs in a pull request as data, and the chart diffs beside it as code.

## FAQ

### Can I generate the report as a PDF?

Yes — `@quario/pdf` renders it straight to PDF bytes, with no browser anywhere, and draws the chart
as vector paths. [Full example →](https://getquario.com/guides/json-to-pdf.md)

### Can I export the report to Excel or Word?

Not with an SVG chart. 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 it, 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 number in the workbook is still a number.

### Does the chart work in HTML?

Yes. `@quario/html` writes it as an `<img>` with a `data:` URI. A browser runs no script in it and
loads nothing it names, so a chart never becomes markup on your page.

### Where are the other templates?

[Browse the template gallery →](https://getquario.com/templates.md) — summary reports, statements, 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":"Revenue report","item":"https://getquario.com/templates/revenue-report"}]}
```
