chartforge 0.0.2 → 0.1.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/LICENSE +21 -0
- package/README.md +272 -0
- package/cli/dist/AxisPlugin-Cge0pH5P.cjs +135 -0
- package/cli/dist/BasePlugin-DcBTEJBs.cjs +13 -0
- package/cli/dist/ChartForge-BMKdVldJ.cjs +1108 -0
- package/cli/dist/GridPlugin-C90wMQsN.cjs +69 -0
- package/cli/dist/builtins-5HYzyd_B.cjs +44 -0
- package/cli/dist/chartforge.cjs +3 -0
- package/cli/dist/index-BtGYSrj-.cjs +240 -0
- package/cli/dist/misc-tOf-LXeZ.cjs +52 -0
- package/cli/dist/png-By3-3Jat.cjs +61 -0
- package/cli/dist/render-OAxwCzdV.cjs +94 -0
- package/cli/dist/serve-74PsUgLE.cjs +58 -0
- package/cli/dist/watch-Dv_eG4Wh.cjs +34 -0
- package/cli/dist/writer-CRUdthqh.cjs +697 -0
- package/dist/plugins.umd.cjs +1 -0
- package/dist/themes.umd.cjs +1 -0
- package/package.json +88 -68
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
const index = require("./index-BtGYSrj-.cjs");
|
|
25
|
+
const jsdom = require("jsdom");
|
|
26
|
+
const fs = require("fs");
|
|
27
|
+
const path = require("path");
|
|
28
|
+
function parseJSON(raw, jq) {
|
|
29
|
+
let obj = JSON.parse(raw);
|
|
30
|
+
if (jq) {
|
|
31
|
+
for (const key of jq.split(".")) {
|
|
32
|
+
if (obj && typeof obj === "object" && key in obj) {
|
|
33
|
+
obj = obj[key];
|
|
34
|
+
} else {
|
|
35
|
+
throw new Error(`jq path "${jq}" not found in response`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (isChartShape(obj)) return obj;
|
|
40
|
+
if (Array.isArray(obj) && obj.every((v) => typeof v === "number")) {
|
|
41
|
+
return { series: [{ name: "Series 1", data: obj }] };
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(obj) && obj.length && typeof obj[0] === "object") {
|
|
44
|
+
return parseObjectArray(obj);
|
|
45
|
+
}
|
|
46
|
+
if (typeof obj === "object" && obj !== null && !Array.isArray(obj)) {
|
|
47
|
+
const entries = Object.entries(obj);
|
|
48
|
+
return {
|
|
49
|
+
labels: entries.map(([k]) => k),
|
|
50
|
+
series: [{ name: "Series 1", data: entries.map(([, v]) => Number(v)) }]
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
throw new Error("Could not auto-detect data shape from JSON. Use ChartForge shape: { series: [{ data: [] }] }");
|
|
54
|
+
}
|
|
55
|
+
function isChartShape(obj) {
|
|
56
|
+
return typeof obj === "object" && obj !== null && "series" in obj && Array.isArray(obj["series"]);
|
|
57
|
+
}
|
|
58
|
+
function parseObjectArray(arr) {
|
|
59
|
+
const labelKeys = ["label", "name", "x", "key", "category", "date", "month"];
|
|
60
|
+
const valueKeys = ["value", "y", "v", "count", "total", "amount", "sales", "revenue"];
|
|
61
|
+
const labelKey = labelKeys.find((k) => k in arr[0]);
|
|
62
|
+
const valueKey = valueKeys.find((k) => k in arr[0]);
|
|
63
|
+
if (!valueKey) {
|
|
64
|
+
throw new Error(`Cannot find a value field. Tried: ${valueKeys.join(", ")}`);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
labels: labelKey ? arr.map((r) => String(r[labelKey])) : void 0,
|
|
68
|
+
series: [{ name: "Series 1", data: arr.map((r) => Number(r[valueKey])) }]
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function parseCSV(raw, delimiter = ",") {
|
|
72
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
73
|
+
if (!lines.length) throw new Error("CSV is empty");
|
|
74
|
+
const header = lines[0].split(delimiter).map((h) => h.trim().replace(/^"|"$/g, ""));
|
|
75
|
+
const rows = lines.slice(1).map(
|
|
76
|
+
(l) => l.split(delimiter).map((v) => v.trim().replace(/^"|"$/g, ""))
|
|
77
|
+
);
|
|
78
|
+
const isNumericCol = (ci) => rows.every((r) => r[ci] === "" || !isNaN(Number(r[ci])));
|
|
79
|
+
let labelColIdx = -1;
|
|
80
|
+
if (!isNumericCol(0)) labelColIdx = 0;
|
|
81
|
+
const valueCols = header.map((_, ci) => ci).filter((ci) => ci !== labelColIdx && isNumericCol(ci));
|
|
82
|
+
return {
|
|
83
|
+
labels: labelColIdx >= 0 ? rows.map((r) => r[labelColIdx]) : void 0,
|
|
84
|
+
series: valueCols.map((ci) => ({
|
|
85
|
+
name: header[ci],
|
|
86
|
+
data: rows.map((r) => r[ci] === "" ? 0 : Number(r[ci]))
|
|
87
|
+
}))
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function parseTSV(raw) {
|
|
91
|
+
return parseCSV(raw, " ");
|
|
92
|
+
}
|
|
93
|
+
function parseYAML(raw) {
|
|
94
|
+
const lines = raw.split("\n");
|
|
95
|
+
const obj = {};
|
|
96
|
+
let current = obj;
|
|
97
|
+
for (let line of lines) {
|
|
98
|
+
line = line.trimEnd();
|
|
99
|
+
if (!line || line.startsWith("#")) continue;
|
|
100
|
+
const match = line.match(/^(\s*)(\w[\w\s]*):\s*(.*)/);
|
|
101
|
+
if (!match) continue;
|
|
102
|
+
const [, , key, value] = match;
|
|
103
|
+
const trimKey = key.trim();
|
|
104
|
+
if (value.startsWith("[")) {
|
|
105
|
+
try {
|
|
106
|
+
current[trimKey] = JSON.parse(value);
|
|
107
|
+
} catch {
|
|
108
|
+
current[trimKey] = value;
|
|
109
|
+
}
|
|
110
|
+
} else if (value !== "") {
|
|
111
|
+
const num = Number(value);
|
|
112
|
+
current[trimKey] = isNaN(num) ? value.replace(/^['"]|['"]$/g, "") : num;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (isChartShape(obj)) return obj;
|
|
116
|
+
return parseJSON(JSON.stringify(obj));
|
|
117
|
+
}
|
|
118
|
+
function parseInput(raw, format) {
|
|
119
|
+
const fmt = (format ?? "").toLowerCase();
|
|
120
|
+
if (fmt === "tsv") return parseTSV(raw);
|
|
121
|
+
if (fmt === "csv") return parseCSV(raw);
|
|
122
|
+
if (fmt === "yaml" || fmt === "yml") return parseYAML(raw);
|
|
123
|
+
const trimmed = raw.trimStart();
|
|
124
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) return parseJSON(raw);
|
|
125
|
+
if (trimmed.includes(" ")) return parseTSV(raw);
|
|
126
|
+
return parseCSV(raw);
|
|
127
|
+
}
|
|
128
|
+
const parser = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
129
|
+
__proto__: null,
|
|
130
|
+
parseCSV,
|
|
131
|
+
parseInput,
|
|
132
|
+
parseJSON,
|
|
133
|
+
parseTSV,
|
|
134
|
+
parseYAML
|
|
135
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
136
|
+
async function fetchData(opts) {
|
|
137
|
+
const { url, method = "GET", headers = {}, body, jq, timeout = 15e3 } = opts;
|
|
138
|
+
index.logger.step(`Fetching ${method} ${url}`);
|
|
139
|
+
const controller = new AbortController();
|
|
140
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
141
|
+
try {
|
|
142
|
+
const res = await fetch(url, {
|
|
143
|
+
method,
|
|
144
|
+
headers: { "Accept": "application/json", ...headers },
|
|
145
|
+
body: body && method !== "GET" ? body : void 0,
|
|
146
|
+
signal: controller.signal
|
|
147
|
+
});
|
|
148
|
+
clearTimeout(timer);
|
|
149
|
+
if (!res.ok) {
|
|
150
|
+
throw new Error(`HTTP ${res.status} ${res.statusText} from ${url}`);
|
|
151
|
+
}
|
|
152
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
153
|
+
let raw;
|
|
154
|
+
if (contentType.includes("json")) {
|
|
155
|
+
raw = await res.text();
|
|
156
|
+
} else if (contentType.includes("csv") || contentType.includes("text/plain")) {
|
|
157
|
+
const { parseCSV: parseCSV2 } = await Promise.resolve().then(() => parser);
|
|
158
|
+
raw = await res.text();
|
|
159
|
+
return parseCSV2(raw);
|
|
160
|
+
} else {
|
|
161
|
+
raw = await res.text();
|
|
162
|
+
}
|
|
163
|
+
return parseJSON(raw, jq);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
clearTimeout(timer);
|
|
166
|
+
if (err.name === "AbortError") {
|
|
167
|
+
throw new Error(`Request to ${url} timed out after ${timeout}ms`);
|
|
168
|
+
}
|
|
169
|
+
throw err;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function pollData(opts, interval, callback, signal) {
|
|
173
|
+
let tick = 0;
|
|
174
|
+
const run = async () => {
|
|
175
|
+
try {
|
|
176
|
+
const data = await fetchData(opts);
|
|
177
|
+
await callback(data, tick++);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
index.logger.error("Poll fetch failed", err);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
await run();
|
|
183
|
+
await new Promise((resolve) => {
|
|
184
|
+
const id = setInterval(async () => {
|
|
185
|
+
if (signal?.aborted) {
|
|
186
|
+
clearInterval(id);
|
|
187
|
+
resolve();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
await run();
|
|
191
|
+
}, interval * 1e3);
|
|
192
|
+
signal?.addEventListener("abort", () => {
|
|
193
|
+
clearInterval(id);
|
|
194
|
+
resolve();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const BRAILLE_OFFSET = 10240;
|
|
199
|
+
const BRAILLE_DOTS = [1, 8, 2, 16, 4, 32, 64, 128];
|
|
200
|
+
const USE_COLOR = !process.env["NO_COLOR"] && process.stdout.isTTY;
|
|
201
|
+
const c = (code, s) => USE_COLOR ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
202
|
+
const COLORS = ["36", "33", "32", "35", "34", "31", "37", "90"];
|
|
203
|
+
const seriesColor = (i, s) => c(COLORS[i % COLORS.length], s);
|
|
204
|
+
const BLOCKS = ["▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"];
|
|
205
|
+
const FULL = "█";
|
|
206
|
+
function barChart(data, termWidth = 80) {
|
|
207
|
+
const lines = [];
|
|
208
|
+
const series = data.series[0];
|
|
209
|
+
if (!series) return "(no data)";
|
|
210
|
+
const values = series.data;
|
|
211
|
+
const labels = data.labels ?? values.map((_, i) => String(i + 1));
|
|
212
|
+
const maxVal = Math.max(...values.filter((v) => isFinite(v)));
|
|
213
|
+
const maxLbl = Math.max(...labels.map((l) => l.length));
|
|
214
|
+
const barW = Math.max(20, Math.min(50, termWidth - maxLbl - 18));
|
|
215
|
+
const formatVal = (v) => {
|
|
216
|
+
if (v >= 1e6) return `${(v / 1e6).toFixed(1)}M`;
|
|
217
|
+
if (v >= 1e3) return `${(v / 1e3).toFixed(1)}k`;
|
|
218
|
+
return String(v);
|
|
219
|
+
};
|
|
220
|
+
for (let i = 0; i < values.length; i++) {
|
|
221
|
+
const val = values[i];
|
|
222
|
+
const lbl = labels[i].padStart(maxLbl);
|
|
223
|
+
const ratio = maxVal > 0 ? val / maxVal : 0;
|
|
224
|
+
const full = Math.floor(ratio * barW);
|
|
225
|
+
const part = Math.floor((ratio * barW - full) * 8);
|
|
226
|
+
const bar = FULL.repeat(full) + (part > 0 ? BLOCKS[part - 1] : "");
|
|
227
|
+
const blank = " ".repeat(Math.max(0, barW - full - (part > 0 ? 1 : 0)));
|
|
228
|
+
const valStr = formatVal(val).padStart(8);
|
|
229
|
+
lines.push(
|
|
230
|
+
` ${c("90", lbl)} ${seriesColor(0, bar)}${c("90", blank)} ${c("2", valStr)}`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return lines.join("\n");
|
|
234
|
+
}
|
|
235
|
+
const SPARK = "▁▂▃▄▅▆▇█";
|
|
236
|
+
function sparkline(values) {
|
|
237
|
+
const min = Math.min(...values);
|
|
238
|
+
const max = Math.max(...values);
|
|
239
|
+
const rng = max - min || 1;
|
|
240
|
+
return values.map((v) => {
|
|
241
|
+
const idx = Math.round((v - min) / rng * (SPARK.length - 1));
|
|
242
|
+
return SPARK[idx];
|
|
243
|
+
}).join("");
|
|
244
|
+
}
|
|
245
|
+
function brailleChart(data, cols = 60, rows = 16) {
|
|
246
|
+
const canvas = Array.from(
|
|
247
|
+
{ length: rows * 4 },
|
|
248
|
+
() => new Array(cols * 2).fill(0)
|
|
249
|
+
);
|
|
250
|
+
const allVals = data.series.flatMap((s) => s.data);
|
|
251
|
+
const minVal = Math.min(...allVals);
|
|
252
|
+
const maxVal = Math.max(...allVals);
|
|
253
|
+
const rng = maxVal - minVal || 1;
|
|
254
|
+
data.series.forEach((s, si) => {
|
|
255
|
+
const vals = s.data;
|
|
256
|
+
for (let i = 0; i < vals.length - 1; i++) {
|
|
257
|
+
const x1 = Math.round(i / (vals.length - 1) * (cols * 2 - 1));
|
|
258
|
+
const x2 = Math.round((i + 1) / (vals.length - 1) * (cols * 2 - 1));
|
|
259
|
+
const y1 = Math.round((vals[i] - minVal) / rng * (rows * 4 - 1));
|
|
260
|
+
const y2 = Math.round((vals[i + 1] - minVal) / rng * (rows * 4 - 1));
|
|
261
|
+
let dx = x2 - x1, dy = y2 - y1;
|
|
262
|
+
const sx = dx > 0 ? 1 : -1, sy = dy > 0 ? 1 : -1;
|
|
263
|
+
dx = Math.abs(dx);
|
|
264
|
+
dy = Math.abs(dy);
|
|
265
|
+
let err = dx - dy, cx = x1, cy = y1;
|
|
266
|
+
while (true) {
|
|
267
|
+
const px = cx, py = rows * 4 - 1 - cy;
|
|
268
|
+
if (px >= 0 && px < cols * 2 && py >= 0 && py < rows * 4) {
|
|
269
|
+
canvas[py][px] = si + 1;
|
|
270
|
+
}
|
|
271
|
+
if (cx === x2 && cy === y2) break;
|
|
272
|
+
const e2 = 2 * err;
|
|
273
|
+
if (e2 > -dy) {
|
|
274
|
+
err -= dy;
|
|
275
|
+
cx += sx;
|
|
276
|
+
}
|
|
277
|
+
if (e2 < dx) {
|
|
278
|
+
err += dx;
|
|
279
|
+
cy += sy;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
const brailleLines = [];
|
|
285
|
+
for (let row = 0; row < rows; row++) {
|
|
286
|
+
let line = " ";
|
|
287
|
+
for (let col = 0; col < cols; col++) {
|
|
288
|
+
let char = BRAILLE_OFFSET;
|
|
289
|
+
let si = 0;
|
|
290
|
+
for (let dy = 0; dy < 4; dy++) {
|
|
291
|
+
for (let dx = 0; dx < 2; dx++) {
|
|
292
|
+
const px = col * 2 + dx;
|
|
293
|
+
const py = row * 4 + dy;
|
|
294
|
+
const sv = canvas[py]?.[px] ?? 0;
|
|
295
|
+
if (sv) {
|
|
296
|
+
char |= BRAILLE_DOTS[dy * 2 + dx];
|
|
297
|
+
si = sv - 1;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const ch = String.fromCharCode(char);
|
|
302
|
+
line += char === BRAILLE_OFFSET ? " " : seriesColor(si, ch);
|
|
303
|
+
}
|
|
304
|
+
brailleLines.push(line);
|
|
305
|
+
}
|
|
306
|
+
return brailleLines.join("\n");
|
|
307
|
+
}
|
|
308
|
+
function yAxis(values, height = 16) {
|
|
309
|
+
const min = Math.min(...values);
|
|
310
|
+
const max = Math.max(...values);
|
|
311
|
+
const fmt = (v) => {
|
|
312
|
+
if (Math.abs(v) >= 1e6) return `${(v / 1e6).toFixed(1)}M`;
|
|
313
|
+
if (Math.abs(v) >= 1e3) return `${(v / 1e3).toFixed(1)}k`;
|
|
314
|
+
return v.toFixed(1);
|
|
315
|
+
};
|
|
316
|
+
const lines = [];
|
|
317
|
+
for (let i = 0; i < height; i++) {
|
|
318
|
+
const v = max - i / (height - 1) * (max - min);
|
|
319
|
+
lines.push(i % 4 === 0 ? c("90", fmt(v).padStart(8)) : " ".repeat(8));
|
|
320
|
+
}
|
|
321
|
+
return lines;
|
|
322
|
+
}
|
|
323
|
+
function legend(data) {
|
|
324
|
+
return data.series.map((s, i) => seriesColor(i, ` ● ${s.name ?? `Series ${i + 1}`}`)).join(" ");
|
|
325
|
+
}
|
|
326
|
+
function renderToTerminal(data, opts = {}) {
|
|
327
|
+
const termW = opts.width ?? Math.min(process.stdout.columns ?? 80, 100);
|
|
328
|
+
const type = opts.type ?? "column";
|
|
329
|
+
const lines = [];
|
|
330
|
+
if (opts.title) {
|
|
331
|
+
lines.push("");
|
|
332
|
+
lines.push(` ${c("1", opts.title)}`);
|
|
333
|
+
lines.push(` ${c("90", "─".repeat(Math.min(opts.title.length + 2, termW - 4)))}`);
|
|
334
|
+
}
|
|
335
|
+
lines.push("");
|
|
336
|
+
if (type === "line" || type === "scatter") {
|
|
337
|
+
const allVals2 = data.series.flatMap((s) => s.data);
|
|
338
|
+
const yLabels = yAxis(allVals2);
|
|
339
|
+
const braille = brailleChart(data, Math.max(30, Math.floor((termW - 12) / 2)));
|
|
340
|
+
const brailleLines = braille.split("\n");
|
|
341
|
+
brailleLines.forEach((bl, i) => {
|
|
342
|
+
lines.push((yLabels[i] ?? " ".repeat(8)) + c("90", " │") + bl);
|
|
343
|
+
});
|
|
344
|
+
lines.push(" ".repeat(9) + c("90", "└" + "─".repeat(termW - 11)));
|
|
345
|
+
if (data.labels?.length) {
|
|
346
|
+
const step = Math.max(1, Math.floor(data.labels.length / 8));
|
|
347
|
+
const xLine = data.labels.filter((_, i) => i % step === 0).map((l) => l.substring(0, 8).padEnd(9)).join("");
|
|
348
|
+
lines.push(c("90", " " + xLine));
|
|
349
|
+
}
|
|
350
|
+
} else if (type === "pie" || type === "donut") {
|
|
351
|
+
const vals = data.series[0]?.data ?? [];
|
|
352
|
+
const labels = data.labels ?? vals.map((_, i) => `Slice ${i + 1}`);
|
|
353
|
+
const total = vals.reduce((s, v) => s + v, 0);
|
|
354
|
+
const barW = Math.min(40, termW - 30);
|
|
355
|
+
for (let i = 0; i < vals.length; i++) {
|
|
356
|
+
const pct = total > 0 ? vals[i] / total : 0;
|
|
357
|
+
const fill = Math.round(pct * barW);
|
|
358
|
+
const bar = "█".repeat(fill) + c("90", "░".repeat(barW - fill));
|
|
359
|
+
lines.push(
|
|
360
|
+
` ${seriesColor(i, labels[i].padEnd(15).substring(0, 15))} ${seriesColor(i, bar)} ${c("1", (pct * 100).toFixed(1))}%`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
} else if (type === "bar") {
|
|
364
|
+
lines.push(barChart({ ...data, series: data.series }, termW));
|
|
365
|
+
} else {
|
|
366
|
+
lines.push(barChart(data, termW));
|
|
367
|
+
}
|
|
368
|
+
lines.push("");
|
|
369
|
+
data.series.forEach((s, i) => {
|
|
370
|
+
const spark = sparkline(s.data);
|
|
371
|
+
lines.push(` ${seriesColor(i, `${s.name ?? `Series ${i + 1}`}:`).padEnd(20)} ${seriesColor(i, spark)}`);
|
|
372
|
+
});
|
|
373
|
+
if (data.series.length > 1) {
|
|
374
|
+
lines.push("");
|
|
375
|
+
lines.push(legend(data));
|
|
376
|
+
}
|
|
377
|
+
const allVals = data.series.flatMap((s) => s.data).filter(isFinite);
|
|
378
|
+
const min = Math.min(...allVals);
|
|
379
|
+
const max = Math.max(...allVals);
|
|
380
|
+
const avg = allVals.reduce((s, v) => s + v, 0) / allVals.length;
|
|
381
|
+
lines.push("");
|
|
382
|
+
lines.push(c("90", [
|
|
383
|
+
` Min: ${min.toLocaleString()}`,
|
|
384
|
+
`Max: ${max.toLocaleString()}`,
|
|
385
|
+
`Avg: ${avg.toFixed(2)}`,
|
|
386
|
+
`Points: ${allVals.length}`
|
|
387
|
+
].join(" ")));
|
|
388
|
+
lines.push("");
|
|
389
|
+
return lines.join("\n");
|
|
390
|
+
}
|
|
391
|
+
let _shimmed = false;
|
|
392
|
+
function shimBrowser() {
|
|
393
|
+
if (_shimmed) return;
|
|
394
|
+
_shimmed = true;
|
|
395
|
+
const dom = new jsdom.JSDOM('<!DOCTYPE html><html><body><div id="root"></div></body></html>', {
|
|
396
|
+
pretendToBeVisual: true
|
|
397
|
+
});
|
|
398
|
+
const { window } = dom;
|
|
399
|
+
Object.assign(globalThis, {
|
|
400
|
+
window,
|
|
401
|
+
document: window.document,
|
|
402
|
+
navigator: window.navigator,
|
|
403
|
+
SVGElement: window.SVGElement,
|
|
404
|
+
SVGSVGElement: window.SVGSVGElement,
|
|
405
|
+
HTMLElement: window.HTMLElement,
|
|
406
|
+
Element: window.Element,
|
|
407
|
+
Event: window.Event,
|
|
408
|
+
MouseEvent: window.MouseEvent,
|
|
409
|
+
CustomEvent: window.CustomEvent,
|
|
410
|
+
MutationObserver: window.MutationObserver,
|
|
411
|
+
ResizeObserver: class MockResizeObserver {
|
|
412
|
+
observe() {
|
|
413
|
+
}
|
|
414
|
+
unobserve() {
|
|
415
|
+
}
|
|
416
|
+
disconnect() {
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
requestAnimationFrame: (cb) => setTimeout(() => cb(performance.now()), 0),
|
|
420
|
+
cancelAnimationFrame: (id) => clearTimeout(id),
|
|
421
|
+
performance: globalThis.performance ?? { now: Date.now.bind(Date) }
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
async function renderToSVG(data, opts) {
|
|
425
|
+
shimBrowser();
|
|
426
|
+
const { ChartForge } = await Promise.resolve().then(() => require("./ChartForge-BMKdVldJ.cjs"));
|
|
427
|
+
const { BUILT_IN_THEMES } = await Promise.resolve().then(() => require("./builtins-5HYzyd_B.cjs"));
|
|
428
|
+
const width = opts.width ?? 800;
|
|
429
|
+
const height = opts.height ?? 450;
|
|
430
|
+
const theme = opts.theme ?? "dark";
|
|
431
|
+
const container = globalThis.document.createElement("div");
|
|
432
|
+
container.style.width = `${width}px`;
|
|
433
|
+
container.style.height = `${height}px`;
|
|
434
|
+
globalThis.document.body.appendChild(container);
|
|
435
|
+
const chart = new ChartForge(container, {
|
|
436
|
+
type: opts.type ?? "column",
|
|
437
|
+
theme,
|
|
438
|
+
width,
|
|
439
|
+
height,
|
|
440
|
+
responsive: false,
|
|
441
|
+
animation: { enabled: false },
|
|
442
|
+
// always disable for static export
|
|
443
|
+
data: {
|
|
444
|
+
labels: opts.labels ? opts.labels.split(",").map((l) => l.trim()) : data.labels,
|
|
445
|
+
series: data.series
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
const noAxis = ["pie", "donut", "funnel", "heatmap"];
|
|
449
|
+
if (!noAxis.includes(opts.type ?? "column")) {
|
|
450
|
+
const { AxisPlugin } = await Promise.resolve().then(() => require("./AxisPlugin-Cge0pH5P.cjs"));
|
|
451
|
+
const { GridPlugin } = await Promise.resolve().then(() => require("./GridPlugin-C90wMQsN.cjs"));
|
|
452
|
+
chart.use("axis", AxisPlugin);
|
|
453
|
+
chart.use("grid", GridPlugin);
|
|
454
|
+
}
|
|
455
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
456
|
+
const svg = container.querySelector("svg");
|
|
457
|
+
if (!svg) throw new Error("Renderer produced no SVG element");
|
|
458
|
+
chart.themeManager.get(theme) ?? BUILT_IN_THEMES["dark"];
|
|
459
|
+
const style = globalThis.document.createElementNS("http://www.w3.org/2000/svg", "style");
|
|
460
|
+
style.textContent = `
|
|
461
|
+
.chartforge-svg { font-family: 'Segoe UI', system-ui, sans-serif; }
|
|
462
|
+
text { font-family: inherit; }
|
|
463
|
+
`;
|
|
464
|
+
svg.insertBefore(style, svg.firstChild);
|
|
465
|
+
if (!svg.getAttribute("viewBox")) {
|
|
466
|
+
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
467
|
+
}
|
|
468
|
+
svg.setAttribute("width", String(width));
|
|
469
|
+
svg.setAttribute("height", String(height));
|
|
470
|
+
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
471
|
+
const svgStr = svg.outerHTML;
|
|
472
|
+
chart.destroy();
|
|
473
|
+
globalThis.document.body.removeChild(container);
|
|
474
|
+
return { svg: svgStr, width, height };
|
|
475
|
+
}
|
|
476
|
+
function generateHTML(svgResult, data, opts) {
|
|
477
|
+
const title = opts.title ?? "ChartForge Export";
|
|
478
|
+
const theme = opts.theme ?? "dark";
|
|
479
|
+
const isDark = theme !== "light";
|
|
480
|
+
const bg = isDark ? "#0f0f13" : "#f8f9fa";
|
|
481
|
+
const surface = isDark ? "#18181f" : "#ffffff";
|
|
482
|
+
const border = isDark ? "#2a2a35" : "#e5e7eb";
|
|
483
|
+
const text = isDark ? "#e2e2f0" : "#111827";
|
|
484
|
+
const muted = isDark ? "#6b6b80" : "#6b7280";
|
|
485
|
+
const accent = "#6366f1";
|
|
486
|
+
const now = (/* @__PURE__ */ new Date()).toLocaleString();
|
|
487
|
+
const stats = buildStats(data);
|
|
488
|
+
return `<!DOCTYPE html>
|
|
489
|
+
<html lang="en" data-theme="${theme}">
|
|
490
|
+
<head>
|
|
491
|
+
<meta charset="UTF-8" />
|
|
492
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
493
|
+
<title>${escHtml(title)}</title>
|
|
494
|
+
<style>
|
|
495
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
496
|
+
|
|
497
|
+
:root {
|
|
498
|
+
--bg: ${bg};
|
|
499
|
+
--surface: ${surface};
|
|
500
|
+
--border: ${border};
|
|
501
|
+
--text: ${text};
|
|
502
|
+
--muted: ${muted};
|
|
503
|
+
--accent: ${accent};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
body {
|
|
507
|
+
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
|
508
|
+
background: var(--bg);
|
|
509
|
+
color: var(--text);
|
|
510
|
+
min-height: 100vh;
|
|
511
|
+
display: flex;
|
|
512
|
+
flex-direction: column;
|
|
513
|
+
align-items: center;
|
|
514
|
+
padding: 2rem 1rem;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
.container {
|
|
518
|
+
width: 100%;
|
|
519
|
+
max-width: 960px;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
header {
|
|
523
|
+
margin-bottom: 1.5rem;
|
|
524
|
+
display: flex;
|
|
525
|
+
align-items: flex-start;
|
|
526
|
+
justify-content: space-between;
|
|
527
|
+
flex-wrap: wrap;
|
|
528
|
+
gap: .5rem;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.title-group h1 {
|
|
532
|
+
font-size: 1.5rem;
|
|
533
|
+
font-weight: 700;
|
|
534
|
+
letter-spacing: -.02em;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.title-group p {
|
|
538
|
+
color: var(--muted);
|
|
539
|
+
font-size: .8rem;
|
|
540
|
+
margin-top: .25rem;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.badge {
|
|
544
|
+
font-size: .7rem;
|
|
545
|
+
padding: .25rem .6rem;
|
|
546
|
+
background: color-mix(in srgb, var(--accent) 15%, transparent);
|
|
547
|
+
color: var(--accent);
|
|
548
|
+
border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
|
|
549
|
+
border-radius: 9999px;
|
|
550
|
+
font-weight: 600;
|
|
551
|
+
white-space: nowrap;
|
|
552
|
+
align-self: flex-start;
|
|
553
|
+
margin-top: .2rem;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.chart-card {
|
|
557
|
+
background: var(--surface);
|
|
558
|
+
border: 1px solid var(--border);
|
|
559
|
+
border-radius: 16px;
|
|
560
|
+
padding: 1.5rem;
|
|
561
|
+
margin-bottom: 1.5rem;
|
|
562
|
+
box-shadow: 0 4px 24px rgba(0,0,0,.12);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.chart-card svg {
|
|
566
|
+
width: 100%;
|
|
567
|
+
height: auto;
|
|
568
|
+
display: block;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.stats-grid {
|
|
572
|
+
display: grid;
|
|
573
|
+
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
574
|
+
gap: 1rem;
|
|
575
|
+
margin-bottom: 1.5rem;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
.stat-card {
|
|
579
|
+
background: var(--surface);
|
|
580
|
+
border: 1px solid var(--border);
|
|
581
|
+
border-radius: 12px;
|
|
582
|
+
padding: 1rem 1.25rem;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.stat-card .label {
|
|
586
|
+
font-size: .75rem;
|
|
587
|
+
color: var(--muted);
|
|
588
|
+
text-transform: uppercase;
|
|
589
|
+
letter-spacing: .06em;
|
|
590
|
+
margin-bottom: .35rem;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.stat-card .value {
|
|
594
|
+
font-size: 1.5rem;
|
|
595
|
+
font-weight: 700;
|
|
596
|
+
color: var(--text);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.meta {
|
|
600
|
+
font-size: .75rem;
|
|
601
|
+
color: var(--muted);
|
|
602
|
+
text-align: center;
|
|
603
|
+
margin-top: 1rem;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
.meta a { color: var(--accent); text-decoration: none; }
|
|
607
|
+
.meta a:hover { text-decoration: underline; }
|
|
608
|
+
|
|
609
|
+
@media (max-width: 600px) {
|
|
610
|
+
.stats-grid { grid-template-columns: 1fr 1fr; }
|
|
611
|
+
}
|
|
612
|
+
</style>
|
|
613
|
+
</head>
|
|
614
|
+
<body>
|
|
615
|
+
<div class="container">
|
|
616
|
+
<header>
|
|
617
|
+
<div class="title-group">
|
|
618
|
+
<h1>${escHtml(title)}</h1>
|
|
619
|
+
<p>Generated by ChartForge CLI · ${escHtml(now)}</p>
|
|
620
|
+
</div>
|
|
621
|
+
<span class="badge">⬡ ChartForge</span>
|
|
622
|
+
</header>
|
|
623
|
+
|
|
624
|
+
<div class="chart-card">
|
|
625
|
+
${svgResult.svg}
|
|
626
|
+
</div>
|
|
627
|
+
|
|
628
|
+
<div class="stats-grid">
|
|
629
|
+
${stats.map((s) => `
|
|
630
|
+
<div class="stat-card">
|
|
631
|
+
<div class="label">${escHtml(s.label)}</div>
|
|
632
|
+
<div class="value">${escHtml(s.value)}</div>
|
|
633
|
+
</div>`).join("")}
|
|
634
|
+
</div>
|
|
635
|
+
|
|
636
|
+
<p class="meta">
|
|
637
|
+
Rendered with <a href="https://github.com/anandpilania/chartforge" target="_blank">ChartForge</a>
|
|
638
|
+
</p>
|
|
639
|
+
</div>
|
|
640
|
+
</body>
|
|
641
|
+
</html>`;
|
|
642
|
+
}
|
|
643
|
+
function buildStats(data) {
|
|
644
|
+
const allVals = data.series.flatMap((s) => s.data).filter((v) => isFinite(v));
|
|
645
|
+
if (!allVals.length) return [];
|
|
646
|
+
const sum = allVals.reduce((s, v) => s + v, 0);
|
|
647
|
+
const avg = sum / allVals.length;
|
|
648
|
+
const fmt = (v) => {
|
|
649
|
+
if (Math.abs(v) >= 1e6) return `${(v / 1e6).toFixed(2)}M`;
|
|
650
|
+
if (Math.abs(v) >= 1e3) return `${(v / 1e3).toFixed(1)}k`;
|
|
651
|
+
return Number.isInteger(v) ? String(v) : v.toFixed(2);
|
|
652
|
+
};
|
|
653
|
+
return [
|
|
654
|
+
{ label: "Data Points", value: String(allVals.length) },
|
|
655
|
+
{ label: "Series", value: String(data.series.length) },
|
|
656
|
+
{ label: "Maximum", value: fmt(Math.max(...allVals)) },
|
|
657
|
+
{ label: "Minimum", value: fmt(Math.min(...allVals)) },
|
|
658
|
+
{ label: "Average", value: fmt(avg) },
|
|
659
|
+
{ label: "Total / Sum", value: fmt(sum) }
|
|
660
|
+
];
|
|
661
|
+
}
|
|
662
|
+
function escHtml(s) {
|
|
663
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
664
|
+
}
|
|
665
|
+
function writeOutput(content, outPath) {
|
|
666
|
+
const abs = path.resolve(outPath);
|
|
667
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
668
|
+
fs.writeFileSync(abs, content);
|
|
669
|
+
index.logger.success(`Saved → ${abs}`);
|
|
670
|
+
}
|
|
671
|
+
function writeStdout(content) {
|
|
672
|
+
process.stdout.write(content);
|
|
673
|
+
}
|
|
674
|
+
function defaultFilename(format) {
|
|
675
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
676
|
+
return `chartforge-${ts}.${format}`;
|
|
677
|
+
}
|
|
678
|
+
async function openInBrowser(filePath) {
|
|
679
|
+
const abs = path.resolve(filePath);
|
|
680
|
+
const open = await import("open").catch(() => null);
|
|
681
|
+
if (open) {
|
|
682
|
+
await open.default(`file://${abs}`);
|
|
683
|
+
} else {
|
|
684
|
+
index.logger.info(`Open in browser: file://${abs}`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
exports.defaultFilename = defaultFilename;
|
|
688
|
+
exports.fetchData = fetchData;
|
|
689
|
+
exports.generateHTML = generateHTML;
|
|
690
|
+
exports.openInBrowser = openInBrowser;
|
|
691
|
+
exports.parseInput = parseInput;
|
|
692
|
+
exports.parseJSON = parseJSON;
|
|
693
|
+
exports.pollData = pollData;
|
|
694
|
+
exports.renderToSVG = renderToSVG;
|
|
695
|
+
exports.renderToTerminal = renderToTerminal;
|
|
696
|
+
exports.writeOutput = writeOutput;
|
|
697
|
+
exports.writeStdout = writeStdout;
|