pipechart 1.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 ADDED
@@ -0,0 +1,228 @@
1
+ # pipechart
2
+
3
+ > Zero-dependency CLI tool that pipes JSON to real-time ASCII charts in the terminal.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/pipechart.svg)](https://www.npmjs.com/package/pipechart)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+ [![Node.js >= 12](https://img.shields.io/badge/node-%3E%3D12-brightgreen.svg)](https://nodejs.org)
8
+
9
+ ---
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install -g pipechart
15
+ ```
16
+
17
+ Or run without installing:
18
+
19
+ ```bash
20
+ npx pipechart --help
21
+ ```
22
+
23
+ ---
24
+
25
+ ## Usage
26
+
27
+ ```
28
+ echo '<json>' | pipechart [options]
29
+ cat data.json | pipechart [options]
30
+ tail -f stream | pipechart --live [options]
31
+ ```
32
+
33
+ ### Options
34
+
35
+ | Flag | Short | Description |
36
+ |------|-------|-------------|
37
+ | `--type <bar\|line\|spark\|hist>` | `-t` | Force chart type (auto-detected if omitted) |
38
+ | `--color <name>` | `-c` | Chart color: `red`, `green`, `blue`, `yellow`, `cyan`, `white` |
39
+ | `--width <n>` | `-w` | Override terminal width (default: `process.stdout.columns`) |
40
+ | `--height <n>` | | Chart height in rows for line/hist (default: `12`) |
41
+ | `--title <text>` | | Print a title above the chart |
42
+ | `--field <key>` | `-f` | Which numeric field to use from objects |
43
+ | `--live` | `-l` | Streaming / live mode — reads NDJSON continuously |
44
+ | `--spark` | `-s` | Force single-line sparkline output |
45
+ | `--help` | `-h` | Show help |
46
+ | `--version` | `-v` | Show version |
47
+
48
+ ---
49
+
50
+ ## Examples
51
+
52
+ ### 1. Sparkline — flat array of numbers
53
+
54
+ ```bash
55
+ echo '[3,1,4,1,5,9,2,6,5,3,5]' | pipechart
56
+ ```
57
+
58
+ ```
59
+ ▃▁▄▁▅█▂▅▅▃▅ 1 – 9
60
+ ```
61
+
62
+ ### 2. Bar chart — array of objects
63
+
64
+ ```bash
65
+ echo '[
66
+ {"name":"Alpha","value":42},
67
+ {"name":"Beta","value":87},
68
+ {"name":"Gamma","value":23},
69
+ {"name":"Delta","value":65}
70
+ ]' | pipechart --type bar --color green
71
+ ```
72
+
73
+ ```
74
+ Alpha │ ████████████████████████████ 42
75
+ Beta │ ███████████████████████████████████████████████████████ 87
76
+ Gamma │ ████████████████ 23
77
+ Delta │ ████████████████████████████████████████████ 65
78
+ └────────────────────────────────────────────────────────────
79
+ 0 87
80
+ ```
81
+
82
+ ### 3. Line chart — time-series `{t, v}` objects
83
+
84
+ ```bash
85
+ echo '[
86
+ {"t":1,"v":10},{"t":2,"v":25},{"t":3,"v":18},
87
+ {"t":4,"v":40},{"t":5,"v":35},{"t":6,"v":55},
88
+ {"t":7,"v":48},{"t":8,"v":70}
89
+ ]' | pipechart --type line --color cyan --title "Server Latency (ms)"
90
+ ```
91
+
92
+ ```
93
+ Server Latency (ms)
94
+
95
+ 70 │ // •
96
+ │ //
97
+ │ // //
98
+ │ /// •\\\ //
99
+ │ /// \\\\\•
100
+ │ // //
101
+ 40 │ /// •────────•/
102
+ │ // ///
103
+ │ /// •\\\ //
104
+ │ /// \\\\\•/
105
+ │ //
106
+ 10 │•/
107
+ └─────────────────────────────────────────────────────────────
108
+ 1 5 8
109
+ ```
110
+
111
+ ### 4. Histogram — distribution of values
112
+
113
+ ```bash
114
+ echo '[2,5,8,3,7,1,9,4,6,2,5,8,3,7,1,9,4,6,5,5,5,6,7,8,3,2,1,4,9,6]' \
115
+ | pipechart --type hist --color yellow --title "Value Distribution"
116
+ ```
117
+
118
+ ```
119
+ Value Distribution
120
+
121
+ 9 │ ███████████
122
+ │ ███████████
123
+ │ ███████████
124
+ │ ███████████
125
+ │███████████ ███████████ ███████████
126
+ │███████████ ███████████ ███████████
127
+ 5 │███████████ ███████████ ███████████
128
+ │███████████ ███████████ ███████████
129
+ │███████████████████████████████████████████████████████████████
130
+ │███████████████████████████████████████████████████████████████
131
+ │███████████████████████████████████████████████████████████████
132
+ 0 │███████████████████████████████████████████████████████████████
133
+ └──────────────────────────────────────────────────────────────
134
+ 1 5 9
135
+ n=30 bins=6
136
+ ```
137
+
138
+ ### 5. Live streaming mode
139
+
140
+ ```bash
141
+ # Pipe a stream of NDJSON numbers — re-renders in place
142
+ tail -f /var/log/metrics.jsonl | pipechart --live --type line --color green
143
+
144
+ # Simulate a live stream with a shell loop
145
+ while true; do
146
+ echo $RANDOM
147
+ sleep 0.5
148
+ done | pipechart --live --type spark
149
+ ```
150
+
151
+ ### 6. Use a specific field from objects
152
+
153
+ ```bash
154
+ echo '[
155
+ {"host":"web-1","cpu":72.3},
156
+ {"host":"web-2","cpu":45.1},
157
+ {"host":"db-1","cpu":88.9}
158
+ ]' | pipechart --type bar --field cpu --color red
159
+ ```
160
+
161
+ ```
162
+ web-1 │ ████████████████████████████████████████████████████ 72.3
163
+ web-2 │ ████████████████████████████████ 45.1
164
+ db-1 │ ████████████████████████████████████████████████████████ 88.9
165
+ └────────────────────────────────────────────────────────────
166
+ 0 88.9
167
+ ```
168
+
169
+ ### 7. Pipe from `jq`
170
+
171
+ ```bash
172
+ curl -s https://api.example.com/metrics \
173
+ | jq '[.data[] | .response_time]' \
174
+ | pipechart --type hist --color cyan --title "Response Times"
175
+ ```
176
+
177
+ ---
178
+
179
+ ## JSON Shape Auto-Detection
180
+
181
+ pipechart automatically picks the best chart type based on your data:
182
+
183
+ | Input shape | Default chart |
184
+ |-------------|---------------|
185
+ | `[1, 2, 3, ...]` — flat numbers, ≤ 60 items | **sparkline** |
186
+ | `[1, 2, 3, ...]` — flat numbers, > 60 items | **histogram** |
187
+ | `[{"t":…,"v":…}, …]` — time-value pairs | **line chart** |
188
+ | `[{"name":…,"value":…}, …]` — labeled objects | **bar chart** |
189
+
190
+ Override with `--type bar|line|spark|hist`.
191
+
192
+ ---
193
+
194
+ ## Why pipechart?
195
+
196
+ - **Zero dependencies** — pure Node.js, nothing to audit, nothing to break.
197
+ - **Works over SSH** — renders in any terminal that supports ANSI codes (virtually all of them).
198
+ - **Works on legacy hardware** — Node.js 12+ is all you need; no native addons, no compilation.
199
+ - **Composable** — plays nicely with `jq`, `curl`, `tail -f`, `watch`, and any Unix pipeline.
200
+ - **Instant** — no startup overhead, no network calls, no config files.
201
+ - **Readable source** — ~600 lines of plain ES5-compatible JavaScript; easy to audit and fork.
202
+
203
+ ---
204
+
205
+ ## Package Structure
206
+
207
+ ```
208
+ pipechart/
209
+ ├── bin/
210
+ │ └── pipechart.js # CLI entrypoint (shebang)
211
+ ├── lib/
212
+ │ ├── detect.js # JSON shape detection
213
+ │ ├── render/
214
+ │ │ ├── bar.js # Horizontal bar chart
215
+ │ │ ├── line.js # Line chart with ASCII connectors
216
+ │ │ ├── spark.js # Single-line sparkline
217
+ │ │ └── hist.js # Vertical histogram
218
+ │ ├── ansi.js # Color + cursor ANSI helpers
219
+ │ └── scale.js # Normalization + scaling math
220
+ ├── package.json
221
+ └── README.md
222
+ ```
223
+
224
+ ---
225
+
226
+ ## License
227
+
228
+ MIT
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * pipechart — zero-dependency CLI tool that pipes JSON to real-time ASCII charts
6
+ *
7
+ * Usage:
8
+ * echo '[1,2,3,4,5]' | pipechart
9
+ * cat data.json | pipechart --type bar --color green
10
+ * tail -f metrics.jsonl | pipechart --live --type line
11
+ */
12
+
13
+ var readline = require('readline');
14
+ var detect = require('../lib/detect');
15
+ var ansi = require('../lib/ansi');
16
+ var bar = require('../lib/render/bar');
17
+ var line = require('../lib/render/line');
18
+ var spark = require('../lib/render/spark');
19
+ var hist = require('../lib/render/hist');
20
+
21
+ // ── Parse CLI arguments ──────────────────────────────────────────────────────
22
+
23
+ var args = process.argv.slice(2);
24
+
25
+ /**
26
+ * Parse a simple --flag [value] argument list.
27
+ * Flags without a following value are treated as booleans.
28
+ * @param {string[]} argv
29
+ * @returns {object}
30
+ */
31
+ function parseArgs(argv) {
32
+ var opts = {};
33
+ var i = 0;
34
+ while (i < argv.length) {
35
+ var arg = argv[i];
36
+ if (arg.slice(0, 2) === '--') {
37
+ var key = arg.slice(2);
38
+ var next = argv[i + 1];
39
+ if (next !== undefined && next.slice(0, 2) !== '--') {
40
+ opts[key] = next;
41
+ i += 2;
42
+ } else {
43
+ opts[key] = true;
44
+ i += 1;
45
+ }
46
+ } else if (arg.slice(0, 1) === '-' && arg.length === 2) {
47
+ var shortKey = arg.slice(1);
48
+ var shortNext = argv[i + 1];
49
+ if (shortNext !== undefined && shortNext.slice(0, 1) !== '-') {
50
+ opts[shortKey] = shortNext;
51
+ i += 2;
52
+ } else {
53
+ opts[shortKey] = true;
54
+ i += 1;
55
+ }
56
+ } else {
57
+ i += 1;
58
+ }
59
+ }
60
+ return opts;
61
+ }
62
+
63
+ var opts = parseArgs(args);
64
+
65
+ // Show help
66
+ if (opts.help || opts.h) {
67
+ printHelp();
68
+ process.exit(0);
69
+ }
70
+
71
+ // Show version
72
+ if (opts.version || opts.v) {
73
+ try {
74
+ var pkg = require('../package.json');
75
+ process.stdout.write(pkg.version + '\n');
76
+ } catch (e) {
77
+ process.stdout.write('1.0.0\n');
78
+ }
79
+ process.exit(0);
80
+ }
81
+
82
+ var LIVE_MODE = !!(opts.live || opts.l);
83
+ var FORCE_TYPE = opts.type || opts.t || null;
84
+ var COLOR = opts.color || opts.c || 'cyan';
85
+ var WIDTH = parseInt(opts.width || opts.w, 10) || process.stdout.columns || 80;
86
+ var TITLE = opts.title || null;
87
+ var FIELD = opts.field || opts.f || null;
88
+ var SPARK_MODE = !!(opts.spark || opts.s);
89
+ var HEIGHT = parseInt(opts.height, 10) || 12;
90
+
91
+ if (SPARK_MODE && !FORCE_TYPE) FORCE_TYPE = 'spark';
92
+
93
+ // ── Rendering ────────────────────────────────────────────────────────────────
94
+
95
+ /**
96
+ * Dispatch to the correct renderer based on detected/forced type.
97
+ * @param {object} descriptor — from detect.detect()
98
+ * @returns {string}
99
+ */
100
+ function renderChart(descriptor) {
101
+ var type = descriptor.type;
102
+ var renderOpts = {
103
+ values: descriptor.values,
104
+ labels: descriptor.labels,
105
+ times: descriptor.times,
106
+ width: WIDTH,
107
+ color: COLOR,
108
+ title: TITLE,
109
+ height: HEIGHT
110
+ };
111
+
112
+ switch (type) {
113
+ case 'bar': return bar.render(renderOpts);
114
+ case 'line': return line.render(renderOpts);
115
+ case 'spark': return spark.render(renderOpts);
116
+ case 'hist': return hist.render(renderOpts);
117
+ default: return spark.render(renderOpts);
118
+ }
119
+ }
120
+
121
+ // ── Live streaming mode ──────────────────────────────────────────────────────
122
+
123
+ if (LIVE_MODE) {
124
+ runLiveMode();
125
+ } else {
126
+ runBatchMode();
127
+ }
128
+
129
+ // ── Batch mode: read all stdin, parse, render once ───────────────────────────
130
+
131
+ function runBatchMode() {
132
+ var chunks = [];
133
+
134
+ process.stdin.setEncoding('utf8');
135
+ process.stdin.on('data', function(chunk) {
136
+ chunks.push(chunk);
137
+ });
138
+
139
+ process.stdin.on('end', function() {
140
+ var raw = chunks.join('');
141
+ raw = raw.trim();
142
+
143
+ if (!raw) {
144
+ process.stderr.write('pipechart: no input received. Pipe JSON data to stdin.\n');
145
+ printHelp();
146
+ process.exit(1);
147
+ }
148
+
149
+ // Try to parse as a single JSON value first
150
+ var data;
151
+ try {
152
+ data = JSON.parse(raw);
153
+ } catch (e) {
154
+ // Try newline-delimited JSON (NDJSON) — collect all valid lines
155
+ var lines = raw.split('\n');
156
+ var collected = [];
157
+ for (var i = 0; i < lines.length; i++) {
158
+ var l = lines[i].trim();
159
+ if (!l) continue;
160
+ try {
161
+ var parsed = JSON.parse(l);
162
+ collected.push(parsed);
163
+ } catch (e2) {
164
+ // Skip malformed lines gracefully
165
+ }
166
+ }
167
+ if (collected.length === 0) {
168
+ process.stderr.write('pipechart: could not parse input as JSON.\n');
169
+ process.exit(1);
170
+ }
171
+ data = collected;
172
+ }
173
+
174
+ var descriptor = detect.detect(data, { type: FORCE_TYPE, field: FIELD });
175
+
176
+ if (descriptor.values.length === 0) {
177
+ process.stderr.write('pipechart: no numeric values found in input.\n');
178
+ process.exit(1);
179
+ }
180
+
181
+ var output = renderChart(descriptor);
182
+ process.stdout.write(output + '\n');
183
+ });
184
+
185
+ process.stdin.on('error', function(err) {
186
+ process.stderr.write('pipechart: stdin error: ' + err.message + '\n');
187
+ process.exit(1);
188
+ });
189
+ }
190
+
191
+ // ── Live mode: read NDJSON lines, re-render in place ─────────────────────────
192
+
193
+ function runLiveMode() {
194
+ var buffer = [];
195
+ var lastLineCount = 0;
196
+ var firstRender = true;
197
+
198
+ // Hide cursor for cleaner live output
199
+ process.stdout.write(ansi.hideCursor());
200
+
201
+ // Restore cursor on exit
202
+ function cleanup() {
203
+ process.stdout.write(ansi.showCursor());
204
+ process.stdout.write('\n');
205
+ }
206
+ process.on('exit', cleanup);
207
+ process.on('SIGINT', function() {
208
+ cleanup();
209
+ process.exit(0);
210
+ });
211
+ process.on('SIGTERM', function() {
212
+ cleanup();
213
+ process.exit(0);
214
+ });
215
+
216
+ var rl = readline.createInterface({
217
+ input: process.stdin,
218
+ crlfDelay: Infinity
219
+ });
220
+
221
+ rl.on('line', function(rawLine) {
222
+ rawLine = rawLine.trim();
223
+ if (!rawLine) return;
224
+
225
+ var parsed;
226
+ try {
227
+ parsed = JSON.parse(rawLine);
228
+ } catch (e) {
229
+ // Skip malformed lines — don't crash
230
+ return;
231
+ }
232
+
233
+ // Accumulate data points
234
+ // If the line is an array, replace the buffer (useful for watch-style streams)
235
+ // If it's a scalar/object, push it
236
+ if (Array.isArray(parsed)) {
237
+ buffer = parsed;
238
+ } else {
239
+ buffer.push(parsed);
240
+ }
241
+
242
+ // Keep buffer bounded to avoid unbounded memory growth
243
+ var maxBuffer = WIDTH * 4;
244
+ if (buffer.length > maxBuffer) {
245
+ buffer = buffer.slice(buffer.length - maxBuffer);
246
+ }
247
+
248
+ var descriptor = detect.detect(buffer, { type: FORCE_TYPE, field: FIELD });
249
+ if (descriptor.values.length === 0) return;
250
+
251
+ var output = renderChart(descriptor);
252
+ var outputLines = output.split('\n');
253
+ var lineCount = outputLines.length;
254
+
255
+ if (!firstRender) {
256
+ // Move cursor up to overwrite previous render
257
+ process.stdout.write(ansi.cursorUp(lastLineCount));
258
+ }
259
+
260
+ // Clear and rewrite each line
261
+ for (var i = 0; i < lineCount; i++) {
262
+ process.stdout.write(ansi.eraseLine() + outputLines[i] + '\n');
263
+ }
264
+
265
+ // If new render is shorter, clear leftover lines
266
+ if (!firstRender && lineCount < lastLineCount) {
267
+ for (var j = lineCount; j < lastLineCount; j++) {
268
+ process.stdout.write(ansi.eraseLine() + '\n');
269
+ }
270
+ process.stdout.write(ansi.cursorUp(lastLineCount - lineCount));
271
+ }
272
+
273
+ lastLineCount = lineCount;
274
+ firstRender = false;
275
+ });
276
+
277
+ rl.on('close', function() {
278
+ // stdin closed — stay alive showing last render
279
+ });
280
+
281
+ rl.on('error', function(err) {
282
+ process.stderr.write('pipechart: readline error: ' + err.message + '\n');
283
+ });
284
+ }
285
+
286
+ // ── Help text ─────────────────────────────────────────────────────────────────
287
+
288
+ function printHelp() {
289
+ var help = [
290
+ '',
291
+ ansi.bold(ansi.colorize('pipechart', 'cyan')) + ansi.dim(' — zero-dependency JSON → ASCII charts'),
292
+ '',
293
+ ansi.bold('USAGE'),
294
+ ' echo \'[1,2,3,4,5]\' | pipechart [options]',
295
+ ' cat data.json | pipechart --type bar --color green',
296
+ ' tail -f log.jsonl | pipechart --live --type line',
297
+ '',
298
+ ansi.bold('OPTIONS'),
299
+ ' --type, -t <bar|line|spark|hist> Force chart type',
300
+ ' --color, -c <color> Chart color (red|green|blue|yellow|cyan|white)',
301
+ ' --width, -w <n> Override terminal width',
302
+ ' --height <n> Chart height in rows (default: 12)',
303
+ ' --title <text> Print a title above the chart',
304
+ ' --field, -f <key> Which field to use from objects',
305
+ ' --live, -l Streaming / live mode (NDJSON)',
306
+ ' --spark, -s Force single-line sparkline output',
307
+ ' --help, -h Show this help',
308
+ ' --version,-v Show version',
309
+ '',
310
+ ansi.bold('EXAMPLES'),
311
+ ' echo \'[3,1,4,1,5,9,2,6]\' | pipechart',
312
+ ' echo \'[3,1,4,1,5,9,2,6]\' | pipechart --type hist --color yellow',
313
+ ' echo \'[{"name":"A","val":10},{"name":"B","val":20}]\' | pipechart --type bar --field val',
314
+ ' echo \'[{"t":1,"v":3},{"t":2,"v":7}]\' | pipechart --type line',
315
+ ' tail -f metrics.jsonl | pipechart --live --type spark',
316
+ ''
317
+ ].join('\n');
318
+ process.stdout.write(help);
319
+ }
package/lib/ansi.js ADDED
@@ -0,0 +1,131 @@
1
+ 'use strict';
2
+
3
+ // ANSI escape code helpers — color, cursor movement, screen control
4
+
5
+ var COLORS = {
6
+ red: '\x1b[31m',
7
+ green: '\x1b[32m',
8
+ yellow: '\x1b[33m',
9
+ blue: '\x1b[34m',
10
+ magenta: '\x1b[35m',
11
+ cyan: '\x1b[36m',
12
+ white: '\x1b[37m',
13
+ bright_red: '\x1b[91m',
14
+ bright_green: '\x1b[92m',
15
+ bright_yellow: '\x1b[93m',
16
+ bright_blue: '\x1b[94m',
17
+ bright_cyan: '\x1b[96m',
18
+ bright_white: '\x1b[97m',
19
+ reset: '\x1b[0m',
20
+ bold: '\x1b[1m',
21
+ dim: '\x1b[2m'
22
+ };
23
+
24
+ /**
25
+ * Wrap text in an ANSI color code.
26
+ * @param {string} text
27
+ * @param {string} colorName — key from COLORS, or null/undefined for no color
28
+ * @returns {string}
29
+ */
30
+ function colorize(text, colorName) {
31
+ if (!colorName || !COLORS[colorName]) return text;
32
+ return COLORS[colorName] + text + COLORS.reset;
33
+ }
34
+
35
+ /**
36
+ * Bold text.
37
+ * @param {string} text
38
+ * @returns {string}
39
+ */
40
+ function bold(text) {
41
+ return COLORS.bold + text + COLORS.reset;
42
+ }
43
+
44
+ /**
45
+ * Dim text.
46
+ * @param {string} text
47
+ * @returns {string}
48
+ */
49
+ function dim(text) {
50
+ return COLORS.dim + text + COLORS.reset;
51
+ }
52
+
53
+ /**
54
+ * Move cursor to top-left of screen (for live re-render).
55
+ * @returns {string}
56
+ */
57
+ function cursorHome() {
58
+ return '\x1b[H';
59
+ }
60
+
61
+ /**
62
+ * Clear the entire screen.
63
+ * @returns {string}
64
+ */
65
+ function clearScreen() {
66
+ return '\x1b[2J';
67
+ }
68
+
69
+ /**
70
+ * Clear from cursor to end of screen.
71
+ * @returns {string}
72
+ */
73
+ function clearToEnd() {
74
+ return '\x1b[J';
75
+ }
76
+
77
+ /**
78
+ * Hide the terminal cursor.
79
+ * @returns {string}
80
+ */
81
+ function hideCursor() {
82
+ return '\x1b[?25l';
83
+ }
84
+
85
+ /**
86
+ * Show the terminal cursor.
87
+ * @returns {string}
88
+ */
89
+ function showCursor() {
90
+ return '\x1b[?25h';
91
+ }
92
+
93
+ /**
94
+ * Move cursor up N lines.
95
+ * @param {number} n
96
+ * @returns {string}
97
+ */
98
+ function cursorUp(n) {
99
+ return '\x1b[' + n + 'A';
100
+ }
101
+
102
+ /**
103
+ * Move cursor to beginning of line.
104
+ * @returns {string}
105
+ */
106
+ function cursorLineStart() {
107
+ return '\r';
108
+ }
109
+
110
+ /**
111
+ * Erase current line.
112
+ * @returns {string}
113
+ */
114
+ function eraseLine() {
115
+ return '\x1b[2K';
116
+ }
117
+
118
+ module.exports = {
119
+ COLORS: COLORS,
120
+ colorize: colorize,
121
+ bold: bold,
122
+ dim: dim,
123
+ cursorHome: cursorHome,
124
+ clearScreen: clearScreen,
125
+ clearToEnd: clearToEnd,
126
+ hideCursor: hideCursor,
127
+ showCursor: showCursor,
128
+ cursorUp: cursorUp,
129
+ cursorLineStart: cursorLineStart,
130
+ eraseLine: eraseLine
131
+ };