thistogram
Version:
A simple text based histogram and chart generator
139 lines • 6.68 kB
JavaScript
import { barHorizontal } from './bars.js';
import { BoxSymbol, boxSymbols, histoCharsLeftToRight } from './drawingCharacters.js';
const defaultDrawOptions = {
boxSymbols,
histoChars: histoCharsLeftToRight,
};
export function histogram(data, options) {
const { width = 80, maxLabelWidth: maxColumnWidth = Math.floor(width * 0.1), drawOptions: drawOptionsParam = defaultDrawOptions, title = '', min, max, showValues = true, significantDigits, type: chartType = 'bar', headers, } = { ...options };
const { boxSymbols = defaultDrawOptions.boxSymbols, histoChars = defaultDrawOptions.histoChars } = {
...drawOptionsParam,
};
const showMinMax = chartType === 'point-min-max' && showValues;
const [headerLabel = '', headerValue = 'Val', headerMin = (showMinMax && 'Min') || '', headerMax = (showMinMax && 'Max') || '',] = headers ?? ['', '', '', ''];
const vLine = boxSymbols[BoxSymbol.vertical];
const hLine = boxSymbols[BoxSymbol.horizontal];
const cross = boxSymbols[BoxSymbol.cross];
const values = data.map(([, value]) => value);
const labels = data.map(([label]) => String(label));
const needMinMaxValues = chartType === 'point-min-max';
const minValues = needMinMaxValues ? data.map(([, , min]) => min).filter((v) => v !== undefined) : [];
const maxValues = needMinMaxValues
? data.map(([, , , max]) => max).filter((v) => v !== undefined)
: [];
const allValues = [...values, ...minValues, ...maxValues];
const maxGraphValue = max ?? Math.max(...allValues);
const minGraphValue = min ?? Math.min(0, ...allValues);
const range = maxGraphValue - minGraphValue || 1;
const valueToString = (value) => value === undefined
? ''
: significantDigits !== undefined
? value.toFixed(significantDigits)
: value.toString();
const valuesToString = (values) => values.map(valueToString);
const calcColLabelLength = (label, values) => Math.min(Math.max(sLen(label), ...values.map(sLen)), maxColumnWidth);
const colWidthLabel = calcColLabelLength(headerLabel, labels);
const colWidthMin = calcColLabelLength(headerMin, valuesToString(minValues));
const colWidthMax = calcColLabelLength(headerMax, valuesToString(maxValues));
const colWidthValue = calcColLabelLength(headerValue, valuesToString(values));
const colWidths = [colWidthLabel, colWidthValue, colWidthMin, colWidthMax].filter((v) => v > 0);
const maxBarWidth = width - colWidths.reduce((a, b) => a + b, 0) - colWidths.length * 3 + 2;
const barValue = (value) => (Math.max(Math.min(value, maxGraphValue), minGraphValue) - minGraphValue) / range;
const colSep = ` ${vLine} `;
const headerSep = `${hLine}${cross}${hLine}`;
const lines = data.map(([label, value, minVal, maxVal]) => {
const labelValue = formatColValue(String(label), colWidthLabel, false);
const graphLine = chartType === 'bar'
? barHorizontal(barValue(value), maxBarWidth, ' ', histoChars)
: chartType === 'point'
? point(barValue(value), maxBarWidth, ' ')
: chartType === 'point-min-max'
? pointMinMax(barValue(value), minVal !== undefined ? barValue(minVal) : undefined, maxVal !== undefined ? barValue(maxVal) : undefined, maxBarWidth, ' ')
: '';
const cols = [
formatColValue(valueToString(value), colWidthValue, true),
formatColValue(valueToString(minVal), colWidthMin, true),
formatColValue(valueToString(maxVal), colWidthMax, true),
].filter((v) => !!v);
return `${labelValue} ${vLine}${graphLine}${vLine} ${cols.join(colSep)}`;
});
function formatHeader() {
if (!headers)
return '';
const label = formatColValue(headerLabel, colWidthLabel, false);
const cols = [
formatColValue(headerValue, colWidthValue, true),
formatColValue(headerMin, colWidthMin, true),
formatColValue(headerMax, colWidthMax, true),
].filter((v) => !!v);
return (`${label} ${vLine}${' '.repeat(maxBarWidth)}${vLine} ${cols.join(colSep)}\n` +
`${hLine.repeat(colWidthLabel + 1)}${cross}${hLine.repeat(maxBarWidth)}${cross}${hLine}${cols
.map((s) => hLine.repeat(sLen(s)))
.join(headerSep)}\n`);
}
const titleLine = title ? `${title}\n` : '';
const headerLines = formatHeader();
const table = `${titleLine}${headerLines}${lines.join('\n')}`;
return table;
}
/**
* Generate a point line for a value
* @param value - between 0 and 1 inclusive.
* @param width - the width of the graph in characters
* @param padding - optional padding character, defaults to space.
* @param pointChar - optional point character, defaults to `●`
* @returns a string of the point
*/
export function point(value, width, padding = ' ', pointChar = '●') {
const n = calc(value, width);
return pointChar.padStart(n, padding).padEnd(width, padding);
}
function minMaxCap(value, min = 0, max = 1) {
return Math.max(Math.min(value, max), min);
}
function calc(value, width) {
const v = minMaxCap(value);
return Math.floor(v * (width - 1) + 0.5);
}
export const valueMinMaxSymbols = [
'●',
boxSymbols[BoxSymbol.leftT],
boxSymbols[BoxSymbol.horizontal],
boxSymbols[BoxSymbol.rightT],
];
/**
*
* @param value - between 0 and 1 inclusive.
* @param minVal - the minimum value, defaults to the value
* @param maxVal - the maximum value, defaults to the value
* @param width - the width of the graph in characters
* @param padding - optional padding character, defaults to space.
* @param symbols - the symbols to use for the min, max, and value, defaults to `['●', '┣', '━', '┫']`
* @returns
*/
export function pointMinMax(value, minVal, maxVal, width, padding = ' ', symbols = valueMinMaxSymbols) {
const line = [...padding.repeat(width)];
const val = calc(value, width);
const min = calc(minVal ?? value, width);
const max = calc(maxVal ?? value, width);
line[min] = symbols[1];
line[max] = symbols[3];
for (let i = min + 1; i < max; i++) {
line[i] = symbols[2];
}
line[val] = symbols[0];
return line.join('');
}
function sLen(s) {
if (!s)
return 0;
return [...s].length;
}
function sliceStr(s, start, end) {
return [...s].slice(start, end).join('');
}
function formatColValue(s, width, padStart, padding = ' ') {
const ss = sliceStr(s, 0, width);
return padStart ? ss.padStart(width, padding) : ss.padEnd(width, padding);
}
//# sourceMappingURL=histogram.js.map