components/meter/meter.ts
76 lines and 11 functions in DrDustinEdwards/capsomer, at commit d3966cc on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| ratio | 9 to 13 | 4 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| toneFor | 18 to 22 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| num | 24 to 26 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| setBar | 39 to 46 | 4 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| update | 50 to 61 | 4 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| enhance | 67 to 85 | 5 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:76> | 76 to 76 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:78> | 78 to 78 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:80> | 80 to 82 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:84> | 84 to 84 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:84> | 84 to 84 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
Imports
It imports no other module of this repo. Imported by components/media/media-tile.ts, components/media/media.ts, components/meter/meter.react.tsx, components/usage-meter/usage-meter.ts.
Source
// The meter's behaviour: where the fill, the projection and the threshold tick sit on the
// bar, set through the CSSOM so apps keep style-src 'self'. The number itself is in the
// markup's text and aria-valuetext; this only draws the bar. The usage meter, the stat tile
// and any app's bar use these same functions.
export type MeterTone = "ok" | "warn" | "crit";
// The share of the span, from 0 to 1. A missing, negative or non-finite value is 0.
export function ratio(value: number, max: number, min = 0): number {
const span = max - min;
if (!Number.isFinite(value) || !Number.isFinite(span) || span <= 0) return 0;
return Math.min(1, Math.max(0, (value - min) / span));
}
// The tone a value earns by threshold: warning from `warnAt` of the span, critical from
// `critAt` (both 0 to 1; the defaults are the usage meter's 75% and 90%). The caller still
// writes the status word beside it: a tone is a word, a shape and a colour, never colour.
export function toneFor(value: number, max: number, options: { min?: number; warnAt?: number; critAt?: number } = {}): MeterTone {
const { min = 0, warnAt = 0.75, critAt = 0.9 } = options;
const r = ratio(value, max, min);
return r >= critAt ? "crit" : r >= warnAt ? "warn" : "ok";
}
function num(s: string | null | undefined): number {
return s == null || s === "" ? NaN : Number(s);
}
export interface BarValues {
value: number;
max: number;
min?: number;
// The projected value at the end of the period, in the same unit; null or absent: none.
projected?: number | null;
// Where the threshold tick sits, in the same unit; null or absent: no tick.
threshold?: number | null;
}
// Sets one bar's positions from its values.
export function setBar(bar: HTMLElement, v: BarValues): void {
const min = v.min ?? 0;
bar.style.setProperty("--cap-meter-ratio", String(ratio(v.value, v.max, min)));
if (v.projected == null) bar.style.removeProperty("--cap-meter-projected");
else bar.style.setProperty("--cap-meter-projected", String(Math.max(ratio(v.value, v.max, min), ratio(v.projected, v.max, min))));
if (v.threshold == null) bar.style.removeProperty("--cap-meter-threshold");
else bar.style.setProperty("--cap-meter-threshold", String(ratio(v.threshold, v.max, min)));
}
// Reads a bar's aria-valuenow, aria-valuemin, aria-valuemax, data-projected and
// data-threshold and draws it.
export function update(bar: HTMLElement): void {
const min = num(bar.getAttribute("aria-valuemin"));
const projected = num(bar.dataset.projected);
const threshold = num(bar.dataset.threshold);
setBar(bar, {
value: num(bar.getAttribute("aria-valuenow")),
max: num(bar.getAttribute("aria-valuemax")),
min: Number.isFinite(min) ? min : 0,
projected: Number.isFinite(projected) ? projected : null,
threshold: Number.isFinite(threshold) ? threshold : null,
});
}
const WATCHED = ["aria-valuenow", "aria-valuemax", "aria-valuemin", "data-projected", "data-threshold"];
// Attaches to every [data-cap="meter"] under root (or root itself) not yet attached, draws
// its bar, and redraws it whenever a value changes. Returns a function that detaches.
export function enhance(root: ParentNode = document): () => void {
const undo: Array<() => void> = [];
const found = [...root.querySelectorAll<HTMLElement>("[data-cap='meter']:not([data-cap-ready])")];
if (root instanceof HTMLElement && root.matches("[data-cap='meter']:not([data-cap-ready])")) found.unshift(root);
for (const meter of found) {
meter.dataset.capReady = "";
const bar = meter.querySelector<HTMLElement>(".cap-meter-bar");
if (bar) {
update(bar);
const watch = new MutationObserver(() => update(bar));
watch.observe(bar, { attributes: true, attributeFilter: WATCHED });
undo.push(() => watch.disconnect());
}
undo.push(() => {
delete meter.dataset.capReady;
});
}
return () => undo.forEach((f) => f());
}