pivotkit-report 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,88 @@
1
+ # Changelog
2
+
3
+ All notable changes to `pivotkit-report` are documented in this file. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this package adheres to
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.2.0] - 2026-11-14
10
+
11
+ ### Upgrading from 0.1
12
+
13
+ What a script that reads the reports checks; the [upgrade page](https://pivotkit.eize.dev/getting-started/upgrading/#report-words) has the details.
14
+
15
+ - Without `labels`, the report's words follow its locale (German for `de-DE`) and, in English, the
16
+ report on a page: "Yes"/"No" instead of "true"/"false", "Distinct count" instead of "Count
17
+ Unique". `labels: DEFAULT_LABELS` keeps the engine's words.
18
+ - The HTML report's details say "Rows: 8" (was "Rows: 8 rows").
19
+
20
+ ### Added
21
+
22
+ - Operations: `suggest_view` (a draft view file from a source's fields, with its `picks`), and a
23
+ `layout` (a view by field names) instead of a `view` for `render_view` and `validate_view`, with the
24
+ `view` it made in the result; `viewOfLayout` and `LAYOUT_NOTE_SEVERITY`, the one way the operations
25
+ and the command line make a view from a layout ([#253](https://github.com/eizedev/pivotkit/issues/253)).
26
+ - `pivotkit-report/operations`: the one table of operations the command line, the MCP server and a
27
+ REST service share (`OPERATIONS`: list_sources, describe_source, get_view_guide, validate_view,
28
+ describe_view, render_view, create_link, draft_schedule), each with its description, input and
29
+ output JSON Schema, HTTP route and `run(input, env)`; `VIEW_GUIDE`, `exampleViews`, `findingOf`,
30
+ `OperationError` with `OPERATION_ERRORS`; every `run` checks its input against its schema first
31
+ (`checkInput`, `INVALID_INPUT` naming the place) ([#165](https://github.com/eizedev/pivotkit/issues/165)).
32
+ - `summary.keptRows`: the rows the view's filters keep (the CLI warns when it is 0).
33
+
34
+ ### Changed
35
+
36
+ - The HTML report's details give the data's rows as a number ("Rows: 8"; it said "Rows: 8 rows",
37
+ "Zeilen: 8 Zeilen"); the template's `{{rows}}` stays "8 rows".
38
+ - Without `labels`, `render()` writes the report's words in the language of its locale
39
+ (pivotkit-core's `reportLabels`): German for a German locale ("Gesamtergebnis", "Summe von Units",
40
+ where it wrote "Grand Total" next to German numbers), and in English the words the report on a page
41
+ shows ("Distinct count", yes/no as "Yes" and "No"). Pass `labels: DEFAULT_LABELS` for the engine's
42
+ own words.
43
+ - Depends on `pivotkit-core` `^0.6.0` (was `^0.5.0`).
44
+
45
+ ## [0.1.0] - 2026-11-13
46
+
47
+ First release.
48
+
49
+ ### Added
50
+
51
+ - `SIZE_TOKENS`: the HTML report's font sizes as tokens, `--pk-font-size` (14 px), `--pk-small`
52
+ (the details, at least 14 px, were 12 px), `--pk-report-title-size` (20 px at 14 px),
53
+ `--pk-print-size` (10 pt) and `--pk-print-small` (7 pt); a host's `tokens` change them, and the
54
+ chart's text with `--pk-chart-text-size` in px (default 14 px, was ECharts' 12 px). A token's value
55
+ may hold a `calc()`'s `*`, never a comment ([#220](https://github.com/eizedev/pivotkit/issues/220), [#221](https://github.com/eizedev/pivotkit/issues/221)).
56
+ - `SHAPE_TOKENS`: the corner of the HTML report's chart card, `--pk-radius-card` (8 px, pivotkit-react's
57
+ value); a host's `tokens` change it, as the chart's title size (`--pk-chart-title-size`) and its marks'
58
+ corners (`--pk-radius-chart`) in px ([#216](https://github.com/eizedev/pivotkit/issues/216)).
59
+ - A colour scale or data bars without colours of the view's own take the host's `tokens` in the HTML
60
+ report's table and in the workbook (`--pk-scale-low`, `--pk-scale-mid`, `--pk-scale-high`,
61
+ `--pk-chart-1`), as the report on screen does; by default the light theme's heat colours, light
62
+ for low and strong for high ([#212](https://github.com/eizedev/pivotkit/issues/212)).
63
+ - `WEIGHT_TOKENS`: pivotkit-react's font weight scale (`--pk-weight-normal`, `-medium`, `-strong`,
64
+ `-bold`); the report's stylesheet and its chart take their weights from these tokens, so a host's
65
+ `tokens` change them too ([#201](https://github.com/eizedev/pivotkit/issues/201)).
66
+ - `render(view, data, options)`: a view over rows, columnar or encoded data as JSON, CSV, XLSX, SVG or
67
+ a printable HTML report, with the view's issues, `ok` and a summary; a bad view never throws, a
68
+ limit or a refused template throws `PivotkitError` ([#165](https://github.com/eizedev/pivotkit/issues/165)).
69
+ - The HTML report: the table as the screen shows it (merged headers, totals and subtotals, colour
70
+ scales and data bars, collapsed groups), the chart as an image, the details (source, rows, filters,
71
+ generated), print rules (the header on every page, rows not split, landscape for a wide table) and
72
+ a Content-Security-Policy that loads and runs nothing.
73
+ - The print template (`pivotkit-report/template`): `{{placeholders}}` from a fixed table, allowed in
74
+ text and a few attributes only, checked before anything is rendered (`INVALID_TEMPLATE`); the
75
+ `content` of a `<meta>` takes one only for `name="description"` without `http-equiv`.
76
+ - A host's `tokens` are left out when a value holds a line break or `url(` (also in a quoted string,
77
+ which a line break would end), a comment or a function that is not a colour, `calc()`, `min()`,
78
+ `max()`, `clamp()` or `var()`. The HTML report and the SVG leave out bidi controls, as the CSV does (in the SVG also as numeric references).
79
+ - The chart as SVG through ECharts' server-side renderer, checked and cleaned (`cleanSvg`): no
80
+ script, event attribute, link or style attribute; attribute names plain names, namespaces only
81
+ SVG's and xlink's.
82
+ - Options are checked: an unknown one is refused with the closest name, an option of a few values
83
+ (`chart`, `chartEmbed`, `bom`, `strict`, `trusted`) takes only those, and `chart: true` without
84
+ `echarts` is an error. `bom: false` writes CSV without a byte order mark.
85
+
86
+ [Unreleased]: https://github.com/eizedev/pivotkit/compare/pivotkit-report@0.2.0...HEAD
87
+ [0.2.0]: https://github.com/eizedev/pivotkit/compare/pivotkit-report@0.1.0...pivotkit-report@0.2.0
88
+ [0.1.0]: https://github.com/eizedev/pivotkit/releases/tag/pivotkit-report@0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 eizedev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,45 @@
1
- # Temporary Holding Version
1
+ # pivotkit-report
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Render [pivotkit](https://pivotkit.eize.dev/) views without a browser: a view over rows as an HTML
4
+ report to print or save as PDF, CSV, JSON, an Excel workbook (.xlsx) or the chart as SVG. The same
5
+ table and chart as the report on the screen, from pivotkit-core's engine and exporters; the print
6
+ template is HTML with `{{placeholders}}`, and everything filled into it is escaped. Runs in Node and
7
+ in the browser.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install pivotkit-report echarts
13
+ ```
14
+
15
+ ES modules for Node 22 or later and browsers from Chrome 111, Firefox 113 and Safari 16.4. It depends
16
+ on pivotkit-core (a caret range on the minor version it was released with, see its `package.json`). ECharts (`>=6.1.0 <7`) is an optional peer dependency: only
17
+ the SVG and the chart of an HTML report need it, passed in as `options.echarts`. TypeScript
18
+ declarations are included.
19
+
20
+ ## Example
21
+
22
+ ```js
23
+ import { writeFile } from "node:fs/promises";
24
+ import * as echarts from "echarts";
25
+ import { render } from "pivotkit-report";
26
+
27
+ const view = { rows: ["Region"], values: [{ field: "Units", agg: "sum" }], display: "chart" };
28
+ const result = await render(view, rows, { format: "html", echarts, now: "2026-11-13" });
29
+ if (!result.ok) console.warn(result.issues); // a bad view never throws: its issues come back
30
+ await writeFile("report.html", result.body); // Uint8Array; result.text for the text formats
31
+ ```
32
+
33
+ ## Where in the docs
34
+
35
+ | Topic | Page |
36
+ | --- | --- |
37
+ | Getting started: a complete script, rows in code and data files | [Getting started with reports](https://pivotkit.eize.dev/getting-started/report/) |
38
+ | `render()`, its options, formats, limits and errors | [pivotkit-report reference](https://pivotkit.eize.dev/reference/report/) |
39
+ | The print template and its placeholders | [Print template](https://pivotkit.eize.dev/reference/print-template/) |
40
+ | The command line, the REST service and the MCP server on top of it | [pivotkit-cli](https://pivotkit.eize.dev/reference/cli/), [pivotkit-server](https://pivotkit.eize.dev/reference/rest/), [pivotkit-mcp](https://pivotkit.eize.dev/reference/mcp/) |
41
+ | Changes | [CHANGELOG](https://github.com/eizedev/pivotkit/blob/main/packages/pivotkit-report/CHANGELOG.md) |
42
+
43
+ ## License
44
+
45
+ [MIT](https://github.com/eizedev/pivotkit/blob/main/LICENSE)
package/package.json CHANGED
@@ -1,6 +1,80 @@
1
1
  {
2
2
  "name": "pivotkit-report",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Render pivotkit views without a browser: JSON, CSV, XLSX, SVG charts and a printable HTML report from a template, in Node and in the browser.",
5
+ "license": "MIT",
6
+ "author": "eizedev <oss@eize.dev>",
7
+ "homepage": "https://pivotkit.eize.dev/reference/report/",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/eizedev/pivotkit.git",
11
+ "directory": "packages/pivotkit-report"
12
+ },
13
+ "bugs": "https://github.com/eizedev/pivotkit/issues",
14
+ "keywords": [
15
+ "pivotkit",
16
+ "pivot-table",
17
+ "reporting",
18
+ "report",
19
+ "print",
20
+ "svg",
21
+ "echarts",
22
+ "excel-export",
23
+ "pivot-grid",
24
+ "pivottable",
25
+ "excel"
26
+ ],
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "exports": {
30
+ ".": {
31
+ "types": "./types/index.d.ts",
32
+ "default": "./src/index.js"
33
+ },
34
+ "./template": {
35
+ "types": "./types/template.d.ts",
36
+ "default": "./src/template.js"
37
+ },
38
+ "./operations": {
39
+ "types": "./types/operations.d.ts",
40
+ "default": "./src/operations.js"
41
+ },
42
+ "./package.json": "./package.json"
43
+ },
44
+ "files": [
45
+ "src",
46
+ "types",
47
+ "README.md",
48
+ "CHANGELOG.md",
49
+ "LICENSE"
50
+ ],
51
+ "engines": {
52
+ "node": ">=22"
53
+ },
54
+ "scripts": {
55
+ "test": "node --test \"test/*.test.js\"",
56
+ "coverage": "node --test --experimental-test-coverage --test-coverage-include=\"src/**\" \"test/*.test.js\"",
57
+ "types": "tsc -p .",
58
+ "test:types": "tsc -p test/types",
59
+ "prepack": "npm run types && node -e \"require('node:fs').copyFileSync('../../LICENSE', 'LICENSE')\""
60
+ },
61
+ "peerDependencies": {
62
+ "echarts": ">=6.1.0 <7"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "echarts": {
66
+ "optional": true
67
+ }
68
+ },
69
+ "dependencies": {
70
+ "pivotkit-core": "^0.6.0"
71
+ },
72
+ "devDependencies": {
73
+ "echarts": "6.1.0",
74
+ "pivotkit-xlsx": "0.3.0"
75
+ },
76
+ "types": "./types/index.d.ts",
77
+ "publishConfig": {
78
+ "access": "public"
79
+ }
80
+ }
@@ -0,0 +1,33 @@
1
+ /*
2
+ * The default print template: a document without script, its Content-Security-Policy first in <head>
3
+ * (nothing it shows can load or run anything: styles inline, images as data: addresses only), the
4
+ * title, the chart (an image), the table and the details below it. A host or `--template` replaces it
5
+ * with its own; docs/reference/print-template.md lists the placeholders.
6
+ */
7
+
8
+ export const REPORT_CSP = "default-src 'none'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'";
9
+
10
+ export const DEFAULT_TEMPLATE = `<!doctype html>
11
+ <html lang="{{lang}}" dir="{{dir}}">
12
+ <head>
13
+ <meta http-equiv="Content-Security-Policy" content="${REPORT_CSP}">
14
+ <meta charset="utf-8">
15
+ <meta name="viewport" content="width=device-width, initial-scale=1">
16
+ <meta name="generator" content="pivotkit">
17
+ <meta name="description" content="{{summary}}">
18
+ <title>{{title}}</title>
19
+ {{style}}
20
+ </head>
21
+ <body>
22
+ <main class="pk-report">
23
+ <header>
24
+ <h1>{{title}}</h1>
25
+ <p class="pk-report-description">{{description}}</p>
26
+ </header>
27
+ {{chart}}
28
+ {{table}}
29
+ {{details}}
30
+ </main>
31
+ </body>
32
+ </html>
33
+ `;
@@ -0,0 +1,104 @@
1
+ /*
2
+ * The pivot table as HTML, written from pivotkit-core's grid (toGrid): what the Excel export keeps,
3
+ * as the table shows it. Merged headers and row labels as colspan and rowspan, totals and subtotals
4
+ * as classes (bold, shaded), colour scales and data bars as classes with their colours in a rule of
5
+ * their own (no `style` attribute, so a page with a strict Content-Security-Policy can show it), a
6
+ * collapsed group marked. Every text is escaped; every class comes from the fixed names below; a
7
+ * colour is written only when it is a hex colour.
8
+ *
9
+ * Left out on purpose: the header's sort buttons, the toggles and drill-down (nothing runs in a
10
+ * report). A table over `maxCells` cells is cut after the last whole row that fits: `cut` says how
11
+ * many body rows it shows of how many.
12
+ */
13
+
14
+ import { DEFAULT_THEME } from "pivotkit-core";
15
+ import { scaleColor } from "pivotkit-core/export";
16
+ import { escapeHtml } from "./template.js";
17
+
18
+ const HEX = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
19
+
20
+ /** The class of a total: subtotal or grand total. */
21
+ const totalClass = (total) => (total === 2 ? " pk-grand" : total === 1 ? " pk-sub" : "");
22
+
23
+ /**
24
+ * @typedef {object} HtmlTable
25
+ * @property {string} html the <table>
26
+ * @property {string} css the rules of the conditional colours, per cell class
27
+ * @property {{ rows: number, of: number } | null} cut body rows shown of all, when the table was cut
28
+ */
29
+
30
+ /**
31
+ * @param {import("pivotkit-core").Grid} grid
32
+ * @param {{ maxCells: number, caption?: string, colorScale?: string[], dataBar?: string }} options
33
+ * colorScale, dataBar: the colours of a scale or a bar whose view names none (the host's tokens,
34
+ * tokens.js conditionalColors; else pivotkit-core's DEFAULT_THEME)
35
+ * @returns {HtmlTable}
36
+ */
37
+ export function htmlTable(grid, { maxCells, caption, colorScale = [...DEFAULT_THEME.colorScale], dataBar = DEFAULT_THEME.dataBar }) {
38
+ const width = Math.max(1, grid.columnCount);
39
+ const bodyCount = grid.rows.length - grid.headerRows;
40
+ // the header always; then whole body rows while the cells fit
41
+ const fit = Math.max(0, Math.min(bodyCount, Math.floor(maxCells / width) - grid.headerRows));
42
+ const rules = [];
43
+ let ruleId = 0;
44
+ const colorRule = (style) => {
45
+ if (!style) {
46
+ return "";
47
+ }
48
+ if (style.bar !== undefined) {
49
+ const color = style.color && HEX.test(style.color) ? style.color : dataBar;
50
+ const bar = Math.round(Math.max(0, Math.min(1, style.bar)) * 1000) / 10;
51
+ const name = `pk-c${ruleId++}`;
52
+ rules.push(`.${name}{--pk-bar:${bar}%;--pk-bar-color:${HEX.test(color) ? color : DEFAULT_THEME.dataBar}}`);
53
+ return ` pk-has-bar ${name}`;
54
+ }
55
+ if (style.scale !== undefined) {
56
+ const t = Math.max(0, Math.min(1, style.scale));
57
+ const color = style.color && HEX.test(style.color) ? style.color : scaleColor(colorScale.filter((c) => HEX.test(c)).length >= 2 ? colorScale.filter((c) => HEX.test(c)) : [...DEFAULT_THEME.colorScale], t);
58
+ const name = `pk-c${ruleId++}`;
59
+ rules.push(`.${name}{--pk-cell-bg:${color}}`);
60
+ return ` pk-has-scale ${name}`;
61
+ }
62
+ return "";
63
+ };
64
+ const span = (cell) => `${(cell.colSpan ?? 1) > 1 ? ` colspan="${cell.colSpan}"` : ""}${(cell.rowSpan ?? 1) > 1 ? ` rowspan="${cell.rowSpan}"` : ""}`;
65
+ const head = [];
66
+ for (let r = 0; r < grid.headerRows; r++) {
67
+ const cells = grid.rows[r]
68
+ .map((cell) => {
69
+ if (!cell) {
70
+ return "";
71
+ }
72
+ const kind = cell.kind === "corner" ? " pk-corner" : "";
73
+ return `<th scope="col" class="pk-th${kind}${totalClass(cell.total)}"${span(cell)}>${escapeHtml(cell.text)}</th>`;
74
+ })
75
+ .join("");
76
+ head.push(`<tr>${cells}</tr>`);
77
+ }
78
+ const body = [];
79
+ for (let r = grid.headerRows; r < grid.headerRows + fit; r++) {
80
+ const cells = grid.rows[r]
81
+ .map((cell) => {
82
+ if (!cell) {
83
+ return "";
84
+ }
85
+ // a row label merged past the rows the cut keeps ends there
86
+ const rowSpan = Math.min(cell.rowSpan ?? 1, grid.headerRows + fit - r);
87
+ const spans = span({ ...cell, rowSpan });
88
+ if (cell.kind === "rowHeader") {
89
+ const collapsed = cell.toggle?.collapsed ? " pk-collapsed" : "";
90
+ return `<th scope="row" class="pk-th pk-row${totalClass(cell.total)}${collapsed}"${spans}>${escapeHtml(cell.text)}</th>`;
91
+ }
92
+ const number = cell.valueType === "text" ? " pk-text" : " pk-num";
93
+ return `<td class="pk-td${number}${totalClass(cell.total)}${colorRule(cell.style)}"${spans}>${escapeHtml(cell.text)}</td>`;
94
+ })
95
+ .join("");
96
+ body.push(`<tr>${cells}</tr>`);
97
+ }
98
+ const captionHtml = caption ? `<caption>${escapeHtml(caption)}</caption>` : "";
99
+ return {
100
+ html: `<table class="pk-table">${captionHtml}<thead>${head.join("")}</thead><tbody>${body.join("")}</tbody></table>`,
101
+ css: rules.join("\n"),
102
+ cut: fit < bodyCount ? { rows: fit, of: bodyCount } : null
103
+ };
104
+ }
package/src/index.js ADDED
@@ -0,0 +1,24 @@
1
+ /*
2
+ * pivotkit-report: a view over rows as a file without a browser - JSON, CSV, XLSX, SVG and a printable
3
+ * HTML report from a template with placeholders - on pivotkit-core, in Node and in the browser alike.
4
+ *
5
+ * import * as echarts from "echarts";
6
+ * const { body, issues, ok } = await render(view, rows, { format: "html", echarts, now: "2026-11-13" });
7
+ * // import { parseTemplate, fillTemplate, PLACEHOLDERS } from "pivotkit-report/template"
8
+ *
9
+ * Conventions as in pivotkit-core: required inputs first, everything optional in one options object;
10
+ * a bad view never throws (issues), a programmer error, a limit or a refused template throws
11
+ * PivotkitError.
12
+ */
13
+
14
+ /**
15
+ * @typedef {import("./render.js").RenderOptions} RenderOptions
16
+ * @typedef {import("./render.js").RenderResult} RenderResult
17
+ * @typedef {import("./texts.js").ReportTexts} ReportTexts
18
+ */
19
+
20
+ export { render, RENDER_FORMATS, REPORT_LIMITS, REPORT_ISSUE_CODES, CHART_SIZE } from "./render.js";
21
+ export { cleanSvg } from "./svg.js";
22
+ export { DEFAULT_TEMPLATE, REPORT_CSP } from "./default-template.js";
23
+ export { REPORT_TEXTS } from "./texts.js";
24
+ export { LIGHT_TOKENS, DARK_TOKENS, WEIGHT_TOKENS, SHAPE_TOKENS, SIZE_TOKENS } from "./tokens.js";