---
title: "Expense report template with a chart for JavaScript — quario"
description: "A ready-made quario expense report with a ring chart. Spend per category at a glance, every receipt under its category, and the reimbursement due after the cash advance. Copy the definition and the chart function, point them at your expense claims, and render to PDF or HTML."
url: "https://getquario.com/templates/expense-breakdown"
---

# Expense report template with a chart

An expense report is an employee’s claim. It lists what they spent on the company’s behalf over a
period, and what the company owes them back. This one opens with who and why, then four figures —
the total, the receipts, the cash advance, and the reimbursement due. The ring under them shows where the money
went at a glance. Each category has one colour, and its amount and its share stand beside it.
Underneath, every receipt sits under its category with a subtotal, so an approver who doubts a slice
can count it. Your code draws the ring from the same receipts the table lists, every time the report
renders. This is a ready-made quario expense report — copy it, point it at your expense claims, and
render to PDF or HTML.

## The template

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

```json title="expense-breakdown.report.json"
{
  "data": "$.expenses.items[*]",
  "sort": [{ "by": "=@.category" }, { "by": "=@.date" }],
  "aggregates": {
    "total": "sum:=@.amount",
    "count": "count"
  },
  "style": { "family": "sans", "size": 10 },
  "header": [
    {
      "type": "split",
      "slots": [
        { "type": "text", "value": "Expense report", "width": 60,
          "style": { "family": "serif", "size": 19, "bold": true, "color": "#1e1f22" } },
        { "type": "text", "value": [{ "value": "{{ $.input.expenses.period.from }}" }, { "value": " to " }, { "value": "{{ $.input.expenses.period.to }}" }],
          "style": { "size": 8, "bold": false, "color": "#989a97", "align": "right", "format": "date" } }
      ]
    },
    {
      "type": "split",
      "slots": [
        { "type": "text", "value": "No. {{ $.input.expenses.number }}", "width": 60,
          "style": { "size": 8, "bold": false, "color": "#989a97" } },
        { "type": "text", "value": [{ "value": "Submitted " }, { "value": "{{ $.input.expenses.submitted }}" }],
          "style": { "size": 8, "bold": false, "color": "#989a97", "align": "right", "format": "date" } }
      ]
    },
    {
      "type": "grid",
      "columns": [{ "width": 50 }, {}],
      "rows": [
        { "style": { "family": "mono", "size": 7, "bold": false, "uppercase": true, "color": "#989a97", "paddingTop": 22 },
          "slots": [{ "type": "text", "value": "Employee" }, { "type": "text", "value": "Purpose" }] },
        { "style": { "size": 10, "bold": true, "color": "#1e1f22" },
          "slots": [{ "type": "text", "value": "{{ $.input.expenses.employee.name }}" }, { "type": "text", "value": "{{ $.input.expenses.purpose }}" }] },
        { "style": { "size": 10, "bold": false, "color": "#5e6062" },
          "slots": [{ "type": "text", "value": "{{ $.input.expenses.employee.role }}, No. {{ $.input.expenses.employee.number }}" }, { "type": "text", "value": "{{ $.input.expenses.company }}, {{ $.input.expenses.period.label }}" }] }
      ]
    },
    {
      "type": "split",
      "style": { "spaceBefore": 24 },
      "slots": [
        { "type": "text", "value": "Total expenses", "width": 25,
          "style": { "family": "mono", "size": 7, "bold": false, "uppercase": true, "color": "#989a97" } },
        { "type": "text", "value": "Receipts", "width": 25,
          "style": { "family": "mono", "size": 7, "bold": false, "uppercase": true, "color": "#989a97" } },
        { "type": "text", "value": "Cash advance", "width": 25,
          "style": { "family": "mono", "size": 7, "bold": false, "uppercase": true, "color": "#989a97" } },
        { "type": "text", "value": "Reimbursement due",
          "style": { "family": "mono", "size": 7, "bold": false, "uppercase": true, "color": "#989a97" } }
      ]
    },
    {
      "type": "split",
      "style": { "spaceAfter": 20 },
      "slots": [
        { "type": "text", "value": "{{ round($.total, 2) }}", "width": 25,
          "style": { "family": "serif", "size": 16, "bold": false, "color": "#1e1f22", "format": "currency" } },
        { "type": "text", "value": "{{ $.count }}", "width": 25,
          "style": { "family": "serif", "size": 16, "bold": false, "color": "#1e1f22" } },
        { "type": "text", "value": "{{ $.input.expenses.advance }}", "width": 25,
          "style": { "family": "serif", "size": 16, "bold": false, "color": "#1e1f22", "format": "currency" } },
        { "type": "text", "value": "{{ round($.total - $.input.expenses.advance, 2) }}",
          "style": { "family": "serif", "size": 16, "bold": false, "color": "#1e1f22", "format": "currency" } }
      ]
    },
    {
      "type": "image",
      "source": "=chart($.input.expenses.items, $.input.expenses.categories)",
      "fit": "width",
      "alt": "Expenses per category, {{ $.input.expenses.period.label }}",
      "style": { "spaceAfter": 6 }
    }
  ],
  "groups": [
    {
      "name": "category",
      "by": "=@.category",
      "aggregates": { "subtotal": "sum:=@.amount", "receipts": "count" },
      "header": [
        {
          "type": "split",
          "style": { "spaceBefore": 14, "spaceAfter": 2 },
          "slots": [
            { "type": "image", "source": "=swatch(category.key, $.input.expenses.categories)", "fit": "width", "alt": "", "width": 3, "style": { "valign": "middle" } },
            { "type": "text", "value": "{{ category.key }}", "width": 47,
              "style": { "size": 11, "bold": true, "color": "#1e1f22" } },
            { "type": "text",
              "value": [{ "value": "{{ category.receipts }} receipts · " }, { "value": "{{ round(category.subtotal / $.total, 3) }}", "style": { "format": { "kind": "percent", "digits": 1 } } }, { "value": " of the claim" }],
              "style": { "size": 8, "bold": false, "color": "#989a97", "align": "right" } }
          ]
        }
      ]
    }
  ],
  "detail": {
    "header": {
      "style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97",
        "borderBottomWidth": 0.5, "borderBottomStyle": "solid", "borderBottomColor": "#1e1f22" }
    },
    "row": { "style": { "color": "#5e6062" } },
    "columns": [
      { "header": "Date", "value": "{{ @.date }}", "width": 16, "style": { "format": "date" } },
      { "header": "Merchant", "value": "{{ @.merchant }}", "width": 28 },
      { "header": "Description", "value": "{{ @.description }}", "width": 40 },
      { "header": { "value": "Amount", "style": { "align": "right" } }, "value": "{{ @.amount }}", "width": 16, "style": { "align": "right", "format": "currency" } }
    ],
    "total": {
      "style": { "color": "#1e1f22", "align": "right", "paddingTop": 3, "paddingBottom": 0,
        "borderTopWidth": 0.5, "borderTopStyle": "solid", "borderTopColor": "#989a97" },
      "rows": [
        {
          "cells": [
            { "value": "Subtotal {{ category.key }}", "span": 3 },
            { "value": "{{ round(category.subtotal, 2) }}", "style": { "format": "currency" } }
          ]
        }
      ]
    }
  },
  "footer": [
    {
      "type": "grid",
      "columns": [{ "width": 84, "style": { "align": "right" } }, { "style": { "align": "right", "format": "currency" } }],
      "rows": [
        { "style": { "color": "#5e6062", "paddingTop": 16 },
          "slots": [{ "type": "text", "value": "Total expenses, {{ $.count }} receipts" }, { "type": "text", "value": "{{ round($.total, 2) }}" }] },
        { "style": { "color": "#5e6062" },
          "slots": [{ "type": "text", "value": "Less cash advance" }, { "type": "text", "value": "{{ $.input.expenses.advance }}" }] },
        { "style": { "size": 11, "bold": true, "color": "#1e1f22", "paddingTop": 6, "paddingBottom": 4,
            "borderTopWidth": 0.5, "borderTopStyle": "solid", "borderTopColor": "#1e1f22" },
          "slots": [{ "type": "text", "value": "Reimbursement due" }, { "type": "text", "value": "{{ round($.total - $.input.expenses.advance, 2) }}" }] }
      ]
    },
    {
      "type": "grid",
      "columns": [{ "width": 45 }, { "width": 10 }, {}],
      "rows": [
        { "style": { "paddingTop": 34 },
          "slots": [
            { "type": "text", "value": "", "style": { "borderBottomWidth": 0.5, "borderBottomStyle": "solid", "borderBottomColor": "#1e1f22" } },
            { "type": "text", "value": "" },
            { "type": "text", "value": "", "style": { "borderBottomWidth": 0.5, "borderBottomStyle": "solid", "borderBottomColor": "#1e1f22" } }
          ] },
        { "style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97", "paddingTop": 4 },
          "slots": [
            { "type": "text", "value": "Employee · {{ $.input.expenses.employee.name }}" },
            { "type": "text", "value": "" },
            { "type": "text", "value": "Approved · {{ $.input.expenses.approver }}" }
          ] }
      ]
    }
  ],
  "page": {
    "header": [
      {
        "type": "text",
        "value": "Expense report {{ $.input.expenses.number }}, {{ $.input.expenses.employee.name }} · 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": "Amounts as charged, VAT included. Every line has its receipt attached. Approval releases the reimbursement with the next payroll.", "width": 80,
            "style": { "size": 8, "color": "#989a97" } },
          { "type": "text", "value": "Page {{ page.number }} of {{ page.total }}",
            "style": { "size": 8, "color": "#989a97", "align": "right" } }
        ]
      }
    ]
  }
}
```

