UNPKG

@teaui/core

Version:

A high-level terminal UI library for Node

122 lines 4.57 kB
import { Point, interpolate } from '../../geometry.js'; import { Chart } from './Chart.js'; // Block characters for sub-cell height resolution (8 levels per row) // Index 0 = empty, 1 = ▁ (1/8), 2 = ▂ (2/8), ... 8 = █ (full) const BLOCKS = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']; export class BarChart extends Chart { #extract; #xLabelsFn; #yLabelsFn; #barWidth; #gap; constructor(data, props) { const { extract, xLabels, yLabels, barWidth, gap, ...rest } = props; super(data, rest); this.#extract = extract; this.#xLabelsFn = xLabels; this.#yLabelsFn = yLabels; this.#barWidth = barWidth ?? 1; this.#gap = gap ?? 0; } getXRange() { return { min: 0, max: Math.max(1, this.data.length - 1) }; } getYRange() { if (this.data.length === 0) return { min: 0, max: 1 }; let min = 0; // bars always start from 0 let max = -Infinity; for (const row of this.data) { const y = this.#extract(row); if (y > max) max = y; } if (max <= 0) max = 1; return { min, max }; } getXLabels() { if (!this.#xLabelsFn) return []; return this.data.map(this.#xLabelsFn); } getYLabels(count) { if (!this.#yLabelsFn) { return defaultYLabels(this.getYRange(), count); } const range = this.getYRange(); const labels = []; for (let i = 0; i < count; i++) { const value = interpolate(i, [0, count - 1], [range.max, range.min]); labels.push(this.#yLabelsFn(value)); } return labels; } renderChart(viewport, layout) { if (viewport.isEmpty || this.data.length === 0) return; const style = this.chartStyle ?? this.purpose.ui({ isHover: true }).invert(); const emptyStyle = this.purpose.text(); const totalBarSlots = this.data.length; const totalWidth = layout.width; const totalHeight = layout.height; // Calculate bar positions const barStep = this.#barWidth + this.#gap; const usedWidth = totalBarSlots * barStep - this.#gap; const startX = Math.max(0, Math.floor((totalWidth - usedWidth) / 2)); // Each terminal row gives 8 sub-levels of height via block chars const maxSubRows = totalHeight * 8; const pt = new Point(0, 0).mutableCopy(); for (let i = 0; i < totalBarSlots; i++) { const value = this.#extract(this.data[i]); const barSubHeight = Math.round(interpolate(value, [layout.yRange.min, layout.yRange.max], [0, maxSubRows], true)); const barX = startX + i * barStep; // For each column of this bar for (let bx = 0; bx < this.#barWidth; bx++) { const x = barX + bx; if (x >= totalWidth) break; // Fill from bottom to top // Number of completely full rows const fullRows = Math.floor(barSubHeight / 8); // Fractional part for the topmost partial row const partialEighths = barSubHeight % 8; for (let row = 0; row < totalHeight; row++) { pt.x = x; pt.y = row; const rowFromBottom = totalHeight - 1 - row; if (rowFromBottom < fullRows) { // Full block viewport.write(BLOCKS[8], pt, style); } else if (rowFromBottom === fullRows && partialEighths > 0) { // Partial block viewport.write(BLOCKS[partialEighths], pt, style); } else { // Empty viewport.write(' ', pt, emptyStyle); } } } } } } function defaultYLabels(range, count) { const labels = []; for (let i = 0; i < count; i++) { const value = interpolate(i, [0, count - 1], [range.max, range.min]); labels.push(formatNumber(value)); } return labels; } function formatNumber(n) { if (Number.isInteger(n)) return String(n); if (Math.abs(n) >= 100) return String(Math.round(n)); if (Math.abs(n) >= 10) return n.toFixed(1); return n.toFixed(2); } //# sourceMappingURL=BarChart.js.map