Escape
HomeTemplatesSummary report

Summary report template for JavaScript

A summary report answers one question at the top and shows its working underneath. Here the question is how the quarter went. Revenue, orders, the average and the largest, then every order by region with a subtotal per section and the region’s share of the whole. What makes it awkward to build is that the sections come from the data. You don’t know how many there are until you run it, and a quarter of orders runs past a page. This is a ready-made quario summary report — copy it, point it at your ledger, and render to HTML, PDF, Excel, or Word.

The template

The whole report is a definition — data you can version and review. The headline figures, the sections, the subtotals, the grand total and the page furniture are all in here. Nothing about this document lives in code. This is the exact file the preview below renders:

summary-report.report.json
{
  "data": "$.sales.orders[*]",
  "sort": [{ "by": "[email protected]" }, { "by": "[email protected]" }],
  "aggregates": {
    "revenue": "sum:[email protected]",
    "orders": "count",
    "average": "avg:[email protected]",
    "largest": "max:[email protected]"
  },
  "style": { "family": "sans", "size": 10 },
  "header": [
    {
      "type": "split",
      "slots": [
        { "type": "text", "value": "Sales summary, {{ $.input.sales.period.label }}", "width": 60,
          "style": { "family": "serif", "size": 19, "bold": true, "color": "#1e1f22" } },
        { "type": "text", "value": [{ "value": "{{ $.input.sales.period.from }}" }, { "value": " to " }, { "value": "{{ $.input.sales.period.to }}" }],
          "style": { "size": 8, "color": "#989a97", "align": "right", "format": "date" } }
      ]
    },
    {
      "type": "split",
      "slots": [
        { "type": "text", "value": "{{ $.input.sales.company }}", "width": 60,
          "style": { "size": 8, "color": "#989a97" } },
        { "type": "text", "value": [{ "value": "Prepared " }, { "value": "{{ $.input.sales.prepared }}" }],
          "style": { "size": 8, "color": "#989a97", "align": "right", "format": "date" } }
      ]
    },
    {
      "type": "split",
      "style": { "spaceBefore": 28 },
      "slots": [
        { "type": "text", "value": "Revenue", "width": 25,
          "style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97" } },
        { "type": "text", "value": "Orders", "width": 25,
          "style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97" } },
        { "type": "text", "value": "Average order", "width": 25,
          "style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97" } },
        { "type": "text", "value": "Largest order",
          "style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97" } }
      ]
    },
    {
      "type": "split",
      "style": { "spaceAfter": 20 },
      "slots": [
        { "type": "text", "value": "{{ round($.revenue, 2) }}", "width": 25,
          "style": { "family": "serif", "size": 16, "color": "#1e1f22", "format": { "kind": "currency", "digits": 0 } } },
        { "type": "text", "value": "{{ $.orders }}", "width": 25,
          "style": { "family": "serif", "size": 16, "color": "#1e1f22" } },
        { "type": "text", "value": "{{ round($.average, 2) }}", "width": 25,
          "style": { "family": "serif", "size": 16, "color": "#1e1f22", "format": { "kind": "currency", "digits": 0 } } },
        { "type": "text", "value": "{{ $.largest }}",
          "style": { "family": "serif", "size": 16, "color": "#1e1f22", "format": { "kind": "currency", "digits": 0 } } }
      ]
    }
  ],
  "groups": [
    {
      "name": "region",
      "by": "[email protected]",
      "aggregates": { "subtotal": "sum:[email protected]", "orders": "count" },
      "header": [
        {
          "type": "split",
          "style": { "spaceBefore": 16, "spaceAfter": 2 },
          "slots": [
            { "type": "text", "value": "{{ region.key }}", "width": 50,
              "style": { "size": 11, "bold": true, "color": "#1e1f22" } },
            { "type": "text", "value": "{{ region.orders }} orders · {{ round(region.subtotal / $.revenue * 100, 1) }}% of revenue",
              "style": { "size": 8, "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": "Order", "value": "{{ @.number }}", "width": 16, "style": { "family": "mono", "size": 8, "color": "#989a97" } },
      { "header": "Customer", "value": "{{ @.customer }}", "width": 46 },
      { "header": { "value": "Amount", "style": { "align": "right" } }, "value": "{{ @.amount }}", "width": 22, "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 {{ region.key }}", "span": 3 },
            { "value": "{{ round(region.subtotal, 2) }}", "style": { "format": "currency" } }
          ]
        }
      ]
    }
  },
  "footer": [
    {
      "type": "split",
      "style": { "spaceBefore": 20, "paddingTop": 6,
        "borderTopWidth": 0.5, "borderTopStyle": "solid", "borderTopColor": "#1e1f22" },
      "slots": [
        { "type": "text", "value": "Total revenue, {{ $.orders }} orders", "width": 70,
          "style": { "bold": true, "color": "#1e1f22" } },
        { "type": "text", "value": "{{ round($.revenue, 2) }}",
          "style": { "bold": true, "color": "#1e1f22", "align": "right", "format": "currency" } }
      ]
    },
    { "type": "text", "value": "Amounts exclude VAT. Source: {{ $.input.sales.source }}.",
      "style": { "size": 8, "color": "#989a97", "spaceBefore": 8 } }
  ],
  "page": {
    "header": [
      {
        "type": "text",
        "value": "Sales summary, {{ $.input.sales.period.label }} · 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.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 four report aggregates — sum, count, avg and max over the same rows the table lists — read from $ in the header. Each sits in a slot of its own so its format applies to that number alone. { "kind": "currency", "digits": 0 } presents the money at headline size without cents. The order count carries no format and prints as the integer it is.

The sections are a group on [email protected]. Each instance carries its own aggregates — region.subtotal and region.orders. Its header reads them beside region.key, the region name, and beside the report’s $.revenue. That’s how the share is one expression: round(region.subtotal / $.revenue * 100, 1). A group instance doesn’t know it’s the second of three. It knows its own rows and the document’s totals, and that’s enough.

The subtotal is detail.total. Inside a group the table repeats per instance, so its total block renders once per section reading region.subtotal. That’s a subtotal row in the section’s own columns rather than a split aligned by eye. The grand total is the report footer, which comes after the last section and reads $.revenue and $.orders. Both wrap in round(…, 2). A sum of exact amounts can land on an inexact float, and where a page presents the number the workbook stores it.

The page bands are what make it a report rather than a list. page.header declares "visible": "=page.number > 1", so the continued line appears from the second page on, and page.footer carries Page {{ page.number }} of {{ page.total }}. The column labels repeat on every page the table runs onto, and nothing asks them to.

The data

A definition renders nothing on its own — you hand it a document. This is the sample the preview below uses. $.sales.orders[*] in the definition selects its rows and $.input reads the period and the company. The rows arrive in date order and the report’s sort puts them by region first:

summary-report.data.json
{
  "sales": {
    "company": "Quire Press Ltd.",
    "period": { "label": "Q3 2026", "from": "2026-07-01", "to": "2026-09-30" },
    "prepared": "2026-10-02",
    "source": "sales ledger, invoices issued in the period",
    "orders": [
      {"date": "2026-07-02", "number": "2026-1029", "customer": "Folio & Sons", "region": "Ireland & UK", "amount": 1600},
      {"date": "2026-07-03", "number": "2026-1030", "customer": "Studio Halfweg", "region": "Benelux", "amount": 500},
      {"date": "2026-07-08", "number": "2026-1031", "customer": "Boekhandel Van Rossem", "region": "Benelux", "amount": 240},
      {"date": "2026-07-10", "number": "2026-1032", "customer": "Museum of Print, Antwerp", "region": "Benelux", "amount": 3200},
      {"date": "2026-07-15", "number": "2026-1033", "customer": "Folio & Sons", "region": "Ireland & UK", "amount": 40},
      {"date": "2026-07-17", "number": "2026-1034", "customer": "Lindqvist Förlag", "region": "Nordics", "amount": 390},
      {"date": "2026-07-22", "number": "2026-1035", "customer": "Hatch & Lane, Bristol", "region": "Ireland & UK", "amount": 500},
      {"date": "2026-07-24", "number": "2026-1036", "customer": "Studio Halfweg", "region": "Benelux", "amount": 300},
      {"date": "2026-07-28", "number": "2026-1037", "customer": "Fjeldstad Papir", "region": "Nordics", "amount": 950},
      {"date": "2026-07-30", "number": "2026-1038", "customer": "Boekhandel Van Rossem", "region": "Benelux", "amount": 1000},
      {"date": "2026-08-04", "number": "2026-1039", "customer": "Folio & Sons", "region": "Ireland & UK", "amount": 150},
      {"date": "2026-08-06", "number": "2026-1040", "customer": "Lindqvist Förlag", "region": "Nordics", "amount": 1280},
      {"date": "2026-08-11", "number": "2026-1041", "customer": "Galerie Noord", "region": "Benelux", "amount": 480},
      {"date": "2026-08-13", "number": "2026-1042", "customer": "Folio & Sons", "region": "Ireland & UK", "amount": 3683},
      {"date": "2026-08-14", "number": "2026-1043", "customer": "Hatch & Lane, Bristol", "region": "Ireland & UK", "amount": 48},
      {"date": "2026-08-18", "number": "2026-1044", "customer": "Museum of Print, Antwerp", "region": "Benelux", "amount": 450},
      {"date": "2026-08-19", "number": "2026-1045", "customer": "Studio Halfweg", "region": "Benelux", "amount": 800},
      {"date": "2026-08-21", "number": "2026-1046", "customer": "Fjeldstad Papir", "region": "Nordics", "amount": 330},
      {"date": "2026-08-25", "number": "2026-1047", "customer": "Galerie Noord", "region": "Benelux", "amount": 2150},
      {"date": "2026-08-26", "number": "2026-1048", "customer": "Folio & Sons", "region": "Ireland & UK", "amount": 800},
      {"date": "2026-08-27", "number": "2026-1049", "customer": "Boekhandel Van Rossem", "region": "Benelux", "amount": 330},
      {"date": "2026-09-01", "number": "2026-1050", "customer": "Lindqvist Förlag", "region": "Nordics", "amount": 2400},
      {"date": "2026-09-03", "number": "2026-1051", "customer": "Hatch & Lane, Bristol", "region": "Ireland & UK", "amount": 1125},
      {"date": "2026-09-08", "number": "2026-1052", "customer": "Studio Halfweg", "region": "Benelux", "amount": 620},
      {"date": "2026-09-09", "number": "2026-1053", "customer": "Museum of Print, Antwerp", "region": "Benelux", "amount": 1880},
      {"date": "2026-09-11", "number": "2026-1054", "customer": "Fjeldstad Papir", "region": "Nordics", "amount": 175},
      {"date": "2026-09-15", "number": "2026-1055", "customer": "Folio & Sons", "region": "Ireland & UK", "amount": 2160},
      {"date": "2026-09-17", "number": "2026-1056", "customer": "Galerie Noord", "region": "Benelux", "amount": 725},
      {"date": "2026-09-22", "number": "2026-1057", "customer": "Boekhandel Van Rossem", "region": "Benelux", "amount": 410},
      {"date": "2026-09-23", "number": "2026-1058", "customer": "Lindqvist Förlag", "region": "Nordics", "amount": 640},
      {"date": "2026-09-25", "number": "2026-1059", "customer": "Hatch & Lane, Bristol", "region": "Ireland & UK", "amount": 275},
      {"date": "2026-09-29", "number": "2026-1060", "customer": "Studio Halfweg", "region": "Benelux", "amount": 1440}
    ]
  }
}

What it renders

This isn’t a screenshot. quario compiles the definition and data above, and @quario/viewer renders them in your browser, right here. The PDF button hands you the same report as real @quario/pdf bytes, no browser rendering involved. The sheet is the layout’s own pages painted on a canvas. The page break, the continuation header, and the page numbers you see are the ones the PDF carries. @quario/html renders an unpaginated fragment and ignores page bands, so an HTML render has the sections and the totals but no page numbers. The DOCX button hands the same definition to @quario/docx, where Word paginates and resolves the faces itself. The page numbers survive that trip as fields Word recomputes:

Rendering live with @quario/viewerA4

Use it

shell
npm install quario @quario/pdf
summary-report.js
import { writeFileSync } from 'node:fs'
import { quario } from 'quario'
import { pdf } from '@quario/pdf'
import summary from './summary-report.report.json' with { type: 'json' }

const q = quario({ locale: 'en-IE', currency: 'EUR' })   // what `format` presents through
const report = q.report(summary)

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

writeFileSync('sales-summary.pdf', bytes)

The same compiled report renders to a workbook with nothing to change — see JSON to Excel. Every section lands with its subtotal row, and every amount is a number.

Make it yours

Because the report is declarative, changing it means changing data. A second level — region, then customer — is a second entry in groups with its own aggregates and its own header. A top-ten is a sort on the amount and "take": 10 on the report or on a group. One region per page is "break": "page" on the group. Swap the KPI row for the ones your reader asks about, and the sections follow whatever field you group on. All of it diffs in a pull request, and the same file renders identically in Node, the browser, and at the edge. It renders without eval, so customer names from the ledger never become code. Security →

FAQ

Can I generate the report as a PDF?

Yes — @quario/pdf renders the same definition straight to PDF bytes, with no browser anywhere, and it’s the target that draws the page bands. Full example →

Can I export the report to Excel?

Yes — @quario/xlsx renders the same definition to a workbook, with nothing to change. Each section is its own block of rows with a bold header. The subtotal rows carry "format": "currency" so they’re real numbers, and the Date column is a typed date that sorts and filters as one. Full example →

Where do the sections come from?

From the data. A group opens one instance per distinct key it meets. A fourth region in next quarter’s ledger is a fourth section with no change to the definition. Sort the rows by the group field, or the same key opens more than once.

Can I chart it?

Not from the definition. quario lays out text, tables and images, and a chart is none of those. Render the chart yourself and hand it to an image item as bytes, or export the workbook and chart it there.

Where are the other templates?

Browse the template gallery → — statements, certificates, invoices, and more.

Fold a quarter of orders into this report.

npm install quario
Getting started
© 2026 quario · KvK 61815977