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 +228 -0
- package/bin/pipechart.js +319 -0
- package/lib/ansi.js +131 -0
- package/lib/detect.js +177 -0
- package/lib/render/bar.js +131 -0
- package/lib/render/hist.js +151 -0
- package/lib/render/line.js +239 -0
- package/lib/render/spark.js +101 -0
- package/lib/scale.js +185 -0
- package/package.json +39 -0
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
|
+
[](https://www.npmjs.com/package/pipechart)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](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
|
package/bin/pipechart.js
ADDED
|
@@ -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
|
+
};
|