Files
HealthLog/src/lib/charts/window-stats.ts
T
Marc-André Bombeck 1f4ba5939d style: apply prettier across the tree
One-shot prettier pass so the new format:check gate starts from a clean
baseline. Formatting only — no behaviour change. Generator-owned and
vendored artifacts stay untouched via .prettierignore.
2026-06-19 22:20:31 +02:00

76 lines
2.6 KiB
TypeScript

/**
* v1.12.7 — chart-reactive metric statistics.
*
* When the user brushes a window in a metric sub-page chart, the
* Min / Max / Median / Mittelwert strip recomputes over just the visible
* domain rather than the full range. This helper is the single pure
* computation behind that: hand it the visible per-type values and it
* returns the four central statistics.
*
* The four-statistic shape matches the subset of `DataSummary` the strip
* renders (`min` / `max` / `median` / `mean`), and the median uses the same
* linear-interpolated midpoint definition as `summarize()` in
* `src/lib/analytics/trends.ts` so a brushed window and the full-range
* summary read on the same algorithm — only the window differs.
*
* The helper is deliberately free of any Recharts / React dependency so the
* chart can compute windowed stats off the array it already holds without
* re-fetching, and the unit suite can pin the maths without mounting a chart.
*/
export interface MetricWindowStats {
/** Number of finite values in the window. */
count: number;
min: number | null;
max: number | null;
median: number | null;
mean: number | null;
}
const EMPTY: MetricWindowStats = {
count: 0,
min: null,
max: null,
median: null,
mean: null,
};
/**
* Compute Min / Max / Median / Mean over a list of values. Non-finite
* entries (NaN, Infinity, null, undefined) are dropped before the fold so a
* sparse series with gaps still summarises cleanly. An empty (or all-gap)
* input returns the all-null shape so the caller can fall back to the
* full-range summary rather than paint zeros.
*/
export function computeWindowStats(
values: Array<number | null | undefined>,
): MetricWindowStats {
const finite: number[] = [];
for (const v of values) {
if (typeof v === "number" && Number.isFinite(v)) finite.push(v);
}
if (finite.length === 0) return { ...EMPTY };
// Single-pass sum/min/max — mirrors the bounded fold in `summarize()`
// so a multi-year power user's brushed window never spreads a huge
// argument list onto the stack.
let sum = 0;
let min = finite[0];
let max = finite[0];
for (const v of finite) {
sum += v;
if (v < min) min = v;
if (v > max) max = v;
}
const mean = sum / finite.length;
// Linear-interpolated midpoint on an even-length series — the standard
// PERCENTILE_CONT(0.5) definition, matching `summarize()`.
const sorted = [...finite].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
const median =
sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
return { count: finite.length, min, max, median, mean };
}