|
| 1 | +import * as asciichart from "asciichart"; |
| 2 | + |
| 3 | +import { AsciiChartOptions, PDFEstimation } from "./types"; |
| 4 | + |
1 | 5 | /** |
2 | 6 | * Smooth an array of numbers using a simple moving average filter. |
3 | 7 | * Default window size is 3. |
@@ -54,3 +58,104 @@ export function widenData(data: number[], factor: number = 1): number[] { |
54 | 58 | widened.push(data[data.length - 1]); |
55 | 59 | return widened; |
56 | 60 | } |
| 61 | + |
| 62 | +/** |
| 63 | + * Estimate the probability density function (PDF) from raw data using a histogram. |
| 64 | + * |
| 65 | + * If the data are discrete (all integer values) then a binWidth of 1 is used. |
| 66 | + * |
| 67 | + * @param data - Raw data points. |
| 68 | + * @param numBins - Number of bins for the histogram (optional). |
| 69 | + * @returns An object containing binCenters, estimated pdf values, and the overall x-range. |
| 70 | + */ |
| 71 | +export function estimatePDF(data: number[], numBins?: number): PDFEstimation { |
| 72 | + const isDiscrete: boolean = data.every((v) => Number.isInteger(v)); |
| 73 | + let dataMin = Math.min(...data); |
| 74 | + let dataMax = Math.max(...data); |
| 75 | + |
| 76 | + let binWidth: number; |
| 77 | + let bins: number[]; |
| 78 | + |
| 79 | + if (isDiscrete) { |
| 80 | + dataMin = Math.floor(dataMin); |
| 81 | + dataMax = Math.ceil(dataMax); |
| 82 | + numBins = dataMax - dataMin + 1; |
| 83 | + binWidth = 1; |
| 84 | + bins = new Array(numBins).fill(0); |
| 85 | + } else { |
| 86 | + numBins = numBins || 20; |
| 87 | + binWidth = (dataMax - dataMin) / numBins; |
| 88 | + bins = new Array(numBins).fill(0); |
| 89 | + } |
| 90 | + |
| 91 | + // Count data points in each bin. |
| 92 | + data.forEach((value) => { |
| 93 | + let binIndex = Math.floor((value - dataMin) / binWidth); |
| 94 | + if (binIndex < 0) binIndex = 0; |
| 95 | + else if (binIndex >= bins.length) binIndex = bins.length - 1; |
| 96 | + bins[binIndex]++; |
| 97 | + }); |
| 98 | + |
| 99 | + const totalCount = data.length; |
| 100 | + // Convert counts into density: density = count / (totalCount * binWidth) |
| 101 | + let pdf = bins.map((count) => count / (totalCount * binWidth)); |
| 102 | + |
| 103 | + // Smooth the PDF for better visual appearance. |
| 104 | + pdf = smooth(pdf, 3); |
| 105 | + |
| 106 | + // Compute bin centers (to be used as x-axis tick marks). |
| 107 | + const binCenters: number[] = Array.from( |
| 108 | + { length: numBins }, |
| 109 | + (_, i) => dataMin + (i + 0.5) * binWidth |
| 110 | + ); |
| 111 | + |
| 112 | + return { binCenters, pdf, xMin: dataMin, xMax: dataMax }; |
| 113 | +} |
| 114 | + |
| 115 | +/** |
| 116 | + * Plot an array of y-values using asciichart with custom x-axis labels. |
| 117 | + * |
| 118 | + * An optional xLabelOffset (number of spaces) can be provided via options to shift the left x-axis label. |
| 119 | + * |
| 120 | + * @param dataValues - The y-values (e.g., estimated PDF values). |
| 121 | + * @param xMin - The minimum x value for labeling. |
| 122 | + * @param xMax - The maximum x value for labeling. |
| 123 | + * @param options - Additional options for asciichart. |
| 124 | + */ |
| 125 | +export function plotWithXAxis( |
| 126 | + dataValues: number[], |
| 127 | + xMin: number, |
| 128 | + xMax: number, |
| 129 | + options: AsciiChartOptions = {} |
| 130 | +): void { |
| 131 | + // Set fixed y-axis limits for a tighter plot. |
| 132 | + const yMax: number = Math.max(...dataValues) * 1.05; |
| 133 | + const plotOptions = Object.assign( |
| 134 | + { |
| 135 | + height: 15, |
| 136 | + min: 0, |
| 137 | + max: yMax, |
| 138 | + }, |
| 139 | + options |
| 140 | + ); |
| 141 | + |
| 142 | + // Create and display the plot. |
| 143 | + const chart = asciichart.plot(dataValues, plotOptions); |
| 144 | + console.log(chart); |
| 145 | + |
| 146 | + // Determine the width of the chart. |
| 147 | + const chartLines = chart.split("\n"); |
| 148 | + const chartWidth = Math.max(...chartLines.map((line) => line.length)); |
| 149 | + |
| 150 | + // Use the xLabelOffset option (defaulting to 0) to add extra spaces before the left label. |
| 151 | + const xLabelOffset: number = options.xLabelOffset || 0; |
| 152 | + |
| 153 | + // Build an x-axis label line. |
| 154 | + const leftLabel = " ".repeat(xLabelOffset) + xMin.toFixed(1); |
| 155 | + const rightLabel = xMax.toFixed(1); |
| 156 | + let labelLine = " ".repeat(chartWidth); |
| 157 | + labelLine = leftLabel + labelLine.slice(leftLabel.length); |
| 158 | + const rightStart = chartWidth - rightLabel.length; |
| 159 | + labelLine = labelLine.slice(0, rightStart) + rightLabel; |
| 160 | + console.log(labelLine); |
| 161 | +} |
0 commit comments