Skip to content

pivotkit-report

pivotkit-report runs a view over data and writes a file: an HTML report to print or save as PDF, CSV, JSON, an Excel workbook (.xlsx) or the chart as SVG. It joins pivotkit-core's steps (the view file, normalize, computePivot, toGrid and its exporters, chartPlan and the chart's marks), so the table and the chart are the ones the report shows on the screen. It runs in Node and in the browser; the command line and the report's own Export menu (HTML report, SVG, print) use it.

npm install pivotkit-report echarts

ECharts (>=6.1.0 <7) is an optional peer dependency: only the SVG and the chart of an HTML report need it, and you pass the module in, so a page uses the copy it already has.

import { writeFile } from "node:fs/promises";
import * as echarts from "echarts";
import { render } from "pivotkit-report";

const view = { rows: ["Region"], values: [{ field: "Units", agg: "sum" }], display: "chart" };
const result = await render(view, rows, { format: "html", echarts, now: "2026-11-13", locale: "de-DE" });
if (!result.ok) throw new Error(result.issues.map((i) => i.message).join("; "));   // a bad view never throws
await writeFile("report.html", result.body);   // Uint8Array, the whole file

render(view, data, options)

view is a view, a view file or a template { name, view }, as an object or JSON text. data is rows, columnar data ({ columns, types, data }, what readFile returns) or encode()'s result. It returns a promise of:

Property What it is
format, mediaType, extension "html", "text/html;charset=utf-8", "html", and so on.
body The file as a Uint8Array; null with strict when the view has an error.
text The body as text, for json, csv, svg and html.
issues The view's issues (VIEW_ISSUES), then the report's own (REPORT_ISSUE_CODES).
ok false when an issue has the severity error.
summary { rows, columns, sourceRows, keptRows, truncated }: the table's body rows and columns, the data's rows, the rows its filters keep, whether a part was cut.
view The normalized view that was rendered.

The whole body is built in memory; the limits keep it bounded.

Options

Option Default What it does
format "html" json, csv, xlsx, svg or html.
locale "en-US" Numbers, dates and the report's words when the view names no locale.
timeZone "UTC" Of rows not encoded yet, and of the "generated" time.
now the clock The "today" of relative date filters and the generated time; fix it for the same bytes on every run.
limits LIMITS pivotkit-core's limits.
trusted false The view is the host's own: its limits instead of outsideLimits().
strict false No body when the view has an error. Without it the normalized view is rendered, as the report on screen does.
labels, texts the locale's language pivotkit-core's texts (totals, value names; by default reportLabels of the view's or locale's language: "Gesamtergebnis", "Summe von Units" for de-DE, the words the report on a page shows) and the report's words around the table (REPORT_TEXTS, English and German).
title, description, source the view file's name and description The report's heading, its line below, and the data's name in the details.
tokens the light theme --pk-* colours, font weights, font sizes and corners over LIGHT_TOKENS, WEIGHT_TOKENS (--pk-weight-normal to --pk-weight-bold, the chart's title and group labels too), SIZE_TOKENS (the HTML report's font sizes: --pk-font-size 14 px, --pk-small, --pk-report-title-size, --pk-print-size and --pk-print-small, theming) and SHAPE_TOKENS (--pk-radius-card, the chart card), and the chart's --pk-chart-title-size, --pk-chart-text-size and --pk-radius-chart in px (else 16 px, 14 px and 4 px): names and plain values only (a comment, a line break and url( anywhere, also in a quoted string, leave the token out), and no function but colours, calc(), min(), max(), clamp() and var(), so a token loads nothing (url() and image-set() are left out). A colour scale or data bars whose view names no colours take --pk-scale-low, --pk-scale-mid (when set) and --pk-scale-high (by default the heat tokens) and --pk-chart-1 in the HTML report's table and in the workbook, when they are #rgb or #rrggbb.
template DEFAULT_TEMPLATE html: a print template.
chart "auto" html: the chart above the table: "auto" when the view shows the chart, true, false.
chartEmbed "image" html: the chart as a data: image, or "inline" SVG.
echarts The ECharts module with its SVG renderer: svg, and html with a chart.
size { width: 800, height: 480 } The chart's size in px, 100 to 8,000.
tableCells 200,000 html: the cells of the table at most; a larger one is cut after the last whole row and says so.
bom true csv: start with a byte order mark, so Excel reads the file as UTF-8. false for a program that reads the first header cell by its name.

Any other option name is refused (INVALID_OPTION, with the closest name), so a misspelt one is not ignored.

Library and command line

render() pivotkit render
A view with an error ok: false, body rendered exit 1, nothing written (--lenient writes it, still exit 1)
A warning in issues stderr; exit 1 with --strict
Limits for the view outsideLimits() unless trusted outsideLimits()
A limit, a refused template throws PivotkitError exit 2

Errors

A bad view never throws: its issues come back. PivotkitError is thrown for a wrong argument (INVALID_ARGUMENT), a wrong option (INVALID_OPTION with details.option: an unknown format, a size out of range, an unknown option, a chart, chartEmbed, bom, strict or trusted of a value it does not take, an SVG or chart: true without echarts, a now that is no date), a limit (LIMIT_EXCEEDED) and a refused template (INVALID_TEMPLATE, see print template). The report's own issues are warnings: chartUnavailable (a view that shows the chart, rendered as HTML with chart "auto" and without echarts: the table is there, the chart is not; pass strict and check ok, or pass chart: true, when the chart must be there), tableCut, chartCut (the first REPORT_LIMITS.categories categories, 50) and viewFile (an error: the input is no view).

