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/lib/detect.js ADDED
@@ -0,0 +1,177 @@
1
+ 'use strict';
2
+
3
+ // JSON shape auto-detection
4
+ // Determines the best chart type for a given dataset
5
+
6
+ /**
7
+ * Check if a value is a finite number.
8
+ * @param {*} v
9
+ * @returns {boolean}
10
+ */
11
+ function isNum(v) {
12
+ return typeof v === 'number' && isFinite(v);
13
+ }
14
+
15
+ /**
16
+ * Check if an object looks like a time-value pair.
17
+ * Accepts { t, v }, { time, value }, { timestamp, value }, { x, y }
18
+ * @param {object} obj
19
+ * @returns {boolean}
20
+ */
21
+ function isTimeSeries(obj) {
22
+ if (!obj || typeof obj !== 'object') return false;
23
+ var hasTime = ('t' in obj) || ('time' in obj) || ('timestamp' in obj) || ('x' in obj);
24
+ var hasValue = ('v' in obj) || ('value' in obj) || ('y' in obj);
25
+ return hasTime && hasValue;
26
+ }
27
+
28
+ /**
29
+ * Extract the time value from a time-series object.
30
+ * @param {object} obj
31
+ * @returns {number|string}
32
+ */
33
+ function extractTime(obj) {
34
+ if ('t' in obj) return obj.t;
35
+ if ('time' in obj) return obj.time;
36
+ if ('timestamp' in obj) return obj.timestamp;
37
+ if ('x' in obj) return obj.x;
38
+ return null;
39
+ }
40
+
41
+ /**
42
+ * Extract the numeric value from a time-series object.
43
+ * @param {object} obj
44
+ * @returns {number}
45
+ */
46
+ function extractValue(obj) {
47
+ if ('v' in obj) return obj.v;
48
+ if ('value' in obj) return obj.value;
49
+ if ('y' in obj) return obj.y;
50
+ return null;
51
+ }
52
+
53
+ /**
54
+ * Find the first numeric field in an object.
55
+ * @param {object} obj
56
+ * @returns {string|null}
57
+ */
58
+ function findNumericField(obj) {
59
+ if (!obj || typeof obj !== 'object') return null;
60
+ var keys = Object.keys(obj);
61
+ for (var i = 0; i < keys.length; i++) {
62
+ if (isNum(obj[keys[i]])) return keys[i];
63
+ }
64
+ return null;
65
+ }
66
+
67
+ /**
68
+ * Detect the shape of the input data and return a descriptor.
69
+ *
70
+ * Returns an object:
71
+ * {
72
+ * type: 'bar' | 'line' | 'spark' | 'hist',
73
+ * values: number[], // extracted numeric values
74
+ * labels: string[] | null, // labels for bar chart
75
+ * times: any[] | null, // time axis for line chart
76
+ * field: string | null // which field was used
77
+ * }
78
+ *
79
+ * @param {*} data — parsed JSON input
80
+ * @param {object} opts — CLI options (type, field)
81
+ * @returns {object}
82
+ */
83
+ function detect(data, opts) {
84
+ opts = opts || {};
85
+
86
+ // ── Flat array of numbers ────────────────────────────────────────────────
87
+ if (Array.isArray(data) && data.length > 0 && isNum(data[0])) {
88
+ var allNums = data.every(function(v) { return isNum(v); });
89
+ if (allNums) {
90
+ var defaultType = opts.type || (data.length > 60 ? 'hist' : 'spark');
91
+ return {
92
+ type: defaultType,
93
+ values: data,
94
+ labels: null,
95
+ times: null,
96
+ field: null
97
+ };
98
+ }
99
+ }
100
+
101
+ // ── Array of objects ─────────────────────────────────────────────────────
102
+ if (Array.isArray(data) && data.length > 0 && typeof data[0] === 'object' && data[0] !== null) {
103
+
104
+ // Time-series: { t, v } or { time, value } etc.
105
+ if (!opts.field && isTimeSeries(data[0])) {
106
+ var tsValues = [];
107
+ var tsTimes = [];
108
+ var valid = true;
109
+ for (var i = 0; i < data.length; i++) {
110
+ var tv = extractValue(data[i]);
111
+ if (!isNum(tv)) { valid = false; break; }
112
+ tsValues.push(tv);
113
+ tsTimes.push(extractTime(data[i]));
114
+ }
115
+ if (valid) {
116
+ return {
117
+ type: opts.type || 'line',
118
+ values: tsValues,
119
+ labels: null,
120
+ times: tsTimes,
121
+ field: null
122
+ };
123
+ }
124
+ }
125
+
126
+ // Array of objects with a specified or auto-detected numeric field
127
+ var field = opts.field || findNumericField(data[0]);
128
+ if (field) {
129
+ var objValues = [];
130
+ var objLabels = [];
131
+ for (var j = 0; j < data.length; j++) {
132
+ var v = data[j][field];
133
+ if (!isNum(v)) continue;
134
+ objValues.push(v);
135
+ // Use 'name', 'label', 'key', 'id', or index as label
136
+ var label = data[j].name || data[j].label || data[j].key || data[j].id || String(j);
137
+ objLabels.push(String(label));
138
+ }
139
+ return {
140
+ type: opts.type || 'bar',
141
+ values: objValues,
142
+ labels: objLabels,
143
+ times: null,
144
+ field: field
145
+ };
146
+ }
147
+ }
148
+
149
+ // ── Single number ────────────────────────────────────────────────────────
150
+ if (isNum(data)) {
151
+ return {
152
+ type: opts.type || 'spark',
153
+ values: [data],
154
+ labels: null,
155
+ times: null,
156
+ field: null
157
+ };
158
+ }
159
+
160
+ // ── Fallback: try to extract any numbers we can find ────────────────────
161
+ return {
162
+ type: opts.type || 'spark',
163
+ values: [],
164
+ labels: null,
165
+ times: null,
166
+ field: null
167
+ };
168
+ }
169
+
170
+ module.exports = {
171
+ detect: detect,
172
+ isNum: isNum,
173
+ isTimeSeries: isTimeSeries,
174
+ extractTime: extractTime,
175
+ extractValue: extractValue,
176
+ findNumericField: findNumericField
177
+ };
@@ -0,0 +1,131 @@
1
+ 'use strict';
2
+
3
+ // Horizontal bar chart renderer
4
+
5
+ var ansi = require('../ansi');
6
+ var scale = require('../scale');
7
+
8
+ var BAR_CHAR = '\u2588'; // █
9
+ var HALF_BAR = '\u258C'; // ▌
10
+
11
+ /**
12
+ * Render a horizontal bar chart.
13
+ *
14
+ * @param {object} opts
15
+ * @param {number[]} opts.values — data values
16
+ * @param {string[]} opts.labels — bar labels (parallel to values)
17
+ * @param {number} opts.width — total terminal width
18
+ * @param {string} [opts.color] — ANSI color name
19
+ * @param {string} [opts.title] — chart title
20
+ * @returns {string} — multi-line string ready to print
21
+ */
22
+ function render(opts) {
23
+ var values = opts.values || [];
24
+ var labels = opts.labels || [];
25
+ var width = opts.width || 80;
26
+ var color = opts.color || 'cyan';
27
+ var title = opts.title || null;
28
+
29
+ if (values.length === 0) {
30
+ return ansi.colorize('(no data)', 'dim');
31
+ }
32
+
33
+ var lines = [];
34
+
35
+ // Title
36
+ if (title) {
37
+ lines.push(ansi.bold(ansi.colorize(title, color)));
38
+ lines.push('');
39
+ }
40
+
41
+ var minVal = scale.min(values);
42
+ var maxVal = scale.max(values);
43
+
44
+ // Determine label column width
45
+ var maxLabelLen = 0;
46
+ for (var i = 0; i < values.length; i++) {
47
+ var lbl = labels[i] !== undefined ? String(labels[i]) : String(i);
48
+ if (lbl.length > maxLabelLen) maxLabelLen = lbl.length;
49
+ }
50
+ // Cap label width at 20 chars
51
+ var labelWidth = Math.min(maxLabelLen, 20);
52
+
53
+ // Value suffix width: " 12345.67"
54
+ var maxValStr = scale.formatNum(maxVal);
55
+ var valWidth = maxValStr.length + 1; // +1 for space
56
+
57
+ // Available bar width
58
+ var barAreaWidth = width - labelWidth - valWidth - 3; // 3 = " │ "
59
+ if (barAreaWidth < 4) barAreaWidth = 4;
60
+
61
+ // Axis info
62
+ var axisMin = Math.min(0, minVal);
63
+ var axisMax = maxVal;
64
+ if (axisMax === axisMin) axisMax = axisMin + 1;
65
+
66
+ // Zero line position (for negative values)
67
+ var zeroPos = scale.mapToRange(0, axisMin, axisMax, barAreaWidth);
68
+
69
+ for (var j = 0; j < values.length; j++) {
70
+ var val = values[j];
71
+ var label = labels[j] !== undefined ? String(labels[j]) : String(j);
72
+ label = scale.truncate(label, labelWidth);
73
+ label = scale.padRight(label, labelWidth);
74
+
75
+ var barLen = scale.mapToRange(Math.max(val, 0), axisMin, axisMax, barAreaWidth);
76
+ var negLen = val < 0 ? scale.mapToRange(Math.abs(val), 0, axisMax - axisMin, barAreaWidth) : 0;
77
+
78
+ var bar;
79
+ if (val >= 0) {
80
+ // Positive bar: spaces up to zero, then bar
81
+ var prefix = zeroPos > 0 ? scale.padRight('', zeroPos) : '';
82
+ bar = prefix + ansi.colorize(repeat(BAR_CHAR, barLen), color);
83
+ } else {
84
+ // Negative bar: spaces, then bar going left, then zero marker
85
+ var negBar = ansi.colorize(repeat(BAR_CHAR, negLen), 'red');
86
+ var afterNeg = scale.padRight('', zeroPos - negLen);
87
+ bar = afterNeg + negBar;
88
+ }
89
+
90
+ // Pad bar area to full width
91
+ var rawBarLen = (val >= 0 ? zeroPos + barLen : zeroPos) ;
92
+ var padding = barAreaWidth - rawBarLen;
93
+ if (padding < 0) padding = 0;
94
+ bar = bar + scale.padRight('', padding);
95
+
96
+ var valStr = scale.padLeft(scale.formatNum(val), valWidth);
97
+
98
+ lines.push(
99
+ ansi.dim(label) + ' \u2502 ' + bar + ansi.dim(valStr)
100
+ );
101
+ }
102
+
103
+ // Bottom axis
104
+ var axisLine = scale.padRight('', labelWidth) + ' \u2514' + repeat('\u2500', barAreaWidth + 1);
105
+ lines.push(ansi.dim(axisLine));
106
+
107
+ // Axis labels: min and max
108
+ var minLabel = scale.formatNum(axisMin);
109
+ var maxLabel = scale.formatNum(axisMax);
110
+ var axisLabels = scale.padRight('', labelWidth + 2) +
111
+ minLabel +
112
+ scale.padRight('', barAreaWidth - minLabel.length - maxLabel.length) +
113
+ maxLabel;
114
+ lines.push(ansi.dim(axisLabels));
115
+
116
+ return lines.join('\n');
117
+ }
118
+
119
+ /**
120
+ * Repeat a character n times.
121
+ * @param {string} ch
122
+ * @param {number} n
123
+ * @returns {string}
124
+ */
125
+ function repeat(ch, n) {
126
+ var s = '';
127
+ for (var i = 0; i < n; i++) s += ch;
128
+ return s;
129
+ }
130
+
131
+ module.exports = { render: render };
@@ -0,0 +1,151 @@
1
+ 'use strict';
2
+
3
+ // Histogram renderer — vertical bars using █ blocks, auto-bins
4
+
5
+ var ansi = require('../ansi');
6
+ var scale = require('../scale');
7
+
8
+ var FULL_BLOCK = '\u2588'; // █
9
+ var UPPER_HALF = '\u2580'; // ▀ (top half block, for finer resolution)
10
+ var LOWER_HALF = '\u2584'; // ▄
11
+
12
+ /**
13
+ * Render a histogram.
14
+ *
15
+ * @param {object} opts
16
+ * @param {number[]} opts.values — raw data values (will be binned)
17
+ * @param {number} opts.width — terminal width
18
+ * @param {string} [opts.color] — ANSI color name
19
+ * @param {string} [opts.title] — chart title
20
+ * @param {number} [opts.height] — chart height in rows (default: 12)
21
+ * @param {number} [opts.bins] — number of bins (default: auto)
22
+ * @returns {string}
23
+ */
24
+ function render(opts) {
25
+ var values = opts.values || [];
26
+ var width = opts.width || 80;
27
+ var color = opts.color || 'yellow';
28
+ var title = opts.title || null;
29
+ var height = opts.height || 12;
30
+
31
+ if (values.length === 0) {
32
+ return ansi.colorize('(no data)', 'dim');
33
+ }
34
+
35
+ // Y-axis label width
36
+ var maxCount = 0; // will be set after binning
37
+
38
+ // Auto-determine bin count: Sturges' rule, capped to available width
39
+ var yLabelWidth = 5; // placeholder, recalculated below
40
+ var plotWidth = width - yLabelWidth - 2;
41
+ if (plotWidth < 4) plotWidth = 4;
42
+
43
+ var binCount = opts.bins || Math.min(
44
+ Math.max(Math.ceil(Math.log2(values.length) + 1), 5),
45
+ plotWidth
46
+ );
47
+
48
+ var bins = scale.autoBin(values, binCount);
49
+
50
+ // Find max count for scaling
51
+ for (var i = 0; i < bins.length; i++) {
52
+ if (bins[i].count > maxCount) maxCount = bins[i].count;
53
+ }
54
+
55
+ // Recalculate y-axis label width based on actual max count
56
+ yLabelWidth = String(maxCount).length + 1;
57
+ plotWidth = width - yLabelWidth - 2;
58
+ if (plotWidth < 4) plotWidth = 4;
59
+
60
+ // Each bin gets at least 1 column; distribute evenly
61
+ var colsPerBin = Math.max(1, Math.floor(plotWidth / bins.length));
62
+ // Actual used width
63
+ var usedWidth = colsPerBin * bins.length;
64
+
65
+ var lines = [];
66
+
67
+ if (title) {
68
+ lines.push(ansi.bold(ansi.colorize(title, color)));
69
+ lines.push('');
70
+ }
71
+
72
+ // Build grid row by row (top to bottom)
73
+ for (var row = 0; row < height; row++) {
74
+ // What count value does this row represent?
75
+ // row 0 = top = maxCount, row height-1 = bottom = 0
76
+ var rowThreshold = maxCount * (1 - row / (height - 1));
77
+
78
+ // Y-axis label
79
+ var yLabel = '';
80
+ if (row === 0) {
81
+ yLabel = String(maxCount);
82
+ } else if (row === Math.floor(height / 2)) {
83
+ yLabel = String(Math.round(maxCount / 2));
84
+ } else if (row === height - 1) {
85
+ yLabel = '0';
86
+ }
87
+ yLabel = scale.padLeft(yLabel, yLabelWidth);
88
+
89
+ var rowStr = '';
90
+ for (var b = 0; b < bins.length; b++) {
91
+ var binHeight = (bins[b].count / maxCount) * height;
92
+ var filledRows = height - Math.ceil(binHeight); // rows from top that are empty
93
+
94
+ var cellChar;
95
+ if (row < filledRows) {
96
+ // Empty above the bar
97
+ cellChar = repeat(' ', colsPerBin);
98
+ } else {
99
+ // Filled bar cell
100
+ cellChar = ansi.colorize(repeat(FULL_BLOCK, colsPerBin), color);
101
+ }
102
+ rowStr += cellChar;
103
+ }
104
+
105
+ // Pad to usedWidth if needed
106
+ var rawLen = bins.length * colsPerBin;
107
+ if (rawLen < plotWidth) {
108
+ rowStr += repeat(' ', plotWidth - rawLen);
109
+ }
110
+
111
+ lines.push(ansi.dim(yLabel) + ' \u2502' + rowStr);
112
+ }
113
+
114
+ // Bottom axis
115
+ var axisLine = scale.padRight('', yLabelWidth) + ' \u2514' + repeat('\u2500', usedWidth);
116
+ lines.push(ansi.dim(axisLine));
117
+
118
+ // Bin range labels: first bin min and last bin max
119
+ var minVal = scale.min(values);
120
+ var maxVal = scale.max(values);
121
+ var minLabel = scale.formatNum(minVal);
122
+ var maxLabel = scale.formatNum(maxVal);
123
+ var midLabel = scale.formatNum((minVal + maxVal) / 2);
124
+
125
+ var labelLine = scale.padRight('', yLabelWidth + 2) +
126
+ minLabel +
127
+ scale.padRight('', Math.floor(usedWidth / 2) - minLabel.length - Math.floor(midLabel.length / 2)) +
128
+ midLabel +
129
+ scale.padRight('', usedWidth - Math.floor(usedWidth / 2) - Math.ceil(midLabel.length / 2) - maxLabel.length) +
130
+ maxLabel;
131
+ lines.push(ansi.dim(labelLine));
132
+
133
+ // Count label
134
+ lines.push(ansi.dim(scale.padRight('', yLabelWidth + 2) + 'n=' + values.length + ' bins=' + binCount));
135
+
136
+ return lines.join('\n');
137
+ }
138
+
139
+ /**
140
+ * Repeat a character n times.
141
+ * @param {string} ch
142
+ * @param {number} n
143
+ * @returns {string}
144
+ */
145
+ function repeat(ch, n) {
146
+ var s = '';
147
+ for (var i = 0; i < n; i++) s += ch;
148
+ return s;
149
+ }
150
+
151
+ module.exports = { render: render };
@@ -0,0 +1,239 @@
1
+ 'use strict';
2
+
3
+ // Line chart renderer — plots points across terminal width
4
+ // Connects adjacent points with ASCII slope characters
5
+
6
+ var ansi = require('../ansi');
7
+ var scale = require('../scale');
8
+
9
+ // Characters used to draw the line
10
+ var CHARS = {
11
+ dot: '\u2022', // • point
12
+ horiz: '\u2500', // ─ flat
13
+ up: '/', // rising
14
+ down: '\\', // falling
15
+ vert: '\u2502', // │ vertical
16
+ cross: '\u253C', // ┼ axis cross
17
+ axisH: '\u2500', // ─
18
+ axisV: '\u2502', // │
19
+ corner: '\u2514', // └
20
+ tickH: '\u252C', // ┬
21
+ tickV: '\u251C' // ├
22
+ };
23
+
24
+ /**
25
+ * Render a line chart.
26
+ *
27
+ * @param {object} opts
28
+ * @param {number[]} opts.values — data values
29
+ * @param {any[]} [opts.times] — time labels (parallel to values)
30
+ * @param {number} opts.width — terminal width
31
+ * @param {string} [opts.color] — ANSI color name
32
+ * @param {string} [opts.title] — chart title
33
+ * @param {number} [opts.height] — chart height in rows (default: 12)
34
+ * @returns {string}
35
+ */
36
+ function render(opts) {
37
+ var values = opts.values || [];
38
+ var times = opts.times || null;
39
+ var width = opts.width || 80;
40
+ var color = opts.color || 'cyan';
41
+ var title = opts.title || null;
42
+ var height = opts.height || 12;
43
+
44
+ if (values.length === 0) {
45
+ return ansi.colorize('(no data)', 'dim');
46
+ }
47
+
48
+ var minVal = scale.min(values);
49
+ var maxVal = scale.max(values);
50
+ // Give a little padding so points don't sit on the very edge
51
+ var range = maxVal - minVal;
52
+ if (range === 0) range = 1;
53
+
54
+ // Y-axis label width
55
+ var yLabelWidth = Math.max(
56
+ scale.formatNum(minVal).length,
57
+ scale.formatNum(maxVal).length
58
+ ) + 1;
59
+
60
+ // Available plot width (after y-axis)
61
+ var plotWidth = width - yLabelWidth - 2; // 2 = "│ "
62
+ if (plotWidth < 4) plotWidth = 4;
63
+
64
+ // Downsample or map values to plotWidth columns
65
+ var plotValues = values;
66
+ if (values.length > plotWidth) {
67
+ plotValues = downsample(values, plotWidth);
68
+ }
69
+
70
+ var cols = plotValues.length;
71
+
72
+ // Build a 2D grid: grid[row][col] = char or ' '
73
+ var grid = [];
74
+ for (var r = 0; r < height; r++) {
75
+ var row = [];
76
+ for (var c = 0; c < plotWidth; c++) {
77
+ row.push(' ');
78
+ }
79
+ grid.push(row);
80
+ }
81
+
82
+ /**
83
+ * Map a data value to a grid row (0 = top, height-1 = bottom).
84
+ */
85
+ function valueToRow(v) {
86
+ var norm = scale.normalize(v, minVal - range * 0.05, maxVal + range * 0.05);
87
+ var row = Math.round((1 - norm) * (height - 1));
88
+ if (row < 0) row = 0;
89
+ if (row >= height) row = height - 1;
90
+ return row;
91
+ }
92
+
93
+ // Plot points and connecting lines
94
+ for (var i = 0; i < cols; i++) {
95
+ var col = Math.floor(i * plotWidth / cols);
96
+ if (col >= plotWidth) col = plotWidth - 1;
97
+
98
+ var row = valueToRow(plotValues[i]);
99
+
100
+ if (i === 0) {
101
+ grid[row][col] = CHARS.dot;
102
+ } else {
103
+ var prevCol = Math.floor((i - 1) * plotWidth / cols);
104
+ if (prevCol >= plotWidth) prevCol = plotWidth - 1;
105
+ var prevRow = valueToRow(plotValues[i - 1]);
106
+
107
+ // Draw the point
108
+ grid[row][col] = CHARS.dot;
109
+
110
+ // Connect with slope character
111
+ if (prevRow === row) {
112
+ // Flat — fill horizontal between prevCol+1 and col-1
113
+ for (var fc = prevCol + 1; fc < col; fc++) {
114
+ if (grid[row][fc] === ' ') grid[row][fc] = CHARS.horiz;
115
+ }
116
+ } else {
117
+ // Sloped — draw diagonal connector
118
+ var rowStep = prevRow < row ? 1 : -1;
119
+ var colStep = col > prevCol ? 1 : -1;
120
+ var dr = Math.abs(row - prevRow);
121
+ var dc = Math.abs(col - prevCol);
122
+ var ch = rowStep > 0 ? CHARS.down : CHARS.up;
123
+
124
+ if (dc === 0) {
125
+ // Vertical segment
126
+ for (var vr = prevRow + rowStep; vr !== row; vr += rowStep) {
127
+ if (grid[vr][col] === ' ') grid[vr][col] = CHARS.vert;
128
+ }
129
+ } else {
130
+ // Bresenham-like diagonal
131
+ var err = 0;
132
+ var cr = prevRow;
133
+ var cc = prevCol;
134
+ for (var step = 0; step < dc + dr; step++) {
135
+ err += dr;
136
+ if (err * 2 >= dc) {
137
+ cr += rowStep;
138
+ err -= dc;
139
+ } else {
140
+ cc += colStep;
141
+ }
142
+ if (cr !== row || cc !== col) {
143
+ if (cc >= 0 && cc < plotWidth && cr >= 0 && cr < height) {
144
+ if (grid[cr][cc] === ' ') grid[cr][cc] = ch;
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ // Render lines
154
+ var lines = [];
155
+
156
+ if (title) {
157
+ lines.push(ansi.bold(ansi.colorize(title, color)));
158
+ lines.push('');
159
+ }
160
+
161
+ for (var row2 = 0; row2 < height; row2++) {
162
+ // Y-axis label (only at top, middle, bottom)
163
+ var yLabel = '';
164
+ if (row2 === 0) {
165
+ yLabel = scale.formatNum(maxVal);
166
+ } else if (row2 === Math.floor(height / 2)) {
167
+ yLabel = scale.formatNum((minVal + maxVal) / 2);
168
+ } else if (row2 === height - 1) {
169
+ yLabel = scale.formatNum(minVal);
170
+ }
171
+ yLabel = scale.padLeft(yLabel, yLabelWidth);
172
+
173
+ var rowStr = '';
174
+ for (var col2 = 0; col2 < plotWidth; col2++) {
175
+ var ch2 = grid[row2][col2];
176
+ if (ch2 !== ' ') {
177
+ rowStr += ansi.colorize(ch2, color);
178
+ } else {
179
+ rowStr += ' ';
180
+ }
181
+ }
182
+
183
+ lines.push(ansi.dim(yLabel) + ' ' + CHARS.axisV + rowStr);
184
+ }
185
+
186
+ // X axis
187
+ var xAxisLine = scale.padRight('', yLabelWidth) + ' ' + CHARS.corner + repeat(CHARS.axisH, plotWidth);
188
+ lines.push(ansi.dim(xAxisLine));
189
+
190
+ // X axis labels (first, middle, last time label)
191
+ if (times && times.length > 0) {
192
+ var t0 = String(times[0]);
193
+ var tMid = String(times[Math.floor(times.length / 2)]);
194
+ var tEnd = String(times[times.length - 1]);
195
+ var xLabels = scale.padRight('', yLabelWidth + 2) +
196
+ t0 +
197
+ scale.padRight('', Math.floor(plotWidth / 2) - t0.length - Math.floor(tMid.length / 2)) +
198
+ tMid +
199
+ scale.padRight('', plotWidth - Math.floor(plotWidth / 2) - Math.ceil(tMid.length / 2) - tEnd.length) +
200
+ tEnd;
201
+ lines.push(ansi.dim(xLabels));
202
+ }
203
+
204
+ return lines.join('\n');
205
+ }
206
+
207
+ /**
208
+ * Downsample an array to targetLen by averaging buckets.
209
+ * @param {number[]} values
210
+ * @param {number} targetLen
211
+ * @returns {number[]}
212
+ */
213
+ function downsample(values, targetLen) {
214
+ var result = [];
215
+ var bucketSize = values.length / targetLen;
216
+ for (var i = 0; i < targetLen; i++) {
217
+ var start = Math.floor(i * bucketSize);
218
+ var end = Math.floor((i + 1) * bucketSize);
219
+ if (end > values.length) end = values.length;
220
+ var sum = 0;
221
+ for (var j = start; j < end; j++) sum += values[j];
222
+ result.push(sum / (end - start));
223
+ }
224
+ return result;
225
+ }
226
+
227
+ /**
228
+ * Repeat a character n times.
229
+ * @param {string} ch
230
+ * @param {number} n
231
+ * @returns {string}
232
+ */
233
+ function repeat(ch, n) {
234
+ var s = '';
235
+ for (var i = 0; i < n; i++) s += ch;
236
+ return s;
237
+ }
238
+
239
+ module.exports = { render: render };