simple-ascii-chart-cli 2.2.0 → 3.0.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/README.md +108 -23
- package/dist/cli.js +621 -63
- package/dist/index.d.ts +2 -0
- package/dist/index.js +23 -0
- package/dist/options.d.ts +28 -0
- package/dist/options.js +59 -0
- package/dist/validators.d.ts +2 -2
- package/dist/validators.js +164 -84
- package/package.json +5 -4
- package/dist/tests/cli.test.d.ts +0 -1
- package/dist/tests/cli.test.js +0 -39
- package/dist/tests/validators.test.d.ts +0 -1
- package/dist/tests/validators.test.js +0 -105
package/dist/cli.js
CHANGED
|
@@ -1,16 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
var __assign = (this && this.__assign) || function () {
|
|
4
|
-
__assign = Object.assign || function(t) {
|
|
5
|
-
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
6
|
-
s = arguments[i];
|
|
7
|
-
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
8
|
-
t[p] = s[p];
|
|
9
|
-
}
|
|
10
|
-
return t;
|
|
11
|
-
};
|
|
12
|
-
return __assign.apply(this, arguments);
|
|
13
|
-
};
|
|
14
3
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
4
|
if (k2 === undefined) k2 = k;
|
|
16
5
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -48,22 +37,583 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
48
37
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
49
38
|
};
|
|
50
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
40
|
+
const fs = __importStar(require("node:fs/promises"));
|
|
41
|
+
const path = __importStar(require("node:path"));
|
|
42
|
+
const readline = __importStar(require("node:readline"));
|
|
43
|
+
const yargs = __importStar(require("yargs"));
|
|
44
|
+
const simple_ascii_chart_1 = __importDefault(require("simple-ascii-chart"));
|
|
45
|
+
const options_1 = require("./options");
|
|
46
|
+
const clearScreen = '\x1b[2J\x1b[H';
|
|
47
|
+
class CliError extends Error {
|
|
48
|
+
cause;
|
|
49
|
+
constructor(message, cause) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.cause = cause;
|
|
52
|
+
this.name = 'CliError';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const fail = (message, cause) => {
|
|
56
|
+
throw new CliError(message, cause);
|
|
57
|
+
};
|
|
58
|
+
const parseNumber = (value) => {
|
|
59
|
+
const parsed = Number(value.trim());
|
|
60
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
61
|
+
};
|
|
62
|
+
const formatElapsedAxisTick = (elapsedMilliseconds) => {
|
|
63
|
+
const elapsedSeconds = Math.max(0, Math.round(elapsedMilliseconds / 1000));
|
|
64
|
+
if (elapsedSeconds < 60) {
|
|
65
|
+
return `+${elapsedSeconds}s`;
|
|
66
|
+
}
|
|
67
|
+
const minutes = Math.floor(elapsedSeconds / 60);
|
|
68
|
+
const seconds = elapsedSeconds % 60;
|
|
69
|
+
if (minutes < 60) {
|
|
70
|
+
return `+${minutes}m${seconds}s`;
|
|
71
|
+
}
|
|
72
|
+
const hours = Math.floor(minutes / 60);
|
|
73
|
+
const remainingMinutes = minutes % 60;
|
|
74
|
+
return `+${hours}h${remainingMinutes}m`;
|
|
75
|
+
};
|
|
76
|
+
const inferFormatFromFilePath = (inputFile) => {
|
|
77
|
+
const extension = path.extname(inputFile).toLowerCase();
|
|
78
|
+
if (extension === '.csv')
|
|
79
|
+
return 'csv';
|
|
80
|
+
if (extension === '.tsv' || extension === '.tab')
|
|
81
|
+
return 'tsv';
|
|
82
|
+
if (extension === '.space' || extension === '.dat' || extension === '.txt')
|
|
83
|
+
return 'space';
|
|
84
|
+
return 'json';
|
|
85
|
+
};
|
|
86
|
+
const inferFormatFromRawInput = (raw) => {
|
|
87
|
+
const trimmed = raw.trim();
|
|
88
|
+
if (!trimmed) {
|
|
89
|
+
return 'json';
|
|
90
|
+
}
|
|
91
|
+
if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
|
|
92
|
+
return 'json';
|
|
93
|
+
}
|
|
94
|
+
const firstLine = trimmed.split(/\r?\n/, 1)[0];
|
|
95
|
+
if (firstLine.includes('\t')) {
|
|
96
|
+
return 'tsv';
|
|
97
|
+
}
|
|
98
|
+
if (firstLine.includes(',')) {
|
|
99
|
+
return 'csv';
|
|
100
|
+
}
|
|
101
|
+
return 'space';
|
|
102
|
+
};
|
|
103
|
+
const getInputFormat = ({ explicit, inputFile, raw, }) => {
|
|
104
|
+
if (explicit) {
|
|
105
|
+
return explicit;
|
|
106
|
+
}
|
|
107
|
+
if (inputFile) {
|
|
108
|
+
return inferFormatFromFilePath(inputFile);
|
|
109
|
+
}
|
|
110
|
+
return inferFormatFromRawInput(raw);
|
|
111
|
+
};
|
|
112
|
+
const isPoint = (value) => {
|
|
113
|
+
return (Array.isArray(value) &&
|
|
114
|
+
value.length === 2 &&
|
|
115
|
+
typeof value[0] === 'number' &&
|
|
116
|
+
Number.isFinite(value[0]) &&
|
|
117
|
+
typeof value[1] === 'number' &&
|
|
118
|
+
Number.isFinite(value[1]));
|
|
119
|
+
};
|
|
120
|
+
const parseJsonCoordinates = (raw, sourceLabel) => {
|
|
121
|
+
let parsedUnknown;
|
|
122
|
+
try {
|
|
123
|
+
parsedUnknown = JSON.parse(raw);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
const details = error instanceof Error ? error.message : String(error);
|
|
127
|
+
return fail(`Invalid ${sourceLabel} JSON: ${details}`);
|
|
128
|
+
}
|
|
129
|
+
if (!Array.isArray(parsedUnknown)) {
|
|
130
|
+
fail(`Invalid ${sourceLabel}: expected an array`);
|
|
131
|
+
}
|
|
132
|
+
const parsed = parsedUnknown;
|
|
133
|
+
if (parsed.length === 0) {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
if (parsed.every((item) => isPoint(item))) {
|
|
137
|
+
return parsed;
|
|
138
|
+
}
|
|
139
|
+
if (parsed.every((series) => Array.isArray(series) && series.every((item) => isPoint(item)))) {
|
|
140
|
+
return parsed;
|
|
141
|
+
}
|
|
142
|
+
return fail(`Invalid ${sourceLabel}: expected [[x,y], ...] or [[[x,y], ...], ...]`);
|
|
143
|
+
};
|
|
144
|
+
const splitDelimitedLine = (line, format, delimiter) => {
|
|
145
|
+
if (delimiter !== undefined) {
|
|
146
|
+
return line.split(delimiter);
|
|
147
|
+
}
|
|
148
|
+
if (format === 'csv') {
|
|
149
|
+
return line.split(',');
|
|
150
|
+
}
|
|
151
|
+
if (format === 'tsv') {
|
|
152
|
+
return line.split('\t');
|
|
153
|
+
}
|
|
154
|
+
return line.trim().split(/\s+/);
|
|
155
|
+
};
|
|
156
|
+
const resolveColumnIndex = ({ column, defaultIndex, axis, headerColumns, }) => {
|
|
157
|
+
if (column === undefined) {
|
|
158
|
+
return defaultIndex;
|
|
159
|
+
}
|
|
160
|
+
if (/^\d+$/.test(column)) {
|
|
161
|
+
const index = Number(column) - 1;
|
|
162
|
+
if (index < 0) {
|
|
163
|
+
fail(`--${axis}-col must be a positive 1-based index`);
|
|
164
|
+
}
|
|
165
|
+
return index;
|
|
166
|
+
}
|
|
167
|
+
if (!headerColumns) {
|
|
168
|
+
return fail(`--${axis}-col="${column}" requires --header or a numeric column index`);
|
|
169
|
+
}
|
|
170
|
+
const index = headerColumns.indexOf(column);
|
|
171
|
+
if (index === -1) {
|
|
172
|
+
fail(`--${axis}-col="${column}" was not found in the header row`);
|
|
173
|
+
}
|
|
174
|
+
return index;
|
|
175
|
+
};
|
|
176
|
+
const parseDelimitedCoordinates = ({ raw, format, delimiter, header, xCol, yCol, sourceLabel, }) => {
|
|
177
|
+
const lines = raw
|
|
178
|
+
.split(/\r?\n/)
|
|
179
|
+
.map((line) => line.trim())
|
|
180
|
+
.filter((line) => line.length > 0);
|
|
181
|
+
if (lines.length === 0) {
|
|
182
|
+
fail(`No data found in ${sourceLabel}`);
|
|
183
|
+
}
|
|
184
|
+
const rows = lines.map((line) => splitDelimitedLine(line, format, delimiter));
|
|
185
|
+
let startIndex = 0;
|
|
186
|
+
let headerColumns;
|
|
187
|
+
if (header) {
|
|
188
|
+
headerColumns = rows[0].map((column) => column.trim());
|
|
189
|
+
startIndex = 1;
|
|
190
|
+
}
|
|
191
|
+
const xIndex = resolveColumnIndex({
|
|
192
|
+
column: xCol,
|
|
193
|
+
defaultIndex: 0,
|
|
194
|
+
axis: 'x',
|
|
195
|
+
headerColumns,
|
|
196
|
+
});
|
|
197
|
+
const yIndex = resolveColumnIndex({
|
|
198
|
+
column: yCol,
|
|
199
|
+
defaultIndex: 1,
|
|
200
|
+
axis: 'y',
|
|
201
|
+
headerColumns,
|
|
202
|
+
});
|
|
203
|
+
const points = [];
|
|
204
|
+
for (let index = startIndex; index < rows.length; index += 1) {
|
|
205
|
+
const row = rows[index];
|
|
206
|
+
const x = parseNumber(row[xIndex] ?? '');
|
|
207
|
+
const y = parseNumber(row[yIndex] ?? '');
|
|
208
|
+
if (x !== undefined && y !== undefined) {
|
|
209
|
+
points.push([x, y]);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (points.length === 0) {
|
|
213
|
+
fail(`No valid numeric points could be parsed from ${sourceLabel}`);
|
|
214
|
+
}
|
|
215
|
+
return points;
|
|
216
|
+
};
|
|
217
|
+
const parseCoordinatesFromRaw = ({ raw, args, sourceLabel, inputFile, }) => {
|
|
218
|
+
const format = getInputFormat({
|
|
219
|
+
explicit: args.format,
|
|
220
|
+
inputFile,
|
|
221
|
+
raw,
|
|
222
|
+
});
|
|
223
|
+
if (format === 'json') {
|
|
224
|
+
return parseJsonCoordinates(raw, sourceLabel);
|
|
225
|
+
}
|
|
226
|
+
return parseDelimitedCoordinates({
|
|
227
|
+
raw,
|
|
228
|
+
format,
|
|
229
|
+
delimiter: args.delimiter,
|
|
230
|
+
header: args.header,
|
|
231
|
+
xCol: args.xCol,
|
|
232
|
+
yCol: args.yCol,
|
|
233
|
+
sourceLabel,
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
const readStdinText = async () => {
|
|
237
|
+
return new Promise((resolve, reject) => {
|
|
238
|
+
let output = '';
|
|
239
|
+
process.stdin.setEncoding('utf8');
|
|
240
|
+
process.stdin.on('data', (chunk) => {
|
|
241
|
+
output += chunk;
|
|
242
|
+
});
|
|
243
|
+
process.stdin.on('error', reject);
|
|
244
|
+
process.stdin.on('end', () => resolve(output));
|
|
245
|
+
});
|
|
246
|
+
};
|
|
247
|
+
const getStaticInput = async (args) => {
|
|
248
|
+
if (args.input !== undefined) {
|
|
249
|
+
if (!args.input.trim()) {
|
|
250
|
+
fail('`--input` was provided but empty. Pass coordinate data or use --input-file/stdin.');
|
|
251
|
+
}
|
|
252
|
+
return parseCoordinatesFromRaw({
|
|
253
|
+
raw: args.input,
|
|
254
|
+
args,
|
|
255
|
+
sourceLabel: '--input',
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
if (args.inputFile) {
|
|
259
|
+
try {
|
|
260
|
+
const content = await fs.readFile(args.inputFile, 'utf8');
|
|
261
|
+
return parseCoordinatesFromRaw({
|
|
262
|
+
raw: content,
|
|
263
|
+
args,
|
|
264
|
+
sourceLabel: `file ${args.inputFile}`,
|
|
265
|
+
inputFile: args.inputFile,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
const details = error instanceof Error ? error.message : String(error);
|
|
270
|
+
fail(`Unable to read --input-file "${args.inputFile}": ${details}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (!process.stdin.isTTY) {
|
|
274
|
+
const stdinContent = await readStdinText();
|
|
275
|
+
if (!stdinContent.trim()) {
|
|
276
|
+
fail('Stdin is empty. Pipe data or provide --input/--input-file.');
|
|
277
|
+
}
|
|
278
|
+
return parseCoordinatesFromRaw({
|
|
279
|
+
raw: stdinContent,
|
|
280
|
+
args,
|
|
281
|
+
sourceLabel: 'stdin',
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return fail('Missing input. Provide --input, --input-file, or pipe data via stdin.');
|
|
285
|
+
};
|
|
286
|
+
const parseStreamSample = ({ line, lastAutoX, seriesCount, }) => {
|
|
287
|
+
const trimmed = line.trim();
|
|
288
|
+
if (!trimmed) {
|
|
289
|
+
return { nextAutoX: lastAutoX };
|
|
290
|
+
}
|
|
291
|
+
const tokens = trimmed.split(/[,\s]+/).filter(Boolean);
|
|
292
|
+
const withAutoX = seriesCount;
|
|
293
|
+
const withExplicitX = seriesCount + 1;
|
|
294
|
+
const parseValues = (startIndex) => {
|
|
295
|
+
const values = tokens
|
|
296
|
+
.slice(startIndex)
|
|
297
|
+
.map((token) => parseNumber(token))
|
|
298
|
+
.filter((value) => value !== undefined);
|
|
299
|
+
if (values.length !== seriesCount) {
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
return values;
|
|
303
|
+
};
|
|
304
|
+
const sampleMs = Date.now();
|
|
305
|
+
if (tokens.length === withAutoX) {
|
|
306
|
+
const values = parseValues(0);
|
|
307
|
+
if (!values) {
|
|
308
|
+
return { nextAutoX: lastAutoX };
|
|
309
|
+
}
|
|
310
|
+
const x = sampleMs > lastAutoX ? sampleMs : lastAutoX + 1;
|
|
311
|
+
return {
|
|
312
|
+
x,
|
|
313
|
+
values,
|
|
314
|
+
sampleMs,
|
|
315
|
+
nextAutoX: x,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
if (tokens.length === withExplicitX) {
|
|
319
|
+
const x = parseNumber(tokens[0]);
|
|
320
|
+
const values = parseValues(1);
|
|
321
|
+
if (x === undefined || !values) {
|
|
322
|
+
return { nextAutoX: lastAutoX };
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
x,
|
|
326
|
+
values,
|
|
327
|
+
sampleMs,
|
|
328
|
+
nextAutoX: x > lastAutoX ? x : lastAutoX,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
return { nextAutoX: lastAutoX };
|
|
332
|
+
};
|
|
333
|
+
const executeStatic = ({ input, options, plotOutput, }) => {
|
|
334
|
+
const output = (0, simple_ascii_chart_1.default)(input, options);
|
|
335
|
+
const stream = plotOutput === 'stderr' ? process.stderr : process.stdout;
|
|
336
|
+
stream.write(output);
|
|
337
|
+
};
|
|
338
|
+
const executeStream = ({ options, plotOutput, refreshMs, seriesCount, rateEnabled, window, passthrough, }) => {
|
|
339
|
+
const plotStream = plotOutput === 'stderr' ? process.stderr : process.stdout;
|
|
340
|
+
const seriesPoints = Array.from({ length: seriesCount }, () => []);
|
|
341
|
+
let nextAutoX = Date.now();
|
|
342
|
+
let streamStartX;
|
|
343
|
+
let previousRateSample;
|
|
344
|
+
let lastRenderAt = 0;
|
|
345
|
+
let hasPendingRender = false;
|
|
346
|
+
let renderTimer;
|
|
347
|
+
let isFinished = false;
|
|
348
|
+
const streamOptions = {
|
|
349
|
+
...(options ?? {}),
|
|
350
|
+
};
|
|
351
|
+
if (seriesCount === 2 && streamOptions.color === undefined) {
|
|
352
|
+
streamOptions.color = ['ansiCyan', 'ansiYellow'];
|
|
353
|
+
}
|
|
354
|
+
if (!streamOptions.formatter) {
|
|
355
|
+
streamOptions.formatter = (value, helpers) => {
|
|
356
|
+
if (helpers.axis === 'x') {
|
|
357
|
+
const startX = streamStartX ?? value;
|
|
358
|
+
return formatElapsedAxisTick(value - startX);
|
|
359
|
+
}
|
|
360
|
+
return value;
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
const render = () => {
|
|
364
|
+
if (seriesPoints[0].length === 0) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const renderOptions = {
|
|
368
|
+
...streamOptions,
|
|
369
|
+
};
|
|
370
|
+
if (options?.width === undefined) {
|
|
371
|
+
const columns = plotStream.columns ?? process.stdout.columns ?? 80;
|
|
372
|
+
renderOptions.width = Math.max(10, columns - 8);
|
|
373
|
+
}
|
|
374
|
+
const coordinates = (seriesCount === 1 ? seriesPoints[0] : seriesPoints);
|
|
375
|
+
const output = (0, simple_ascii_chart_1.default)(coordinates, renderOptions);
|
|
376
|
+
plotStream.write(`${clearScreen}${output}`);
|
|
377
|
+
lastRenderAt = Date.now();
|
|
378
|
+
};
|
|
379
|
+
const scheduleRender = (force = false) => {
|
|
380
|
+
if (force) {
|
|
381
|
+
if (renderTimer) {
|
|
382
|
+
clearTimeout(renderTimer);
|
|
383
|
+
renderTimer = undefined;
|
|
384
|
+
}
|
|
385
|
+
hasPendingRender = false;
|
|
386
|
+
render();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (refreshMs <= 0) {
|
|
390
|
+
render();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const now = Date.now();
|
|
394
|
+
const elapsed = now - lastRenderAt;
|
|
395
|
+
if (elapsed >= refreshMs) {
|
|
396
|
+
render();
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
hasPendingRender = true;
|
|
400
|
+
if (!renderTimer) {
|
|
401
|
+
renderTimer = setTimeout(() => {
|
|
402
|
+
renderTimer = undefined;
|
|
403
|
+
if (hasPendingRender) {
|
|
404
|
+
hasPendingRender = false;
|
|
405
|
+
render();
|
|
406
|
+
}
|
|
407
|
+
}, refreshMs - elapsed);
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
const addSample = (sample) => {
|
|
411
|
+
if (streamStartX === undefined) {
|
|
412
|
+
streamStartX = sample.x;
|
|
413
|
+
}
|
|
414
|
+
let values = sample.values;
|
|
415
|
+
if (rateEnabled) {
|
|
416
|
+
if (!previousRateSample) {
|
|
417
|
+
previousRateSample = {
|
|
418
|
+
sampleMs: sample.sampleMs,
|
|
419
|
+
values: sample.values,
|
|
420
|
+
};
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const deltaSeconds = (sample.sampleMs - previousRateSample.sampleMs) / 1000;
|
|
424
|
+
const previousValues = previousRateSample.values;
|
|
425
|
+
previousRateSample = {
|
|
426
|
+
sampleMs: sample.sampleMs,
|
|
427
|
+
values: sample.values,
|
|
428
|
+
};
|
|
429
|
+
if (deltaSeconds <= 0) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
values = sample.values.map((value, index) => (value - previousValues[index]) / deltaSeconds);
|
|
433
|
+
}
|
|
434
|
+
for (let index = 0; index < seriesCount; index += 1) {
|
|
435
|
+
seriesPoints[index].push([sample.x, values[index]]);
|
|
436
|
+
if (seriesPoints[index].length > window) {
|
|
437
|
+
seriesPoints[index].splice(0, seriesPoints[index].length - window);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
scheduleRender(false);
|
|
441
|
+
};
|
|
442
|
+
const finish = () => {
|
|
443
|
+
if (isFinished) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
isFinished = true;
|
|
447
|
+
if (renderTimer) {
|
|
448
|
+
clearTimeout(renderTimer);
|
|
449
|
+
renderTimer = undefined;
|
|
450
|
+
}
|
|
451
|
+
process.off('SIGWINCH', onResize);
|
|
452
|
+
process.off('SIGINT', onSigInt);
|
|
453
|
+
if (hasPendingRender || seriesPoints[0].length > 0) {
|
|
454
|
+
scheduleRender(true);
|
|
455
|
+
}
|
|
456
|
+
plotStream.write('\n');
|
|
457
|
+
process.exit(0);
|
|
458
|
+
};
|
|
459
|
+
const onResize = () => {
|
|
460
|
+
scheduleRender(true);
|
|
461
|
+
};
|
|
462
|
+
const stream = readline.createInterface({
|
|
463
|
+
input: process.stdin,
|
|
464
|
+
crlfDelay: Infinity,
|
|
465
|
+
terminal: false,
|
|
466
|
+
});
|
|
467
|
+
const onSigInt = () => {
|
|
468
|
+
stream.close();
|
|
469
|
+
finish();
|
|
470
|
+
};
|
|
471
|
+
process.on('SIGWINCH', onResize);
|
|
472
|
+
process.on('SIGINT', onSigInt);
|
|
473
|
+
stream.on('line', (line) => {
|
|
474
|
+
if (passthrough) {
|
|
475
|
+
process.stdout.write(`${line}\n`);
|
|
476
|
+
}
|
|
477
|
+
const parsed = parseStreamSample({
|
|
478
|
+
line,
|
|
479
|
+
lastAutoX: nextAutoX,
|
|
480
|
+
seriesCount,
|
|
481
|
+
});
|
|
482
|
+
nextAutoX = parsed.nextAutoX;
|
|
483
|
+
if ('x' in parsed) {
|
|
484
|
+
addSample(parsed);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
stream.on('close', finish);
|
|
488
|
+
};
|
|
489
|
+
const reportError = (error, verbose) => {
|
|
490
|
+
const primaryMessage = error instanceof CliError
|
|
491
|
+
? error.message
|
|
492
|
+
: error instanceof Error
|
|
493
|
+
? error.message
|
|
494
|
+
: `Unexpected error: ${String(error)}`;
|
|
495
|
+
process.stderr.write(`${primaryMessage}\n`);
|
|
496
|
+
if (verbose) {
|
|
497
|
+
if (error instanceof CliError && error.cause instanceof Error && error.cause.stack) {
|
|
498
|
+
process.stderr.write(`${error.cause.stack}\n`);
|
|
499
|
+
}
|
|
500
|
+
else if (error instanceof Error && error.stack) {
|
|
501
|
+
process.stderr.write(`${error.stack}\n`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
process.exit(1);
|
|
505
|
+
};
|
|
506
|
+
const run = async (argumentsInput) => {
|
|
507
|
+
if (argumentsInput.passthrough && !argumentsInput.stream) {
|
|
508
|
+
fail('--passthrough is only supported with --stream mode');
|
|
509
|
+
}
|
|
510
|
+
if (argumentsInput.rate && !argumentsInput.stream) {
|
|
511
|
+
fail('--rate is only supported with --stream mode');
|
|
512
|
+
}
|
|
513
|
+
const warnings = [];
|
|
514
|
+
const parsedOptions = (0, options_1.preparePlotOptions)({
|
|
515
|
+
...argumentsInput,
|
|
516
|
+
mode: argumentsInput.mode,
|
|
517
|
+
onWarning: (message) => warnings.push(message),
|
|
518
|
+
});
|
|
519
|
+
warnings.forEach((message) => {
|
|
520
|
+
process.stderr.write(`Warning: ${message}\n`);
|
|
521
|
+
});
|
|
522
|
+
const plotOutput = (argumentsInput.plotOutput ??
|
|
523
|
+
(argumentsInput.passthrough ? 'stderr' : 'stdout'));
|
|
524
|
+
if (argumentsInput.stream) {
|
|
525
|
+
executeStream({
|
|
526
|
+
options: parsedOptions,
|
|
527
|
+
plotOutput,
|
|
528
|
+
refreshMs: argumentsInput.refreshMs ?? 200,
|
|
529
|
+
seriesCount: argumentsInput.series ?? 1,
|
|
530
|
+
rateEnabled: argumentsInput.rate ?? false,
|
|
531
|
+
window: argumentsInput.window ?? 60,
|
|
532
|
+
passthrough: argumentsInput.passthrough ?? false,
|
|
533
|
+
});
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const input = await getStaticInput(argumentsInput);
|
|
537
|
+
executeStatic({
|
|
538
|
+
input,
|
|
539
|
+
options: parsedOptions,
|
|
540
|
+
plotOutput,
|
|
541
|
+
});
|
|
542
|
+
};
|
|
543
|
+
const { argv } = yargs
|
|
57
544
|
.option('input', {
|
|
58
545
|
alias: 'i',
|
|
59
546
|
type: 'string',
|
|
60
|
-
|
|
61
|
-
|
|
547
|
+
description: 'Inline data payload. Usually JSON coordinates (for example [[1,2],[2,3]])',
|
|
548
|
+
})
|
|
549
|
+
.option('inputFile', {
|
|
550
|
+
alias: ['input-file'],
|
|
551
|
+
type: 'string',
|
|
552
|
+
description: 'Read input data from a file path',
|
|
553
|
+
})
|
|
554
|
+
.option('format', {
|
|
555
|
+
choices: ['json', 'csv', 'tsv', 'space'],
|
|
556
|
+
description: 'Input format for --input-file or stdin. Auto-detected when omitted',
|
|
557
|
+
})
|
|
558
|
+
.option('delimiter', {
|
|
559
|
+
type: 'string',
|
|
560
|
+
description: 'Custom delimiter for delimited formats',
|
|
561
|
+
})
|
|
562
|
+
.option('header', {
|
|
563
|
+
type: 'boolean',
|
|
564
|
+
default: false,
|
|
565
|
+
description: 'Treat the first row in delimited input as a header row',
|
|
566
|
+
})
|
|
567
|
+
.option('xCol', {
|
|
568
|
+
alias: ['x-col'],
|
|
569
|
+
type: 'string',
|
|
570
|
+
description: 'X column selector for delimited input (1-based index or header name)',
|
|
571
|
+
})
|
|
572
|
+
.option('yCol', {
|
|
573
|
+
alias: ['y-col'],
|
|
574
|
+
type: 'string',
|
|
575
|
+
description: 'Y column selector for delimited input (1-based index or header name)',
|
|
576
|
+
})
|
|
577
|
+
.option('stream', {
|
|
578
|
+
type: 'boolean',
|
|
579
|
+
default: false,
|
|
580
|
+
description: 'Enable streaming mode and read newline-delimited samples from stdin',
|
|
581
|
+
})
|
|
582
|
+
.option('window', {
|
|
583
|
+
type: 'number',
|
|
584
|
+
default: 60,
|
|
585
|
+
description: 'Maximum number of recent samples to keep in stream mode',
|
|
586
|
+
})
|
|
587
|
+
.option('refreshMs', {
|
|
588
|
+
alias: ['refresh-ms'],
|
|
589
|
+
type: 'number',
|
|
590
|
+
default: 200,
|
|
591
|
+
description: 'Minimum render interval in milliseconds for stream mode',
|
|
592
|
+
})
|
|
593
|
+
.option('rate', {
|
|
594
|
+
type: 'boolean',
|
|
595
|
+
default: false,
|
|
596
|
+
description: 'Treat stream values as counters and plot per-second rates',
|
|
597
|
+
})
|
|
598
|
+
.option('series', {
|
|
599
|
+
choices: [1, 2],
|
|
600
|
+
default: 1,
|
|
601
|
+
description: 'Number of stream series to parse per line',
|
|
602
|
+
})
|
|
603
|
+
.option('passthrough', {
|
|
604
|
+
type: 'boolean',
|
|
605
|
+
default: false,
|
|
606
|
+
description: 'Forward incoming stream lines to stdout while plotting',
|
|
607
|
+
})
|
|
608
|
+
.option('plotOutput', {
|
|
609
|
+
alias: ['plot-output'],
|
|
610
|
+
choices: ['stdout', 'stderr'],
|
|
611
|
+
description: 'Output stream used for chart rendering',
|
|
62
612
|
})
|
|
63
613
|
.option('options', {
|
|
64
614
|
alias: 'o',
|
|
65
615
|
type: 'string',
|
|
66
|
-
description: 'Plot settings (
|
|
616
|
+
description: 'Plot settings (JSON object)',
|
|
67
617
|
})
|
|
68
618
|
.option('height', {
|
|
69
619
|
alias: 'h',
|
|
@@ -75,8 +625,13 @@ var argv = yargs
|
|
|
75
625
|
description: 'Hide the x-axis if set to true',
|
|
76
626
|
})
|
|
77
627
|
.option('mode', {
|
|
78
|
-
|
|
79
|
-
description: '
|
|
628
|
+
choices: options_1.GRAPH_MODES,
|
|
629
|
+
description: 'Plot mode',
|
|
630
|
+
})
|
|
631
|
+
.option('debugMode', {
|
|
632
|
+
alias: ['debug-mode'],
|
|
633
|
+
type: 'boolean',
|
|
634
|
+
description: 'Enable debug mode in the chart engine',
|
|
80
635
|
})
|
|
81
636
|
.option('hideYAxis', {
|
|
82
637
|
type: 'boolean',
|
|
@@ -107,7 +662,7 @@ var argv = yargs
|
|
|
107
662
|
.option('color', {
|
|
108
663
|
alias: 'c',
|
|
109
664
|
type: 'array',
|
|
110
|
-
description: 'Array of colors for plot elements',
|
|
665
|
+
description: 'Array of ANSI colors for plot elements',
|
|
111
666
|
})
|
|
112
667
|
.option('axisCenter', {
|
|
113
668
|
type: 'array',
|
|
@@ -123,11 +678,11 @@ var argv = yargs
|
|
|
123
678
|
})
|
|
124
679
|
.option('thresholds', {
|
|
125
680
|
type: 'array',
|
|
126
|
-
description: '
|
|
681
|
+
description: 'Threshold markers: JSON object/array string (for example \'{"y":2}\' or \'[{"y":2}]\') or tokenized JSON objects',
|
|
127
682
|
})
|
|
128
683
|
.option('points', {
|
|
129
684
|
type: 'array',
|
|
130
|
-
description: '
|
|
685
|
+
description: 'Point markers: JSON object/array string (for example \'{"x":1,"y":2}\' or \'[{"x":1,"y":2}]\') or tokenized JSON objects',
|
|
131
686
|
})
|
|
132
687
|
.option('legend', {
|
|
133
688
|
type: 'string',
|
|
@@ -144,50 +699,53 @@ var argv = yargs
|
|
|
144
699
|
.option('symbols', {
|
|
145
700
|
type: 'string',
|
|
146
701
|
description: 'Custom symbols for axis, chart, and background',
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
|
|
702
|
+
})
|
|
703
|
+
.option('verbose', {
|
|
704
|
+
type: 'boolean',
|
|
705
|
+
default: false,
|
|
706
|
+
description: 'Print verbose errors',
|
|
707
|
+
})
|
|
708
|
+
.check((argumentsInput) => {
|
|
709
|
+
if (argumentsInput.window !== undefined &&
|
|
710
|
+
(typeof argumentsInput.window !== 'number' ||
|
|
711
|
+
!Number.isFinite(argumentsInput.window) ||
|
|
712
|
+
argumentsInput.window <= 0)) {
|
|
713
|
+
throw new Error('window must be a positive number');
|
|
714
|
+
}
|
|
715
|
+
if (argumentsInput.refreshMs !== undefined &&
|
|
716
|
+
(typeof argumentsInput.refreshMs !== 'number' ||
|
|
717
|
+
!Number.isFinite(argumentsInput.refreshMs) ||
|
|
718
|
+
argumentsInput.refreshMs < 0)) {
|
|
719
|
+
throw new Error('refreshMs must be a non-negative number');
|
|
720
|
+
}
|
|
721
|
+
if (argumentsInput.width !== undefined &&
|
|
722
|
+
(typeof argumentsInput.width !== 'number' ||
|
|
723
|
+
!Number.isFinite(argumentsInput.width) ||
|
|
724
|
+
argumentsInput.width <= 0)) {
|
|
725
|
+
throw new Error('width must be a positive number');
|
|
726
|
+
}
|
|
727
|
+
if (argumentsInput.height !== undefined &&
|
|
728
|
+
(typeof argumentsInput.height !== 'number' ||
|
|
729
|
+
!Number.isFinite(argumentsInput.height) ||
|
|
730
|
+
argumentsInput.height <= 0)) {
|
|
731
|
+
throw new Error('height must be a positive number');
|
|
732
|
+
}
|
|
733
|
+
return true;
|
|
734
|
+
});
|
|
735
|
+
const runMain = async (parsedArguments) => {
|
|
736
|
+
const args = parsedArguments;
|
|
150
737
|
try {
|
|
151
|
-
|
|
738
|
+
await run(args);
|
|
152
739
|
}
|
|
153
740
|
catch (error) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
process.exit(1);
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
// Main function to execute the plot based on input and options
|
|
160
|
-
var execute = function (_a) {
|
|
161
|
-
var input = _a.input, options = _a.options;
|
|
162
|
-
withError(function () {
|
|
163
|
-
// Generate the ASCII plot based on provided input and settings
|
|
164
|
-
var output = (0, simple_ascii_chart_1.default)(input, options);
|
|
165
|
-
// Output the result to the console
|
|
166
|
-
process.stdout.write(output);
|
|
167
|
-
process.exit(0); // Exit successfully after outputting the plot
|
|
168
|
-
});
|
|
169
|
-
};
|
|
170
|
-
var prepareParams = function (_a) {
|
|
171
|
-
var input = _a.input, options = _a.options, width = _a.width, height = _a.height, hideYAxis = _a.hideYAxis, hideXAxis = _a.hideXAxis, fillArea = _a.fillArea, title = _a.title, xLabel = _a.xLabel, yLabel = _a.yLabel, color = _a.color, axisCenter = _a.axisCenter, yRange = _a.yRange, showTickLabel = _a.showTickLabel, thresholds = _a.thresholds, points = _a.points, legend = _a.legend, formatter = _a.formatter, lineFormatter = _a.lineFormatter, symbols = _a.symbols, mode = _a.mode;
|
|
172
|
-
var currentOptions = options ? JSON.parse(options) : {};
|
|
173
|
-
return {
|
|
174
|
-
input: JSON.parse(input),
|
|
175
|
-
options: __assign(__assign({}, currentOptions), { width: width, height: height, hideYAxis: hideYAxis, hideXAxis: hideXAxis, title: title, xLabel: xLabel, yLabel: yLabel, fillArea: fillArea, mode: mode, color: color ? (0, validators_1.validateColors)(color) : undefined, axisCenter: (0, validators_1.validateAxisCenter)(axisCenter), yRange: (0, validators_1.validateYRange)(yRange), // Validate and format yRange
|
|
176
|
-
showTickLabel: showTickLabel, thresholds: (0, validators_1.validateThresholds)(thresholds), points: (0, validators_1.validatePoints)(points), legend: (0, validators_1.validateLegend)(legend), formatter: (0, validators_1.validateFormatter)(formatter), lineFormatter: (0, validators_1.validateLineFormatter)(lineFormatter), symbols: (0, validators_1.validateSymbols)(symbols) }),
|
|
177
|
-
};
|
|
741
|
+
reportError(error, args.verbose ?? false);
|
|
742
|
+
}
|
|
178
743
|
};
|
|
179
|
-
// Check if argv is a Promise to handle async parsing in yargs
|
|
180
744
|
if (argv instanceof Promise) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
withError(function () {
|
|
184
|
-
execute(prepareParams(parameters));
|
|
185
|
-
});
|
|
745
|
+
argv.then((parsedArguments) => {
|
|
746
|
+
runMain(parsedArguments);
|
|
186
747
|
});
|
|
187
748
|
}
|
|
188
749
|
else {
|
|
189
|
-
|
|
190
|
-
withError(function () {
|
|
191
|
-
execute(prepareParams(argv));
|
|
192
|
-
});
|
|
750
|
+
runMain(argv);
|
|
193
751
|
}
|