inkplot-scratch 0.0.1-placeholder.0 → 1.0.0-beta.1
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/LICENSE +21 -0
- package/README.md +60 -2
- package/bin.mjs +312 -0
- package/fonts/Inter-Bold.otf +0 -0
- package/fonts/Inter-Regular.otf +0 -0
- package/fonts/LICENSE.txt +133 -0
- package/package.json +13 -8
- package/skill/SKILL.md +61 -0
- package/skill/inkplot/SKILL.md +309 -0
- package/skill/inkplot/reference/exports.md +104 -0
- package/skill/inkplot/topics/axes.md +125 -0
- package/skill/inkplot/topics/data.md +141 -0
- package/skill/inkplot/topics/furniture.md +137 -0
- package/skill/inkplot/topics/interaction.md +169 -0
- package/skill/inkplot/topics/lines-bars.md +201 -0
- package/skill/inkplot/topics/markers-color.md +274 -0
- package/skill/inkplot/topics/planes.md +139 -0
- package/skill/inkplot/topics/statistics.md +365 -0
- package/skill/inkplot/topics/styling-output.md +75 -0
- package/skill/inkplot/topics/troubleshooting.md +24 -0
- package/src/app.ts +100 -0
- package/src/figures.ts +105 -0
- package/src/interact.ts +39 -0
- package/src/template.html +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Neofox Solutions LLP and Hamish Milne
|
|
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,61 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
name: inkplot-data-charts
|
|
3
|
+
description: Use when the user wants charts, plots or visualisations of their data (Excel, CSV, JSON) as an interactive HTML page, as SVG/PNG figures, or as figures inside a Word, PowerPoint or PDF report. Builds them with the inkplot npm package and a checking command.
|
|
4
|
+
---
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
# Charts from data with inkplot
|
|
7
|
+
|
|
8
|
+
You write one TypeScript module that exports the charts; `inkplot-scratch` type-checks it, draws it, and writes an interactive HTML page, SVG files, PNG files or JSON facts. Reading the spreadsheet and building the Word/PowerPoint/PDF file are other skills' jobs: use them.
|
|
9
|
+
|
|
10
|
+
## 1. Set up (once per workspace; skip if `inkplot-scratch/` exists)
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
mkdir -p inkplot-scratch && cd inkplot-scratch && npm init -y >/dev/null && npm i inkplot inkplot-scratch
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Run everything below from that folder. If `npm i` cannot reach the registry, say so and stop: do not hand-write SVG.
|
|
17
|
+
|
|
18
|
+
Then read `node_modules/inkplot-scratch/skill/inkplot/SKILL.md` (rules, template, attributes of the common elements). Its examples are plain TypeScript, `new Element(attributes, ...children)`, which is how you write charts here. Its **Topics** index names a file in `…/skill/inkplot/topics/` for each kind of chart (histograms, box plots, polar, heat maps, axes, time…): open only the one or two you need.
|
|
19
|
+
|
|
20
|
+
## 2. Look at the data before charting
|
|
21
|
+
|
|
22
|
+
Print the sheet names, column names, types, ranges and row count. Decide which columns the request means (units too: an angle may be degrees or radians) and say your assumptions in the reply; ask only if a wrong guess would mislead. Write the rows to `inkplot-scratch/data.json`, next to `source.ts` (an array of objects; numbers as numbers, times as epoch milliseconds or ISO strings). The module imports it, so the page is self-contained and every re-run uses the same data.
|
|
23
|
+
|
|
24
|
+
## 3. Write `source.ts`
|
|
25
|
+
|
|
26
|
+
It differs from the template in the inkplot skill in one way: instead of printing the SVG with `toSvgString`/`console.log`, the file ends with the charts:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import rows from './data.json';
|
|
30
|
+
// … imports from 'inkplot' …
|
|
31
|
+
const histogram = () => new Chart({ theme: lightTheme, meta: { title: 'Magnitude histogram' } }, /* plane, axes, series */);
|
|
32
|
+
export default [histogram(), { name: 'Box by second', caption: 'Quartiles per 1 s bucket', chart: box() }];
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- Each item is a chart or `{ chart, name?, caption?, interactive? }`; the name (else `meta.title`, else "Figure N") becomes the figure title and file name.
|
|
36
|
+
- One chart object per figure: a chart used twice, or one element shared between charts, is an error. Make charts with functions.
|
|
37
|
+
- Reshaping the request names (bins, buckets, filters) can be done right in the module on `rows`.
|
|
38
|
+
- "Axis starts at 0" is `min: 0` on that axis (a histogram's axis otherwise starts at its first bin centre).
|
|
39
|
+
- `tsc` checks every `new` call: attribute names and types, and which children each element takes, in what order. Read its message for the element named in it.
|
|
40
|
+
|
|
41
|
+
## 4. Run, read, fix
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
npx inkplot-scratch source.ts --check
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- Type errors print to stderr exactly as `tsc` prints them; runtime errors name the line in `source.ts`. Fix and run again; nothing is written until it type-checks. (`.tsx` with JSX also works, but TypeScript does not check JSX children.)
|
|
48
|
+
- On success it prints, per figure: series and point counts, the **real range of each axis** and its ticks, and `WARNING` lines (empty series, NaN). Compare this with the request: "x starts at 0" must read `range 0 to …`; a bin width shows in the ticks and bar count. Fix before you deliver.
|
|
49
|
+
- `--check` cannot see overlapping labels, clipped text or crowded keys. After it passes, run `--png` once and open the PNG of each figure (the image viewer, not the facts) and fix what you see: shorten category labels, use fewer ticks, widen the chart. Label overlap is the usual one.
|
|
50
|
+
|
|
51
|
+
Outputs (combine freely, `--check` runs with all of them; paths are relative to where you run it): `--html page.html`, `--svg dir`, `--png dir` (2x, white background, fonts bundled; files are named `01-<figure name>.svg`, in order), `--json facts.json` (what `--check` prints: point counts and axis ranges, not statistics), `--title "Page title"` for the page (default: the file name). Charts are 640x400 unless the chart sets `width`/`height` or you pass `--width/--height`.
|
|
52
|
+
|
|
53
|
+
## 5. Deliver
|
|
54
|
+
|
|
55
|
+
- **Interactive:** `--html` gives one self-contained file: every chart live, with Copy/Download for SVG and PNG, a Stats panel, download-all, and pan/zoom, Shift-drag zoom box, crosshair and tooltip added for you (don't add these yourself; `--static` or `interactive: false` on an item turns them off). Give the person that file.
|
|
56
|
+
- **In a document:** write the numbers in the prose by computing them from the data in code (counts, means, quartiles, per-bin counts), or from `--json` for axis ranges and point counts, never by reading the picture. Embed the figures with the Word/PowerPoint/PDF skill. Word takes SVG natively, but docx-js insists on a PNG fallback beside it: run `--svg figs --png figs` and use the pair, `new ImageRun({ type: 'svg', data: svg, fallback: { type: 'png', data: png }, transformation: { width, height } })`, with `data` read as a Buffer (`readFileSync` with no encoding; a string is taken as base64). For anything else use the PNG. A figure's `caption` shows on the HTML page only: write captions into the document yourself.
|
|
57
|
+
- **Changes:** edit `source.ts`, run all the outputs again, re-embed. Keep `source.ts` and `data.json` so the next request starts from them.
|
|
58
|
+
|
|
59
|
+
## Know the limits
|
|
60
|
+
|
|
61
|
+
The page exports a chart as written, not as zoomed (the added interactions never reach the SVG or PNG files). Clipboard buttons need browser permission and fall back to a download. SVG text uses the viewer's `system-ui` font; PNGs use the bundled Inter.
|
package/bin.mjs
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res, err) => function __init() {
|
|
5
|
+
if (err) throw err[0];
|
|
6
|
+
try {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
} catch (e) {
|
|
9
|
+
throw err = [e], e;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// scratch/src/png.ts
|
|
18
|
+
var png_exports = {};
|
|
19
|
+
__export(png_exports, {
|
|
20
|
+
renderer: () => renderer
|
|
21
|
+
});
|
|
22
|
+
import { readFileSync } from "node:fs";
|
|
23
|
+
import { createRequire } from "node:module";
|
|
24
|
+
import { dirname, join } from "node:path";
|
|
25
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
26
|
+
async function renderer(workspace2, background, scale) {
|
|
27
|
+
let resvg, wasm;
|
|
28
|
+
try {
|
|
29
|
+
const need = createRequire(join(workspace2, "x.js"));
|
|
30
|
+
resvg = await load(need.resolve("@resvg/resvg-wasm"));
|
|
31
|
+
wasm = need.resolve("@resvg/resvg-wasm/index_bg.wasm");
|
|
32
|
+
} catch {
|
|
33
|
+
try {
|
|
34
|
+
const own = createRequire(import.meta.url);
|
|
35
|
+
resvg = await load(own.resolve("@resvg/resvg-wasm"));
|
|
36
|
+
wasm = own.resolve("@resvg/resvg-wasm/index_bg.wasm");
|
|
37
|
+
} catch {
|
|
38
|
+
process.stderr.write("error: --png needs @resvg/resvg-wasm. Run: npm i @resvg/resvg-wasm\n");
|
|
39
|
+
return process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
await resvg.initWasm(readFileSync(wasm));
|
|
43
|
+
const here2 = dirname(fileURLToPath(import.meta.url)), fonts = [join(here2, "../fonts"), join(here2, "fonts")].find((d) => {
|
|
44
|
+
try {
|
|
45
|
+
readFileSync(join(d, "Inter-Regular.otf"));
|
|
46
|
+
return true;
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
const fontBuffers = fonts ? ["Inter-Regular.otf", "Inter-Bold.otf"].map((f) => new Uint8Array(readFileSync(join(fonts, f)))) : [];
|
|
52
|
+
return (svg) => new resvg.Resvg(svg, {
|
|
53
|
+
...background !== "none" && { background: background ?? "#ffffff" },
|
|
54
|
+
fitTo: { mode: "zoom", value: scale },
|
|
55
|
+
font: { fontBuffers, defaultFontFamily: "Inter", loadSystemFonts: false }
|
|
56
|
+
}).render().asPng();
|
|
57
|
+
}
|
|
58
|
+
var load;
|
|
59
|
+
var init_png = __esm({
|
|
60
|
+
"scratch/src/png.ts"() {
|
|
61
|
+
"use strict";
|
|
62
|
+
load = async (path) => {
|
|
63
|
+
const m = await import(pathToFileURL(path).href);
|
|
64
|
+
return "initWasm" in m ? m : m.default;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// scratch/src/cli.ts
|
|
70
|
+
import { spawnSync } from "node:child_process";
|
|
71
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync as readFileSync2, rmSync, statSync, writeFileSync } from "node:fs";
|
|
72
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
73
|
+
import { tmpdir } from "node:os";
|
|
74
|
+
import { basename, dirname as dirname2, extname, join as join2, relative, resolve } from "node:path";
|
|
75
|
+
import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL2 } from "node:url";
|
|
76
|
+
import { build, formatMessages } from "esbuild";
|
|
77
|
+
var USAGE = `inkplot-scratch <source.ts|tsx> [options]
|
|
78
|
+
|
|
79
|
+
The source module ends with: export default [chart1, chart2];
|
|
80
|
+
(an item may also be { chart, name?, caption? }; a lone chart is one figure)
|
|
81
|
+
|
|
82
|
+
--check type-check, render every chart, and print what each holds (the default when no output is asked for)
|
|
83
|
+
--svg <dir> one SVG file per chart: 01-histogram.svg, 02-box.svg, \u2026
|
|
84
|
+
--png <dir> one PNG file per chart (needs @resvg/resvg-wasm; fonts are bundled)
|
|
85
|
+
--html <file> one self-contained interactive page with every chart, copy/download buttons and stats
|
|
86
|
+
--json <file|-> the facts as JSON (series, point counts, axis ranges, warnings), for writing prose from
|
|
87
|
+
--width <px> --height <px> size for charts that do not set their own (default 640 by 400)
|
|
88
|
+
--scale <n> PNG pixels per CSS pixel (default 2)
|
|
89
|
+
--background <css> PNG background (default white; "none" for transparent)
|
|
90
|
+
--title <text> page title for --html (default: the file name)
|
|
91
|
+
--static --html without the added pan/zoom, crosshair and tooltip (a figure can opt out with interactive: false)
|
|
92
|
+
--no-typecheck skip tsc (not recommended)
|
|
93
|
+
|
|
94
|
+
Type errors print to stderr exactly as \`tsc\` prints them; exit status 1 means fix the source and run again.`;
|
|
95
|
+
function fail(text, code = 1) {
|
|
96
|
+
process.stderr.write(text.endsWith("\n") ? text : text + "\n");
|
|
97
|
+
return process.exit(code);
|
|
98
|
+
}
|
|
99
|
+
var flags = /* @__PURE__ */ new Set(["--check", "--no-typecheck", "--static"]);
|
|
100
|
+
var valued = /* @__PURE__ */ new Set(["--svg", "--png", "--html", "--json", "--width", "--height", "--scale", "--background", "--title"]);
|
|
101
|
+
var on = /* @__PURE__ */ new Set();
|
|
102
|
+
var values = /* @__PURE__ */ new Map();
|
|
103
|
+
var positional = [];
|
|
104
|
+
for (let i = 2; i < process.argv.length; i++) {
|
|
105
|
+
const a = process.argv[i];
|
|
106
|
+
if (a === "-h" || a === "--help") {
|
|
107
|
+
console.log(USAGE);
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
if (flags.has(a)) on.add(a);
|
|
111
|
+
else if (valued.has(a)) {
|
|
112
|
+
const v = process.argv[++i];
|
|
113
|
+
if (v === void 0) fail(`${a} needs a value
|
|
114
|
+
|
|
115
|
+
${USAGE}`, 2);
|
|
116
|
+
values.set(a, v);
|
|
117
|
+
} else if (a.startsWith("--")) fail(`unknown option ${a}
|
|
118
|
+
|
|
119
|
+
${USAGE}`, 2);
|
|
120
|
+
else positional.push(a);
|
|
121
|
+
}
|
|
122
|
+
var sourceArg = positional[0];
|
|
123
|
+
if (positional.length !== 1 || !sourceArg) fail(`give exactly one source file
|
|
124
|
+
|
|
125
|
+
${USAGE}`, 2);
|
|
126
|
+
var num = (name) => {
|
|
127
|
+
const v = values.get(name);
|
|
128
|
+
if (v === void 0) return void 0;
|
|
129
|
+
const n = Number(v);
|
|
130
|
+
if (!(n > 0) || !Number.isFinite(n)) fail(`${name} must be a number above 0, not "${v}"`, 2);
|
|
131
|
+
return n;
|
|
132
|
+
};
|
|
133
|
+
var width = num("--width");
|
|
134
|
+
var height = num("--height");
|
|
135
|
+
var size = { ...width && { width }, ...height && { height } };
|
|
136
|
+
var source = resolve(sourceArg);
|
|
137
|
+
var workspace = process.cwd();
|
|
138
|
+
if (!existsSync(source)) fail(`no such file: ${sourceArg}`, 2);
|
|
139
|
+
var wantsOutput = ["--svg", "--png", "--html", "--json"].some((k) => values.has(k));
|
|
140
|
+
var check = on.has("--check") || !wantsOutput;
|
|
141
|
+
var here = dirname2(fileURLToPath2(import.meta.url));
|
|
142
|
+
var assets = existsSync(join2(here, "figures.ts")) ? here : join2(here, "src");
|
|
143
|
+
if (!on.has("--no-typecheck")) {
|
|
144
|
+
const find = (from) => {
|
|
145
|
+
try {
|
|
146
|
+
const json = createRequire2(from).resolve("typescript/package.json");
|
|
147
|
+
return join2(dirname2(json), JSON.parse(readFileSync2(json, "utf8")).bin.tsc);
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const tsc = find(join2(workspace, "x.js")) ?? find(import.meta.url) ?? fail("typescript is not installed here. Run: npm i inkplot-scratch typescript");
|
|
153
|
+
const r = spawnSync(process.execPath, [
|
|
154
|
+
tsc,
|
|
155
|
+
"--noEmit",
|
|
156
|
+
"--pretty",
|
|
157
|
+
"false",
|
|
158
|
+
"--strict",
|
|
159
|
+
"--noUncheckedIndexedAccess",
|
|
160
|
+
"--target",
|
|
161
|
+
"es2022",
|
|
162
|
+
"--module",
|
|
163
|
+
"esnext",
|
|
164
|
+
"--moduleResolution",
|
|
165
|
+
"bundler",
|
|
166
|
+
"--lib",
|
|
167
|
+
"es2022,dom",
|
|
168
|
+
"--jsx",
|
|
169
|
+
"react-jsx",
|
|
170
|
+
"--jsxImportSource",
|
|
171
|
+
"inkplot",
|
|
172
|
+
"--resolveJsonModule",
|
|
173
|
+
"--allowImportingTsExtensions",
|
|
174
|
+
"--skipLibCheck",
|
|
175
|
+
"--esModuleInterop",
|
|
176
|
+
source
|
|
177
|
+
], { cwd: workspace, encoding: "utf8" });
|
|
178
|
+
if (r.status !== 0) {
|
|
179
|
+
const said = (r.stdout + r.stderr).trim();
|
|
180
|
+
fail(said + (/error TS\d+/.test(said) ? "" : "\n\nThis is tsc failing to run, not a problem in your source. If it names a missing @typescript/\u2026 package, the install skipped optional dependencies: run npm i again without --omit=optional. To go on without type-checking, add --no-typecheck.") + "\n");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
var oneInkplot = () => ({
|
|
184
|
+
name: "one-inkplot",
|
|
185
|
+
setup(b) {
|
|
186
|
+
b.onResolve({ filter: /^inkplot(\/|$)/ }, async (args) => {
|
|
187
|
+
if (args.pluginData === "inkplot") return void 0;
|
|
188
|
+
const r = await b.resolve(args.path, { resolveDir: workspace, kind: args.kind, pluginData: "inkplot" });
|
|
189
|
+
return r.errors.length ? { errors: [{ text: `cannot find "${args.path}" from ${workspace}. Run: npm i inkplot` }] } : { path: r.path };
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
var common = {
|
|
194
|
+
bundle: true,
|
|
195
|
+
write: false,
|
|
196
|
+
jsx: "automatic",
|
|
197
|
+
jsxImportSource: "inkplot",
|
|
198
|
+
plugins: [oneInkplot()],
|
|
199
|
+
logLevel: "silent",
|
|
200
|
+
target: "es2022",
|
|
201
|
+
loader: { ".csv": "text", ".tsv": "text", ".txt": "text" },
|
|
202
|
+
absWorkingDir: workspace
|
|
203
|
+
};
|
|
204
|
+
var bundle = async (extra, entry) => {
|
|
205
|
+
try {
|
|
206
|
+
const out = await build({ ...common, ...extra, stdin: { contents: entry, resolveDir: dirname2(source), loader: "ts", sourcefile: "inkplot-scratch-entry.ts" } });
|
|
207
|
+
return out.outputFiles?.[0]?.text ?? fail("esbuild produced no output");
|
|
208
|
+
} catch (e) {
|
|
209
|
+
const errors = e.errors;
|
|
210
|
+
if (!errors) throw e;
|
|
211
|
+
return fail((await formatMessages(errors, { kind: "error", color: false, terminalWidth: 100 })).join("\n"));
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
var q = JSON.stringify;
|
|
215
|
+
var tmp = mkdtempSync(join2(tmpdir(), "inkplot-scratch-"));
|
|
216
|
+
process.on("exit", () => rmSync(tmp, { recursive: true, force: true }));
|
|
217
|
+
var nodeCode = await bundle(
|
|
218
|
+
{ platform: "node", format: "esm", sourcemap: "inline", sourcesContent: false },
|
|
219
|
+
`import * as user from ${q(source)};
|
|
220
|
+
import { checkText, figuresOf, factsOf, svgOf } from ${q(join2(assets, "figures.ts"))};
|
|
221
|
+
export { checkText, factsOf, svgOf };
|
|
222
|
+
export const figures = figuresOf(user);
|
|
223
|
+
`
|
|
224
|
+
);
|
|
225
|
+
var file = join2(tmp, "run.mjs");
|
|
226
|
+
writeFileSync(file, nodeCode);
|
|
227
|
+
process.setSourceMapsEnabled(true);
|
|
228
|
+
var describeThrown = (e) => e instanceof Error ? { message: e.message, stack: e.stack ?? "" } : { message: String(e), stack: "" };
|
|
229
|
+
var run;
|
|
230
|
+
try {
|
|
231
|
+
run = await import(pathToFileURL2(file).href);
|
|
232
|
+
} catch (e) {
|
|
233
|
+
const err = describeThrown(e);
|
|
234
|
+
const all = err.stack.split("\n").slice(1).filter((l) => !l.includes("node:internal") && !l.includes("bin.mjs"));
|
|
235
|
+
const mine = all.filter((l) => l.includes(dirname2(source)) && !l.includes("node_modules"));
|
|
236
|
+
fail(`error: ${err.message}
|
|
237
|
+
${(mine.length ? mine : all).slice(0, 6).join("\n")}
|
|
238
|
+
`);
|
|
239
|
+
}
|
|
240
|
+
var kb = (path) => `${(statSync(path).size / 1024).toFixed(1)} KB`;
|
|
241
|
+
var shown = (path) => relative(workspace, path) || path;
|
|
242
|
+
var figures = run.figures;
|
|
243
|
+
var svgs;
|
|
244
|
+
var facts;
|
|
245
|
+
try {
|
|
246
|
+
facts = figures.map((f) => run.factsOf(f, size));
|
|
247
|
+
svgs = figures.map((f) => run.svgOf(f, size));
|
|
248
|
+
} catch (e) {
|
|
249
|
+
const err = describeThrown(e);
|
|
250
|
+
fail(`error while drawing: ${err.message}
|
|
251
|
+
${err.stack.split("\n").slice(1).filter((l) => l.includes(source)).slice(0, 4).join("\n")}
|
|
252
|
+
`);
|
|
253
|
+
}
|
|
254
|
+
if (check) for (const f of facts) console.log(run.checkText(f));
|
|
255
|
+
var dir = (path) => {
|
|
256
|
+
const d = resolve(path);
|
|
257
|
+
mkdirSync(d, { recursive: true });
|
|
258
|
+
return d;
|
|
259
|
+
};
|
|
260
|
+
var written = [];
|
|
261
|
+
var svgDir = values.get("--svg");
|
|
262
|
+
var pngDir = values.get("--png");
|
|
263
|
+
var jsonPath = values.get("--json");
|
|
264
|
+
var htmlPath = values.get("--html");
|
|
265
|
+
if (svgDir !== void 0) {
|
|
266
|
+
const d = dir(svgDir);
|
|
267
|
+
figures.forEach((f, i) => {
|
|
268
|
+
const p = join2(d, `${f.id}.svg`);
|
|
269
|
+
writeFileSync(p, svgs[i]);
|
|
270
|
+
written.push(`${shown(p)} (${kb(p)})`);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
if (pngDir !== void 0) {
|
|
274
|
+
const d = dir(pngDir);
|
|
275
|
+
const { renderer: renderer2 } = await Promise.resolve().then(() => (init_png(), png_exports));
|
|
276
|
+
const draw = await renderer2(workspace, values.get("--background"), num("--scale") ?? 2);
|
|
277
|
+
figures.forEach((f, i) => {
|
|
278
|
+
const p = join2(d, `${f.id}.png`);
|
|
279
|
+
writeFileSync(p, draw(svgs[i]));
|
|
280
|
+
written.push(`${shown(p)} (${kb(p)})`);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
if (jsonPath !== void 0) {
|
|
284
|
+
const text = JSON.stringify(facts, null, 2) + "\n";
|
|
285
|
+
if (jsonPath === "-") process.stdout.write(text);
|
|
286
|
+
else {
|
|
287
|
+
const p = resolve(jsonPath);
|
|
288
|
+
mkdirSync(dirname2(p), { recursive: true });
|
|
289
|
+
writeFileSync(p, text);
|
|
290
|
+
written.push(`${shown(p)} (${kb(p)})`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (htmlPath !== void 0) {
|
|
294
|
+
const title = values.get("--title") ?? basename(source, extname(source));
|
|
295
|
+
const script = await bundle(
|
|
296
|
+
{ platform: "browser", format: "iife", minifyWhitespace: true, minifySyntax: true },
|
|
297
|
+
`import * as user from ${q(source)};
|
|
298
|
+
import { mount } from ${q(join2(assets, "app.ts"))};
|
|
299
|
+
mount(user, ${q(title)}, { interactive: ${on.has("--static") ? "false" : "true"} });
|
|
300
|
+
`
|
|
301
|
+
);
|
|
302
|
+
const template = readFileSync2(join2(assets, "template.html"), "utf8");
|
|
303
|
+
const html = template.replace("/*SCRIPT*/", () => script.replace(/<\/(script)/gi, "<\\/$1")).replace("<title>Charts</title>", () => `<title>${title.replace(/[&<>]/g, (c) => `&#${c.charCodeAt(0)};`)}</title>`);
|
|
304
|
+
const p = resolve(htmlPath);
|
|
305
|
+
mkdirSync(dirname2(p), { recursive: true });
|
|
306
|
+
writeFileSync(p, html);
|
|
307
|
+
written.push(`${shown(p)} (${kb(p)})`);
|
|
308
|
+
}
|
|
309
|
+
for (const w of written) (jsonPath === "-" ? process.stderr : process.stdout).write(`wrote ${w}
|
|
310
|
+
`);
|
|
311
|
+
for (const f of facts) for (const p of f.problems) process.stderr.write(`warning: ${f.id}: ${p}
|
|
312
|
+
`);
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
|
2
|
+
Upstream-Name: fonts-inter
|
|
3
|
+
Upstream-Contact: The Inter Project Authors <rasmus@notion.se>
|
|
4
|
+
Source: https://github.com/rsms/inter
|
|
5
|
+
Files-Excluded: docs/font-files/*.woff* docs/lab docs/samples
|
|
6
|
+
|
|
7
|
+
Files: *
|
|
8
|
+
Copyright: 2016-2023 The Inter Project Authors <rasmus@notion.se>
|
|
9
|
+
License: OFL-1.1 and Apache-2.0
|
|
10
|
+
|
|
11
|
+
License: OFL-1.1
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font
|
|
15
|
+
creation efforts of academic and linguistic communities, and to
|
|
16
|
+
provide a free and open framework in which fonts may be shared and
|
|
17
|
+
improved in partnership with others.
|
|
18
|
+
.
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any
|
|
23
|
+
reserved names are not used by derivative works. The fonts and
|
|
24
|
+
derivatives, however, cannot be released under any other type of
|
|
25
|
+
license. The requirement for fonts to remain under this license
|
|
26
|
+
does not apply to any document created using the fonts or their
|
|
27
|
+
derivatives.
|
|
28
|
+
.
|
|
29
|
+
DEFINITIONS
|
|
30
|
+
Font Software refers to the set of files released by the
|
|
31
|
+
Copyright Holder(s) under this license and clearly marked as such.
|
|
32
|
+
This may include source files, build scripts and documentation.
|
|
33
|
+
.
|
|
34
|
+
Reserved Font Name refers to any names specified as such after
|
|
35
|
+
the copyright statement(s).
|
|
36
|
+
.
|
|
37
|
+
Original Version refers to the collection of Font Software
|
|
38
|
+
components as distributed by the Copyright Holder(s).
|
|
39
|
+
.
|
|
40
|
+
Modified Version refers to any derivative made by adding to,
|
|
41
|
+
deleting, or substituting in part or in whole any of the components
|
|
42
|
+
of the Original Version, by changing formats or by porting the Font
|
|
43
|
+
Software to a new environment.
|
|
44
|
+
.
|
|
45
|
+
Author refers to any designer, engineer, programmer, technical
|
|
46
|
+
writer or other person who contributed to the Font Software.
|
|
47
|
+
.
|
|
48
|
+
PERMISSION & CONDITIONS
|
|
49
|
+
Permission is hereby granted, free of charge, to any person
|
|
50
|
+
obtaining a copy of the Font Software, to use, study, copy, merge,
|
|
51
|
+
embed, modify, redistribute, and sell modified and unmodified copies
|
|
52
|
+
of the Font Software, subject to the following conditions:
|
|
53
|
+
.
|
|
54
|
+
1) Neither the Font Software nor any of its individual components,
|
|
55
|
+
in Original or Modified Versions, may be sold by itself.
|
|
56
|
+
.
|
|
57
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
58
|
+
redistributed and/or sold with any software, provided that each
|
|
59
|
+
copy contains the above copyright notice and this license. These
|
|
60
|
+
can be included either as stand-alone text files, human-readable
|
|
61
|
+
headers or in the appropriate machine-readable metadata fields
|
|
62
|
+
within text or binary files as long as those fields can be easily
|
|
63
|
+
viewed by the user.
|
|
64
|
+
.
|
|
65
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
66
|
+
Name(s) unless explicit written permission is granted by the
|
|
67
|
+
corresponding Copyright Holder. This restriction only applies to
|
|
68
|
+
the primary font name as presented to the users.
|
|
69
|
+
.
|
|
70
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the
|
|
71
|
+
Font Software shall not be used to promote, endorse or advertise
|
|
72
|
+
any Modified Version, except to acknowledge the contribution(s)
|
|
73
|
+
of the Copyright Holder(s) and the Author(s) or with their
|
|
74
|
+
explicit written permission.
|
|
75
|
+
.
|
|
76
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
77
|
+
must be distributed entirely under this license, and must not be
|
|
78
|
+
distributed under any other license. The requirement for fonts to
|
|
79
|
+
remain under this license does not apply to any document created
|
|
80
|
+
using the Font Software.
|
|
81
|
+
.
|
|
82
|
+
TERMINATION
|
|
83
|
+
This license becomes null and void if any of the above conditions
|
|
84
|
+
are not met.
|
|
85
|
+
.
|
|
86
|
+
DISCLAIMER
|
|
87
|
+
THE FONT SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND,
|
|
88
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
89
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
90
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL
|
|
91
|
+
THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
92
|
+
LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR
|
|
93
|
+
CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
94
|
+
OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT
|
|
95
|
+
SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
96
|
+
|
|
97
|
+
License: Apache-2.0
|
|
98
|
+
Licensed to the Apache Software Foundation (ASF) under one or more
|
|
99
|
+
contributor license agreements. See the NOTICE file distributed with
|
|
100
|
+
this work for additional information regarding copyright ownership.
|
|
101
|
+
The ASF licenses this file to You under the Apache License, Version 2.0
|
|
102
|
+
(the "License"); you may not use this file except in compliance with
|
|
103
|
+
the License. You may obtain a copy of the License at
|
|
104
|
+
.
|
|
105
|
+
https://www.apache.org/licenses/LICENSE-2.0
|
|
106
|
+
.
|
|
107
|
+
Unless required by applicable law or agreed to in writing, software
|
|
108
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
109
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
110
|
+
See the License for the specific language governing permissions and
|
|
111
|
+
limitations under the License.
|
|
112
|
+
.
|
|
113
|
+
On Debian systems, the full text of the Apache Software License version 2 can
|
|
114
|
+
be found in the file `/usr/share/common-licenses/Apache-2.0'.
|
|
115
|
+
|
|
116
|
+
Files: debian/*
|
|
117
|
+
Copyright: 2019-2023 Gürkan Myczko <tar@debian.org>
|
|
118
|
+
License: GPL-2+
|
|
119
|
+
This package is free software; you can redistribute it and/or modify
|
|
120
|
+
it under the terms of the GNU General Public License as published by
|
|
121
|
+
the Free Software Foundation; either version 2 of the License, or
|
|
122
|
+
(at your option) any later version.
|
|
123
|
+
.
|
|
124
|
+
This package is distributed in the hope that it will be useful,
|
|
125
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
126
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
127
|
+
GNU General Public License for more details.
|
|
128
|
+
.
|
|
129
|
+
You should have received a copy of the GNU General Public License
|
|
130
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>
|
|
131
|
+
.
|
|
132
|
+
On Debian systems, the complete text of the GNU General
|
|
133
|
+
Public License version 2 can be found in "/usr/share/common-licenses/GPL-2".
|
package/package.json
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "inkplot-scratch",
|
|
3
|
-
"version": "0.0.1
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
|
+
"description": "Turn a module that exports inkplot charts into SVG, PNG, JSON facts or a single-file interactive HTML page.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "inkplot-scratch": "./bin.mjs" },
|
|
7
|
+
"files": ["bin.mjs", "src", "fonts", "skill", "README.md"],
|
|
8
|
+
"dependencies": { "inkplot": "1.0.0-beta.1", "typescript": "^7.0.2", "esbuild": "^0.28.2" },
|
|
9
|
+
"optionalDependencies": { "@resvg/resvg-wasm": "^2.6.2" },
|
|
10
|
+
"engines": { "node": ">=22" },
|
|
5
11
|
"license": "MIT",
|
|
6
|
-
"repository": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
+
"repository": { "type": "git", "url": "git+https://github.com/neofox-solutions/inkplot.git", "directory": "scratch" },
|
|
13
|
+
"homepage": "https://github.com/neofox-solutions/inkplot/tree/main/scratch#readme",
|
|
14
|
+
"bugs": { "url": "https://github.com/neofox-solutions/inkplot/issues" },
|
|
15
|
+
"keywords": ["chart", "svg", "png", "cli", "inkplot"]
|
|
16
|
+
}
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: inkplot-data-charts
|
|
3
|
+
description: Use when the user wants charts, plots or visualisations of their data (Excel, CSV, JSON) as an interactive HTML page, as SVG/PNG figures, or as figures inside a Word, PowerPoint or PDF report. Builds them with the inkplot npm package and a checking command.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Charts from data with inkplot
|
|
7
|
+
|
|
8
|
+
You write one TypeScript module that exports the charts; `inkplot-scratch` type-checks it, draws it, and writes an interactive HTML page, SVG files, PNG files or JSON facts. Reading the spreadsheet and building the Word/PowerPoint/PDF file are other skills' jobs: use them.
|
|
9
|
+
|
|
10
|
+
## 1. Set up (once per workspace; skip if `inkplot-scratch/` exists)
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
mkdir -p inkplot-scratch && cd inkplot-scratch && npm init -y >/dev/null && npm i inkplot inkplot-scratch
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Run everything below from that folder. If `npm i` cannot reach the registry, say so and stop: do not hand-write SVG.
|
|
17
|
+
|
|
18
|
+
Then read `node_modules/inkplot-scratch/skill/inkplot/SKILL.md` (rules, template, attributes of the common elements). Its examples are plain TypeScript, `new Element(attributes, ...children)`, which is how you write charts here. Its **Topics** index names a file in `…/skill/inkplot/topics/` for each kind of chart (histograms, box plots, polar, heat maps, axes, time…): open only the one or two you need.
|
|
19
|
+
|
|
20
|
+
## 2. Look at the data before charting
|
|
21
|
+
|
|
22
|
+
Print the sheet names, column names, types, ranges and row count. Decide which columns the request means (units too: an angle may be degrees or radians) and say your assumptions in the reply; ask only if a wrong guess would mislead. Write the rows to `inkplot-scratch/data.json`, next to `source.ts` (an array of objects; numbers as numbers, times as epoch milliseconds or ISO strings). The module imports it, so the page is self-contained and every re-run uses the same data.
|
|
23
|
+
|
|
24
|
+
## 3. Write `source.ts`
|
|
25
|
+
|
|
26
|
+
It differs from the template in the inkplot skill in one way: instead of printing the SVG with `toSvgString`/`console.log`, the file ends with the charts:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import rows from './data.json';
|
|
30
|
+
// … imports from 'inkplot' …
|
|
31
|
+
const histogram = () => new Chart({ theme: lightTheme, meta: { title: 'Magnitude histogram' } }, /* plane, axes, series */);
|
|
32
|
+
export default [histogram(), { name: 'Box by second', caption: 'Quartiles per 1 s bucket', chart: box() }];
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- Each item is a chart or `{ chart, name?, caption?, interactive? }`; the name (else `meta.title`, else "Figure N") becomes the figure title and file name.
|
|
36
|
+
- One chart object per figure: a chart used twice, or one element shared between charts, is an error. Make charts with functions.
|
|
37
|
+
- Reshaping the request names (bins, buckets, filters) can be done right in the module on `rows`.
|
|
38
|
+
- "Axis starts at 0" is `min: 0` on that axis (a histogram's axis otherwise starts at its first bin centre).
|
|
39
|
+
- `tsc` checks every `new` call: attribute names and types, and which children each element takes, in what order. Read its message for the element named in it.
|
|
40
|
+
|
|
41
|
+
## 4. Run, read, fix
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
npx inkplot-scratch source.ts --check
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- Type errors print to stderr exactly as `tsc` prints them; runtime errors name the line in `source.ts`. Fix and run again; nothing is written until it type-checks. (`.tsx` with JSX also works, but TypeScript does not check JSX children.)
|
|
48
|
+
- On success it prints, per figure: series and point counts, the **real range of each axis** and its ticks, and `WARNING` lines (empty series, NaN). Compare this with the request: "x starts at 0" must read `range 0 to …`; a bin width shows in the ticks and bar count. Fix before you deliver.
|
|
49
|
+
- `--check` cannot see overlapping labels, clipped text or crowded keys. After it passes, run `--png` once and open the PNG of each figure (the image viewer, not the facts) and fix what you see: shorten category labels, use fewer ticks, widen the chart. Label overlap is the usual one.
|
|
50
|
+
|
|
51
|
+
Outputs (combine freely, `--check` runs with all of them; paths are relative to where you run it): `--html page.html`, `--svg dir`, `--png dir` (2x, white background, fonts bundled; files are named `01-<figure name>.svg`, in order), `--json facts.json` (what `--check` prints: point counts and axis ranges, not statistics), `--title "Page title"` for the page (default: the file name). Charts are 640x400 unless the chart sets `width`/`height` or you pass `--width/--height`.
|
|
52
|
+
|
|
53
|
+
## 5. Deliver
|
|
54
|
+
|
|
55
|
+
- **Interactive:** `--html` gives one self-contained file: every chart live, with Copy/Download for SVG and PNG, a Stats panel, download-all, and pan/zoom, Shift-drag zoom box, crosshair and tooltip added for you (don't add these yourself; `--static` or `interactive: false` on an item turns them off). Give the person that file.
|
|
56
|
+
- **In a document:** write the numbers in the prose by computing them from the data in code (counts, means, quartiles, per-bin counts), or from `--json` for axis ranges and point counts, never by reading the picture. Embed the figures with the Word/PowerPoint/PDF skill. Word takes SVG natively, but docx-js insists on a PNG fallback beside it: run `--svg figs --png figs` and use the pair, `new ImageRun({ type: 'svg', data: svg, fallback: { type: 'png', data: png }, transformation: { width, height } })`, with `data` read as a Buffer (`readFileSync` with no encoding; a string is taken as base64). For anything else use the PNG. A figure's `caption` shows on the HTML page only: write captions into the document yourself.
|
|
57
|
+
- **Changes:** edit `source.ts`, run all the outputs again, re-embed. Keep `source.ts` and `data.json` so the next request starts from them.
|
|
58
|
+
|
|
59
|
+
## Know the limits
|
|
60
|
+
|
|
61
|
+
The page exports a chart as written, not as zoomed (the added interactions never reach the SVG or PNG files). Clipboard buttons need browser permission and fall back to a download. SVG text uses the viewer's `system-ui` font; PNGs use the bundled Inter.
|