UNPKG

thistogram

Version:

A simple text based histogram and chart generator

139 lines 6.68 kB
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