UNPKG

@teaui/core

Version:

A high-level terminal UI library for Node

171 lines 6.4 kB
import { View } from '../View.js'; import { Point, Rect, Size } from '../geometry.js'; import { Style } from '../Style.js'; export class Plot extends View { #charts = []; #title; #xAxisLabels = true; #yAxisLabels = true; #showAxes = true; constructor(props = {}) { super(props); this.#update(props); } update(props) { this.#update(props); super.update(props); } #update({ title, xAxisLabels, yAxisLabels, showAxes }) { this.#title = title; this.#xAxisLabels = xAxisLabels ?? true; this.#yAxisLabels = yAxisLabels ?? true; this.#showAxes = showAxes ?? true; } add(chart) { this.#charts.push(chart); this.invalidateRender(); } remove(chart) { const idx = this.#charts.indexOf(chart); if (idx !== -1) { this.#charts.splice(idx, 1); this.invalidateRender(); } } naturalSize(available) { return available; } render(viewport) { if (viewport.isEmpty || this.#charts.length === 0) return; const textStyle = this.purpose.text(); const totalW = viewport.contentSize.width; const totalH = viewport.contentSize.height; // Compute combined data ranges from all charts const xRange = this.#combinedXRange(); const yRange = this.#combinedYRange(); // Layout regions let titleHeight = 0; if (this.#title) { titleHeight = 1; } // Y-axis labels on the left let yLabelWidth = 0; let yLabels = []; const chartHeight = totalH - titleHeight - (this.#xAxisLabels ? 1 : 0) - (this.#showAxes ? 1 : 0); if (this.#yAxisLabels && chartHeight > 0) { // Get labels from first chart const labelCount = Math.max(2, Math.min(chartHeight, 8)); yLabels = this.#charts[0].getYLabels(labelCount); yLabelWidth = Math.max(0, ...yLabels.map(l => l.length)) + 1; // +1 for space } // Axis line takes 1 column if shown const axisLineWidth = this.#showAxes ? 1 : 0; const chartX = yLabelWidth + axisLineWidth; const chartY = titleHeight; const chartW = Math.max(0, totalW - chartX); const chartH = Math.max(0, chartHeight); if (chartW <= 0 || chartH <= 0) return; // Draw title if (this.#title && titleHeight > 0) { const titleX = chartX + Math.max(0, Math.floor((chartW - this.#title.length) / 2)); viewport.write(this.#title.slice(0, totalW), new Point(titleX, 0), new Style({ bold: true }).merge(textStyle)); } // Draw y-axis labels if (this.#yAxisLabels && yLabels.length > 0 && chartH > 0) { for (let i = 0; i < yLabels.length; i++) { const labelY = chartY + Math.round((i * (chartH - 1)) / Math.max(1, yLabels.length - 1)); if (labelY < chartY + chartH) { const label = yLabels[i].padStart(yLabelWidth - 1); viewport.write(label, new Point(0, labelY), textStyle); } } } // Draw axes lines if (this.#showAxes) { const axisX = yLabelWidth; const pt = new Point(0, 0).mutableCopy(); // Vertical axis line for (let y = chartY; y < chartY + chartH; y++) { pt.x = axisX; pt.y = y; viewport.write('│', pt, textStyle); } // Bottom-left corner pt.x = axisX; pt.y = chartY + chartH; viewport.write('└', pt, textStyle); // Horizontal axis line for (let x = axisX + 1; x < totalW; x++) { pt.x = x; pt.y = chartY + chartH; viewport.write('─', pt, textStyle); } } // Draw x-axis labels if (this.#xAxisLabels && this.#charts.length > 0) { const xLabels = this.#charts[0].getXLabels(); if (xLabels.length > 0) { const xAxisY = chartY + chartH + (this.#showAxes ? 1 : 0); if (xAxisY < totalH) { // Distribute labels evenly const labelCount = Math.min(xLabels.length, Math.floor(chartW / 4)); for (let i = 0; i < labelCount; i++) { const dataIdx = Math.round((i * (xLabels.length - 1)) / Math.max(1, labelCount - 1)); const label = xLabels[dataIdx]; const labelX = chartX + Math.round((i * (chartW - 1)) / Math.max(1, labelCount - 1)); const truncated = label.slice(0, Math.min(label.length, chartW - (labelX - chartX))); viewport.write(truncated, new Point(labelX, xAxisY), textStyle); } } } } // Render charts in the chart area const chartRect = new Rect(new Point(chartX, chartY), new Size(chartW, chartH)); for (const chart of this.#charts) { viewport.clipped(chartRect, clippedViewport => { chart.renderChart(clippedViewport, { width: chartW, height: chartH, xRange, yRange, }); }); } } #combinedXRange() { let min = Infinity; let max = -Infinity; for (const chart of this.#charts) { const range = chart.getXRange(); if (range.min < min) min = range.min; if (range.max > max) max = range.max; } if (!isFinite(min)) return { min: 0, max: 1 }; return { min, max }; } #combinedYRange() { let min = Infinity; let max = -Infinity; for (const chart of this.#charts) { const range = chart.getYRange(); if (range.min < min) min = range.min; if (range.max > max) max = range.max; } if (!isFinite(min)) return { min: 0, max: 1 }; return { min, max }; } } //# sourceMappingURL=Plot.js.map