The sections are a **group** on `=@.category`. The rows sort by category first and by date
second. Each instance
carries its own `subtotal` and its count of `receipts`, and its header reads its share of the claim
as `round(category.subtotal / $.total, 3)` under the `percent` kind. The subtotal row is
`detail.total`, which repeats per category.

The report `footer` settles the claim: the total, less the cash advance, and the reimbursement due
as `round($.total - $.input.expenses.advance, 2)`. Then come two signature lines, one for the
employee and one for the approver. The fine print sits in `page.footer`, on every page’s baseline.

Two images come from your code. The ring is `=chart($.input.expenses.items,
$.input.expenses.categories)` in the header. Each group header opens with
`=swatch(category.key, $.input.expenses.categories)`, a small square in that category’s colour. So
the table carries the chart’s colour key, and a reader never depends on the colour alone.

## The chart

d3-shape’s `pie()` and `arc()` turn the totals into ring segments, and the rest is a string of SVG.
The colours follow the order of `categories` in the data, not the size of each slice. A category
keeps its colour from one claim to the next:

```ts title="chart.ts"
import { arc, pie } from "d3-shape";

type Expense = { category: string; amount: number };

/** One colour per category, in the order the data lists them: a category keeps its colour whatever its rank. */
const COLOURS = [
  "#2a78d6",
  "#eb6834",
  "#1baf7a",
  "#eda100",
  "#e87ba4",
  "#008300",
];

const money = new Intl.NumberFormat("en-IE", {
  style: "currency",
  currency: "EUR",
  maximumFractionDigits: 0,
});
const share = new Intl.NumberFormat("en-IE", {
  style: "percent",
  maximumFractionDigits: 1,
});

const svg = (width: number, height: number, body: string) =>
  new TextEncoder().encode(
    `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" font-family="Helvetica, Arial, sans-serif" font-size="12" fill="#5e6062">${body}</svg>`,
  );

