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
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Sparkline renderer — single-line inline chart using Unicode block chars
|
|
4
|
+
|
|
5
|
+
var ansi = require('../ansi');
|
|
6
|
+
var scale = require('../scale');
|
|
7
|
+
|
|
8
|
+
// Unicode block elements from lowest to highest
|
|
9
|
+
var SPARKS = [
|
|
10
|
+
'\u2581', // ▁ 1/8
|
|
11
|
+
'\u2582', // ▂ 2/8
|
|
12
|
+
'\u2583', // ▃ 3/8
|
|
13
|
+
'\u2584', // ▄ 4/8
|
|
14
|
+
'\u2585', // ▅ 5/8
|
|
15
|
+
'\u2586', // ▆ 6/8
|
|
16
|
+
'\u2587', // ▇ 7/8
|
|
17
|
+
'\u2588' // █ 8/8
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Convert a single normalized value [0,1] to a spark character.
|
|
22
|
+
* @param {number} normalized — value in [0, 1]
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
function toSparkChar(normalized) {
|
|
26
|
+
var idx = Math.round(normalized * (SPARKS.length - 1));
|
|
27
|
+
if (idx < 0) idx = 0;
|
|
28
|
+
if (idx >= SPARKS.length) idx = SPARKS.length - 1;
|
|
29
|
+
return SPARKS[idx];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Render a sparkline.
|
|
34
|
+
*
|
|
35
|
+
* @param {object} opts
|
|
36
|
+
* @param {number[]} opts.values — data values
|
|
37
|
+
* @param {number} opts.width — max terminal width (chars)
|
|
38
|
+
* @param {string} [opts.color] — ANSI color name
|
|
39
|
+
* @param {string} [opts.title] — optional title prefix
|
|
40
|
+
* @returns {string} — single line (or title + line)
|
|
41
|
+
*/
|
|
42
|
+
function render(opts) {
|
|
43
|
+
var values = opts.values || [];
|
|
44
|
+
var width = opts.width || 80;
|
|
45
|
+
var color = opts.color || 'green';
|
|
46
|
+
var title = opts.title || null;
|
|
47
|
+
|
|
48
|
+
if (values.length === 0) {
|
|
49
|
+
return ansi.colorize('(no data)', 'dim');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
var minVal = scale.min(values);
|
|
53
|
+
var maxVal = scale.max(values);
|
|
54
|
+
|
|
55
|
+
// If we have more values than width, downsample by averaging buckets
|
|
56
|
+
var displayValues = values;
|
|
57
|
+
if (values.length > width) {
|
|
58
|
+
displayValues = downsample(values, width);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
var spark = '';
|
|
62
|
+
for (var i = 0; i < displayValues.length; i++) {
|
|
63
|
+
var norm = scale.normalize(displayValues[i], minVal, maxVal);
|
|
64
|
+
spark += toSparkChar(norm);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
var colored = ansi.colorize(spark, color);
|
|
68
|
+
|
|
69
|
+
var minStr = ansi.dim(scale.formatNum(minVal));
|
|
70
|
+
var maxStr = ansi.dim(scale.formatNum(maxVal));
|
|
71
|
+
|
|
72
|
+
var lines = [];
|
|
73
|
+
if (title) {
|
|
74
|
+
lines.push(ansi.bold(ansi.colorize(title, color)));
|
|
75
|
+
}
|
|
76
|
+
lines.push(colored + ' ' + minStr + ' \u2013 ' + maxStr);
|
|
77
|
+
|
|
78
|
+
return lines.join('\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Downsample an array to targetLen by averaging buckets.
|
|
83
|
+
* @param {number[]} values
|
|
84
|
+
* @param {number} targetLen
|
|
85
|
+
* @returns {number[]}
|
|
86
|
+
*/
|
|
87
|
+
function downsample(values, targetLen) {
|
|
88
|
+
var result = [];
|
|
89
|
+
var bucketSize = values.length / targetLen;
|
|
90
|
+
for (var i = 0; i < targetLen; i++) {
|
|
91
|
+
var start = Math.floor(i * bucketSize);
|
|
92
|
+
var end = Math.floor((i + 1) * bucketSize);
|
|
93
|
+
if (end > values.length) end = values.length;
|
|
94
|
+
var sum = 0;
|
|
95
|
+
for (var j = start; j < end; j++) sum += values[j];
|
|
96
|
+
result.push(sum / (end - start));
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { render: render, toSparkChar: toSparkChar, downsample: downsample };
|
package/lib/scale.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Normalization and scaling math helpers
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Find the minimum value in an array of numbers.
|
|
7
|
+
* @param {number[]} values
|
|
8
|
+
* @returns {number}
|
|
9
|
+
*/
|
|
10
|
+
function min(values) {
|
|
11
|
+
var m = Infinity;
|
|
12
|
+
for (var i = 0; i < values.length; i++) {
|
|
13
|
+
if (values[i] < m) m = values[i];
|
|
14
|
+
}
|
|
15
|
+
return m;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Find the maximum value in an array of numbers.
|
|
20
|
+
* @param {number[]} values
|
|
21
|
+
* @returns {number}
|
|
22
|
+
*/
|
|
23
|
+
function max(values) {
|
|
24
|
+
var m = -Infinity;
|
|
25
|
+
for (var i = 0; i < values.length; i++) {
|
|
26
|
+
if (values[i] > m) m = values[i];
|
|
27
|
+
}
|
|
28
|
+
return m;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Normalize a value from [minVal, maxVal] to [0, 1].
|
|
33
|
+
* Returns 0 if minVal === maxVal (flat line).
|
|
34
|
+
* @param {number} value
|
|
35
|
+
* @param {number} minVal
|
|
36
|
+
* @param {number} maxVal
|
|
37
|
+
* @returns {number}
|
|
38
|
+
*/
|
|
39
|
+
function normalize(value, minVal, maxVal) {
|
|
40
|
+
if (maxVal === minVal) return 0;
|
|
41
|
+
return (value - minVal) / (maxVal - minVal);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Scale a normalized value [0,1] to [0, range].
|
|
46
|
+
* @param {number} normalized — value in [0, 1]
|
|
47
|
+
* @param {number} range — target integer range (e.g. terminal width)
|
|
48
|
+
* @returns {number} — integer in [0, range]
|
|
49
|
+
*/
|
|
50
|
+
function scaleToRange(normalized, range) {
|
|
51
|
+
return Math.round(normalized * range);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Map a value from [minVal, maxVal] directly to [0, range].
|
|
56
|
+
* @param {number} value
|
|
57
|
+
* @param {number} minVal
|
|
58
|
+
* @param {number} maxVal
|
|
59
|
+
* @param {number} range
|
|
60
|
+
* @returns {number}
|
|
61
|
+
*/
|
|
62
|
+
function mapToRange(value, minVal, maxVal, range) {
|
|
63
|
+
return scaleToRange(normalize(value, minVal, maxVal), range);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Compute a "nice" tick step for axis labels.
|
|
68
|
+
* @param {number} range — the data range (max - min)
|
|
69
|
+
* @param {number} ticks — desired number of ticks
|
|
70
|
+
* @returns {number}
|
|
71
|
+
*/
|
|
72
|
+
function niceStep(range, ticks) {
|
|
73
|
+
if (range === 0) return 1;
|
|
74
|
+
var rawStep = range / ticks;
|
|
75
|
+
var magnitude = Math.pow(10, Math.floor(Math.log(rawStep) / Math.LN10));
|
|
76
|
+
var normalized = rawStep / magnitude;
|
|
77
|
+
var nice;
|
|
78
|
+
if (normalized < 1.5) nice = 1;
|
|
79
|
+
else if (normalized < 3) nice = 2;
|
|
80
|
+
else if (normalized < 7) nice = 5;
|
|
81
|
+
else nice = 10;
|
|
82
|
+
return nice * magnitude;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Format a number for axis display — trim unnecessary decimals.
|
|
87
|
+
* @param {number} value
|
|
88
|
+
* @param {number} [decimals=2]
|
|
89
|
+
* @returns {string}
|
|
90
|
+
*/
|
|
91
|
+
function formatNum(value, decimals) {
|
|
92
|
+
if (decimals === undefined) decimals = 2;
|
|
93
|
+
if (Number.isInteger(value)) return String(value);
|
|
94
|
+
var s = value.toFixed(decimals);
|
|
95
|
+
// Remove trailing zeros after decimal point
|
|
96
|
+
s = s.replace(/\.?0+$/, '');
|
|
97
|
+
return s;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Auto-bin an array of numbers into `binCount` histogram bins.
|
|
102
|
+
* Returns an array of { min, max, count } objects.
|
|
103
|
+
* @param {number[]} values
|
|
104
|
+
* @param {number} binCount
|
|
105
|
+
* @returns {Array<{min: number, max: number, count: number}>}
|
|
106
|
+
*/
|
|
107
|
+
function autoBin(values, binCount) {
|
|
108
|
+
if (!values || values.length === 0) return [];
|
|
109
|
+
var minVal = min(values);
|
|
110
|
+
var maxVal = max(values);
|
|
111
|
+
if (minVal === maxVal) {
|
|
112
|
+
return [{ min: minVal, max: maxVal, count: values.length }];
|
|
113
|
+
}
|
|
114
|
+
var binWidth = (maxVal - minVal) / binCount;
|
|
115
|
+
var bins = [];
|
|
116
|
+
for (var i = 0; i < binCount; i++) {
|
|
117
|
+
bins.push({
|
|
118
|
+
min: minVal + i * binWidth,
|
|
119
|
+
max: minVal + (i + 1) * binWidth,
|
|
120
|
+
count: 0
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
for (var j = 0; j < values.length; j++) {
|
|
124
|
+
var v = values[j];
|
|
125
|
+
var idx = Math.floor((v - minVal) / binWidth);
|
|
126
|
+
// Clamp last value into last bin
|
|
127
|
+
if (idx >= binCount) idx = binCount - 1;
|
|
128
|
+
bins[idx].count++;
|
|
129
|
+
}
|
|
130
|
+
return bins;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Pad a string on the left to a given width.
|
|
135
|
+
* @param {string} s
|
|
136
|
+
* @param {number} width
|
|
137
|
+
* @param {string} [char=' ']
|
|
138
|
+
* @returns {string}
|
|
139
|
+
*/
|
|
140
|
+
function padLeft(s, width, char) {
|
|
141
|
+
if (char === undefined) char = ' ';
|
|
142
|
+
s = String(s);
|
|
143
|
+
while (s.length < width) s = char + s;
|
|
144
|
+
return s;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Pad a string on the right to a given width.
|
|
149
|
+
* @param {string} s
|
|
150
|
+
* @param {number} width
|
|
151
|
+
* @param {string} [char=' ']
|
|
152
|
+
* @returns {string}
|
|
153
|
+
*/
|
|
154
|
+
function padRight(s, width, char) {
|
|
155
|
+
if (char === undefined) char = ' ';
|
|
156
|
+
s = String(s);
|
|
157
|
+
while (s.length < width) s = s + char;
|
|
158
|
+
return s;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Truncate a string to maxLen, appending '…' if truncated.
|
|
163
|
+
* @param {string} s
|
|
164
|
+
* @param {number} maxLen
|
|
165
|
+
* @returns {string}
|
|
166
|
+
*/
|
|
167
|
+
function truncate(s, maxLen) {
|
|
168
|
+
s = String(s);
|
|
169
|
+
if (s.length <= maxLen) return s;
|
|
170
|
+
return s.slice(0, maxLen - 1) + '\u2026';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = {
|
|
174
|
+
min: min,
|
|
175
|
+
max: max,
|
|
176
|
+
normalize: normalize,
|
|
177
|
+
scaleToRange: scaleToRange,
|
|
178
|
+
mapToRange: mapToRange,
|
|
179
|
+
niceStep: niceStep,
|
|
180
|
+
formatNum: formatNum,
|
|
181
|
+
autoBin: autoBin,
|
|
182
|
+
padLeft: padLeft,
|
|
183
|
+
padRight: padRight,
|
|
184
|
+
truncate: truncate
|
|
185
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pipechart",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zero-dependency CLI tool that pipes JSON to real-time ASCII charts in the terminal",
|
|
5
|
+
"main": "bin/pipechart.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pipechart": "./bin/pipechart.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node bin/termviz.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"cli",
|
|
14
|
+
"chart",
|
|
15
|
+
"ascii",
|
|
16
|
+
"terminal",
|
|
17
|
+
"json",
|
|
18
|
+
"visualization",
|
|
19
|
+
"sparkline",
|
|
20
|
+
"histogram",
|
|
21
|
+
"bar-chart",
|
|
22
|
+
"line-chart",
|
|
23
|
+
"ansi"
|
|
24
|
+
],
|
|
25
|
+
"author": "",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"dependencies": {},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=12"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/yourusername/pipechart.git"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/yourusername/pipechart/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/yourusername/pipechart#readme"
|
|
39
|
+
}
|