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 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
- # inkplot-scratch
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
- This is a placeholder. See https://github.com/neofox-solutions/inkplot for the real package.
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-placeholder.0",
4
- "description": "Placeholder for inkplot-scratch; the real releases are published from CI.",
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
- "type": "git",
8
- "url": "git+https://github.com/neofox-solutions/inkplot.git"
9
- },
10
- "homepage": "https://github.com/neofox-solutions/inkplot#readme"
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.