/** Spend per category as a ring, labelled beside it, returned as the SVG's bytes. */
export function chart(expenses: Expense[], categories: string[]): Uint8Array {
  const totals = categories.map((name) =>
    expenses
      .filter((e) => e.category === name)
      .reduce((sum, e) => sum + e.amount, 0),
  );
  const whole = totals.reduce((a, b) => a + b, 0);

  // `sort(null)` keeps the data's order, so the ring reads in the legend's order.
  const slices = pie<number>().sort(null)(totals);
  const ring = arc<(typeof slices)[number]>()
    .innerRadius(62)
    .outerRadius(110)
    .padAngle(0.012);

  const segments = slices
    .map(
      (slice, i) =>
        `<path d="${ring(slice)}" fill="${COLOURS[i]}" stroke="#ffffff" stroke-width="2" />`,
    )
    .join("");

  const legend = categories
    .map((name, i) => {
      const y = 34 + i * 36;
      return `
    <rect x="290" y="${y - 10}" width="12" height="12" rx="2" fill="${COLOURS[i]}" />
    <text x="312" y="${y}" fill="#1e1f22">${name}</text>
    <text x="600" y="${y}" text-anchor="end" fill="#1e1f22">${money.format(totals[i])}</text>
    <text x="680" y="${y}" text-anchor="end">${share.format(totals[i] / whole)}</text>`;
    })
    .join("");

  return svg(
    680,
    240,
    `<g transform="translate(118 120)">${segments}
    <text y="-4" text-anchor="middle" font-size="11">Total</text>
    <text y="16" text-anchor="middle" font-size="16" fill="#1e1f22">${money.format(whole)}</text></g>
    ${legend}`,
  );
}

