@teaui/core
Version:
A high-level terminal UI library for Node
122 lines • 4.57 kB
JavaScript
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