The formats

Format Holds
json pivotkit-core's toJson: the header texts per column and each row's labels and values.
csv toCsvBytes: UTF-8 with a byte order mark, the locale's separator, formulas neutralized.
xlsx toXlsx: every number with its decimals and formats, the sheet named after the title.
svg The chart as the screen draws it (chartPlan, the theme's colours, the axis laid out for the size), the first 50 categories.
html The report below.

The HTML report

The default template writes a page that loads and runs nothing: its Content-Security-Policy is the first element of <head> (default-src 'none'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'), the chart is an image (data:image/svg+xml), and there is no script.

  • The table as the screen shows it: merged headers and row labels, totals and subtotals shaded and bold, colour scales and data bars, a collapsed group marked "+". Numbers as the table writes them. No sorting buttons, toggles or drill-down: a report is read, not used.
  • The chart above the table when the view shows the chart (chart: "auto"), kept whole on a page.
  • The details: the source, the data's rows, the filters in words and the time it was generated.
  • On screen light or dark after the reader's system; on paper always light, with 12 mm margins, the header row on every page, rows never split, landscape from 9 columns and a smaller font (7 pt) from 17. A table wider than a landscape page is cut by the browser: split the view, or export Excel.

The chart as SVG

ECharts' server-side renderer (echarts.init(null, null, { renderer: "svg", ssr: true }), then renderToSVGString()) draws it; cleanSvg then checks every element and attribute: only the elements a chart draws, no event attribute, no link but to a fragment or a data: image, no unescaped text, control characters left out, every style attribute turned into presentation attributes (so the SVG also shows under a strict Content-Security-Policy). Anything else is refused with PivotkitError (details.reason). Text from the data is never trusted to the renderer's escaping alone.

Operations

pivotkit-report/operations is the one table of what pivotkit's front ends without a browser offer: the command line, the MCP server and the REST service. Each entry of OPERATIONS has its name, title and description, its input and output as JSON Schema (draft 2020-12, no $schema key), its HTTP route under the REST service's /api/v1 (http), the CLI command it is (cli), what it needs from its front end (needs), annotations, and run(input, env):

Operation CLI HTTP What it does
list_sources - GET /sources The sources the caller may use (env.sources)
describe_source describe <data> POST /sources/describe describeSource with example views; values only with items
suggest_view describe <data> --suggest POST /views/suggest A draft view file from the source's fields (suggestLayout, viewOfLayout), marked Draft:, with its picks; a layout replaces the parts it gives
get_view_guide - GET /views/guide VIEW_GUIDE: a view's format, its vocabulary from CATALOG, examples
validate_view validate POST /views/validate Findings and the fields the view uses; with a layout instead of a view, also the view it makes
describe_view describe <view> POST /views/describe describeView in parts, with findings
render_view render POST /views/render render(); the file comes back as file, the front end puts it somewhere; a layout instead of a view (the CLI's layout flags), with the view it made in the result
create_link - POST /views/link A link into the source's page (env.link)
draft_schedule - POST /schedules/draft A draft report document (pivotkit-report-draft, version 0) for a person to approve
import { OPERATIONS } from "pivotkit-report/operations";
const env = { data: async (source) => ({ data: encode(rows), name: source }) };
const { result, file } = await OPERATIONS.render_view.run({ source: "sales", view, format: "csv" }, env);

A layout is a view by field names, pivotkit-core's viewFromLayout input: { rows, cols, values: [{ field, agg }], groups: [{ field, by, size }], filters, chart, map }, checked by its JSON Schema (no other keys, at most 64 entries per list). viewOfLayout(description, layout, { suggest, locale }) is the one way the operations and the command line turn it into a view: with suggest, suggestLayout's picks first and the layout's parts over them. Its notes become findings by LAYOUT_NOTE_SEVERITY: noSuggestion (nothing to group by) an error, dateGrouped an info, unusedGroup and unusedMap warnings. A request with both a view and a layout, or neither, is INVALID_INPUT.

env gives what the operations need: data(source) (encoded data, or only { name, type } fields for validate and describe), sources(), link(source), template(source), echarts, limits, locale, timeZone, now, and renderOptions for a front end's own render options. A view with errors is no error (ok or valid false, with findings); what a front end must handle is OperationError with a code of OPERATION_ERRORS and its HTTP status. findingOf turns a pivotkit-core issue into a finding (cleaned, cut at 200 characters); exampleViews(fields) builds views from a data set's fields.

Exports

render, RENDER_FORMATS, REPORT_LIMITS (template, chartWidth, chartHeight, tableCells, categories), REPORT_ISSUE_CODES, CHART_SIZE, cleanSvg, DEFAULT_TEMPLATE, REPORT_CSP, REPORT_TEXTS, LIGHT_TOKENS, DARK_TOKENS, WEIGHT_TOKENS, SHAPE_TOKENS, SIZE_TOKENS; pivotkit-report/template: parseTemplate, fillTemplate, escapeHtml, PLACEHOLDERS, ATTRIBUTES, TEMPLATE_LIMIT (print template); pivotkit-report/operations: OPERATIONS, OPERATION_NAMES, OPERATION_ERRORS, OperationError, VIEW_GUIDE, exampleViews, findingOf, findingText, FINDING_TEXT, viewOfLayout, LAYOUT_NOTE_SEVERITY, checkInput (an operation's input against its JSON Schema) (operations).