/** The colour key for one category, as a small square's SVG bytes. */
export function swatch(category: string, categories: string[]): Uint8Array {
  const colour = COLOURS[categories.indexOf(category)] ?? "#989a97";
  // Wider than the square, so the name beside it starts clear of it.
  return svg(
    18,
    12,
    `<rect y="2" width="10" height="10" rx="2" fill="${colour}" />`,
  );
}
```

Six colours is the ceiling. A seventh category is better folded into one of the six than given a
colour a reader can’t tell from its neighbour.

## The data

The claim carries the employee, the approver, the period, and the advance. Each receipt carries its
category, and `categories` lists them once, in the order the ring and the legend read them:

```json title="expense-breakdown.data.json"
{
  "expenses": {
    "number": "EXP-2026-118",
    "company": "Quire Press Ltd.",
    "employee": { "name": "J. Bakker", "number": "10388", "role": "Rights and sales manager" },
    "approver": "M. de Graaf",
    "purpose": "Client visits in Antwerp, Cologne and Brussels",
    "period": { "label": "September 2026", "from": "2026-09-01", "to": "2026-09-30" },
    "submitted": "2026-10-02",
    "advance": 300,
    "categories": ["Travel", "Lodging", "Meals", "Local transport", "Client entertainment", "Supplies"],
    "items": [
      {"date": "2026-09-02", "merchant": "Hollandia Office", "description": "Sample folders and mailers", "category": "Supplies", "amount": 34.75},
      {"date": "2026-09-03", "merchant": "NS International", "description": "Train, Amsterdam to Antwerp, return", "category": "Travel", "amount": 89.4},
      {"date": "2026-09-03", "merchant": "Hotel Scheldekaai", "description": "1 night, Antwerp", "category": "Lodging", "amount": 168},
      {"date": "2026-09-03", "merchant": "Taxi Antwerpen", "description": "Taxi, station to client", "category": "Local transport", "amount": 18.4},
      {"date": "2026-09-03", "merchant": "Brasserie Zuid", "description": "Dinner, Antwerp", "category": "Meals", "amount": 38.5},
      {"date": "2026-09-04", "merchant": "Het Pakhuis", "description": "Dinner with Folio & Sons, 3 guests", "category": "Client entertainment", "amount": 212.6},
      {"date": "2026-09-11", "merchant": "Q-Park", "description": "Parking, Utrecht client visit", "category": "Local transport", "amount": 24},
      {"date": "2026-09-15", "merchant": "Deutsche Bahn", "description": "ICE, Amsterdam to Cologne, return", "category": "Travel", "amount": 142.8},
      {"date": "2026-09-15", "merchant": "Hotel am Domplatz", "description": "2 nights, Cologne", "category": "Lodging", "amount": 254},
      {"date": "2026-09-15", "merchant": "Bahnhof Deli", "description": "Lunch, Cologne", "category": "Meals", "amount": 21.9},
      {"date": "2026-09-16", "merchant": "KVB", "description": "Day ticket, Cologne", "category": "Local transport", "amount": 9.2},
      {"date": "2026-09-16", "merchant": "Brauhaus am Rhein", "description": "Lunch with Rheinland Verlag, 2 guests", "category": "Client entertainment", "amount": 96.4},
      {"date": "2026-09-16", "merchant": "Altstadt Bistro", "description": "Dinner, Cologne", "category": "Meals", "amount": 44.6},
      {"date": "2026-09-22", "merchant": "Copyshop Centraal", "description": "Printed dummies, 6 copies", "category": "Supplies", "amount": 58.5},
      {"date": "2026-09-24", "merchant": "NS International", "description": "Train, Amsterdam to Brussels, return", "category": "Travel", "amount": 96.2},
      {"date": "2026-09-24", "merchant": "Hotel Zavelhof", "description": "1 night, Brussels", "category": "Lodging", "amount": 149},
      {"date": "2026-09-24", "merchant": "STIB", "description": "Metro, 10-ride card", "category": "Local transport", "amount": 16.8},
      {"date": "2026-09-25", "merchant": "Le Petit Sablon", "description": "Lunch, Brussels", "category": "Meals", "amount": 26.3}
    ]
  }
}
```

## 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 ring and the swatches are vector paths. 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/expense-breakdown).*

## Use it

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

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

// `chart` and `swatch` are the functions from chart.ts
const q = quario({ locale: 'en-IE', currency: 'EUR' })   // what `format` presents through
const report = q.report(expenses, { chart, swatch })

const data = await getExpenses('EXP-2026-118')        // your expense claims
const bytes = await report.render(pdf({ page: { size: 'A4', margin: 54 } }), data)

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

## Make it yours

A policy limit beside each category is a limit per category in the data and one more column in the
legend. A section per trip instead of per category is the group’s `by`. A claim with no advance is
an `advance` of `0`, and the footer still reads true. The ring is your code. A bar chart in its place
is a different function under the same name, and the definition doesn’t change.

## FAQ

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

Not with SVG images 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` and a `swatch` that return PNG bytes.

### Why a ring and not a bar chart?

A ring shows part of a whole at a glance, and six categories is few enough to read that way. It’s
weak at comparing two slices of nearly the same size. That’s why each one carries its amount and
share beside it, and why the table underneath holds every figure.

### Where are the other templates?

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