simple-ascii-chart-cli 2.1.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 +620 -66
- 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 +6 -5
- 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',
|
|
@@ -74,13 +624,14 @@ var argv = yargs
|
|
|
74
624
|
type: 'boolean',
|
|
75
625
|
description: 'Hide the x-axis if set to true',
|
|
76
626
|
})
|
|
77
|
-
.option('
|
|
78
|
-
|
|
79
|
-
description: '
|
|
627
|
+
.option('mode', {
|
|
628
|
+
choices: options_1.GRAPH_MODES,
|
|
629
|
+
description: 'Plot mode',
|
|
80
630
|
})
|
|
81
|
-
.option('
|
|
631
|
+
.option('debugMode', {
|
|
632
|
+
alias: ['debug-mode'],
|
|
82
633
|
type: 'boolean',
|
|
83
|
-
description: '
|
|
634
|
+
description: 'Enable debug mode in the chart engine',
|
|
84
635
|
})
|
|
85
636
|
.option('hideYAxis', {
|
|
86
637
|
type: 'boolean',
|
|
@@ -111,7 +662,7 @@ var argv = yargs
|
|
|
111
662
|
.option('color', {
|
|
112
663
|
alias: 'c',
|
|
113
664
|
type: 'array',
|
|
114
|
-
description: 'Array of colors for plot elements',
|
|
665
|
+
description: 'Array of ANSI colors for plot elements',
|
|
115
666
|
})
|
|
116
667
|
.option('axisCenter', {
|
|
117
668
|
type: 'array',
|
|
@@ -127,11 +678,11 @@ var argv = yargs
|
|
|
127
678
|
})
|
|
128
679
|
.option('thresholds', {
|
|
129
680
|
type: 'array',
|
|
130
|
-
description: '
|
|
681
|
+
description: 'Threshold markers: JSON object/array string (for example \'{"y":2}\' or \'[{"y":2}]\') or tokenized JSON objects',
|
|
131
682
|
})
|
|
132
683
|
.option('points', {
|
|
133
684
|
type: 'array',
|
|
134
|
-
description: '
|
|
685
|
+
description: 'Point markers: JSON object/array string (for example \'{"x":1,"y":2}\' or \'[{"x":1,"y":2}]\') or tokenized JSON objects',
|
|
135
686
|
})
|
|
136
687
|
.option('legend', {
|
|
137
688
|
type: 'string',
|
|
@@ -148,50 +699,53 @@ var argv = yargs
|
|
|
148
699
|
.option('symbols', {
|
|
149
700
|
type: 'string',
|
|
150
701
|
description: 'Custom symbols for axis, chart, and background',
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
|
|
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;
|
|
154
737
|
try {
|
|
155
|
-
|
|
738
|
+
await run(args);
|
|
156
739
|
}
|
|
157
740
|
catch (error) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
process.exit(1);
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
// Main function to execute the plot based on input and options
|
|
164
|
-
var execute = function (_a) {
|
|
165
|
-
var input = _a.input, options = _a.options;
|
|
166
|
-
withError(function () {
|
|
167
|
-
// Generate the ASCII plot based on provided input and settings
|
|
168
|
-
var output = (0, simple_ascii_chart_1.default)(input, options);
|
|
169
|
-
// Output the result to the console
|
|
170
|
-
process.stdout.write(output);
|
|
171
|
-
process.exit(0); // Exit successfully after outputting the plot
|
|
172
|
-
});
|
|
173
|
-
};
|
|
174
|
-
var prepareParams = function (_a) {
|
|
175
|
-
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, barChart = _a.barChart, horizontalBarChart = _a.horizontalBarChart;
|
|
176
|
-
var currentOptions = options ? JSON.parse(options) : {};
|
|
177
|
-
return {
|
|
178
|
-
input: JSON.parse(input),
|
|
179
|
-
options: __assign(__assign({}, currentOptions), { width: width, height: height, hideYAxis: hideYAxis, hideXAxis: hideXAxis, title: title, xLabel: xLabel, yLabel: yLabel, fillArea: fillArea, barChart: barChart, horizontalBarChart: horizontalBarChart, color: color ? (0, validators_1.validateColors)(color) : undefined, axisCenter: (0, validators_1.validateAxisCenter)(axisCenter), yRange: (0, validators_1.validateYRange)(yRange), // Validate and format yRange
|
|
180
|
-
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) }),
|
|
181
|
-
};
|
|
741
|
+
reportError(error, args.verbose ?? false);
|
|
742
|
+
}
|
|
182
743
|
};
|
|
183
|
-
// Check if argv is a Promise to handle async parsing in yargs
|
|
184
744
|
if (argv instanceof Promise) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
withError(function () {
|
|
188
|
-
execute(prepareParams(parameters));
|
|
189
|
-
});
|
|
745
|
+
argv.then((parsedArguments) => {
|
|
746
|
+
runMain(parsedArguments);
|
|
190
747
|
});
|
|
191
748
|
}
|
|
192
749
|
else {
|
|
193
|
-
|
|
194
|
-
withError(function () {
|
|
195
|
-
execute(prepareParams(argv));
|
|
196
|
-
});
|
|
750
|
+
runMain(argv);
|
|
197
751
|
}
|