mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
100 lines
2.8 KiB
TypeScript
100 lines
2.8 KiB
TypeScript
import type { Derived } from "@/lib/insights/derived/types";
|
|
import { userDayKey } from "@/lib/tz/format";
|
|
|
|
import type { ActivityPillarInput, PillarValue } from "./types";
|
|
import {
|
|
clampScore,
|
|
coverage,
|
|
failedPillar,
|
|
insufficientPillar,
|
|
mean,
|
|
okPillar,
|
|
standardDeviation,
|
|
} from "./shared";
|
|
|
|
export const ACTIVITY_WINDOW_DAYS = 28;
|
|
export const ACTIVITY_MIN_DAYS = 21;
|
|
export const OLDER_ADULT_AGE = 60;
|
|
export const YOUNGER_STEP_PLATEAU = 10_000;
|
|
export const OLDER_STEP_PLATEAU = 8_000;
|
|
|
|
export function computeActivityPillar(
|
|
input: ActivityPillarInput,
|
|
): Derived<PillarValue> {
|
|
if (input.readFailed) {
|
|
return failedPillar({
|
|
id: "ACTIVITY",
|
|
asOf: input.asOf,
|
|
windowDays: ACTIVITY_WINDOW_DAYS,
|
|
});
|
|
}
|
|
|
|
const asOfDay = userDayKey(input.asOf, input.timezone);
|
|
const sinceDay = userDayKey(
|
|
new Date(input.asOf.getTime() - (ACTIVITY_WINDOW_DAYS - 1) * 86_400_000),
|
|
input.timezone,
|
|
);
|
|
const points = input.days.filter(
|
|
(point) => point.day <= asOfDay && point.day >= sinceDay,
|
|
);
|
|
if (points.length < ACTIVITY_MIN_DAYS) {
|
|
return insufficientPillar({
|
|
id: "ACTIVITY",
|
|
source: input.source,
|
|
asOf: input.asOf,
|
|
windowDays: ACTIVITY_WINDOW_DAYS,
|
|
requiredInputs: ACTIVITY_MIN_DAYS,
|
|
presentInputs: points.length,
|
|
historyDays: points.length,
|
|
missing: [`${Math.max(0, ACTIVITY_MIN_DAYS - points.length)} days`],
|
|
reason:
|
|
input.days.length > 0 ? "below_day_floor_or_stale" : "not_tracked",
|
|
});
|
|
}
|
|
|
|
const values = points.map((point) => point.value);
|
|
const observed = mean(values);
|
|
const plateau =
|
|
input.ageYears != null && input.ageYears >= OLDER_ADULT_AGE
|
|
? OLDER_STEP_PLATEAU
|
|
: YOUNGER_STEP_PLATEAU;
|
|
const score = clampScore((observed / plateau) * 100);
|
|
const noiseFloor = Math.max(
|
|
1,
|
|
Math.round(((0.5 * standardDeviation(values)) / plateau) * 100),
|
|
);
|
|
const latestDay = [...points].sort((a, b) => b.day.localeCompare(a.day))[0];
|
|
|
|
return okPillar({
|
|
id: "ACTIVITY",
|
|
source: input.source,
|
|
asOf: input.asOf,
|
|
windowDays: ACTIVITY_WINDOW_DAYS,
|
|
coverage: coverage({
|
|
requiredInputs: ACTIVITY_MIN_DAYS,
|
|
presentInputs: ACTIVITY_MIN_DAYS,
|
|
historyDays: points.length,
|
|
}),
|
|
value: {
|
|
score,
|
|
observed: {
|
|
value: Math.round(observed),
|
|
unit: "steps/day",
|
|
label: `${Math.round(observed).toLocaleString("en-US")} steps/day`,
|
|
asOf: `${latestDay.day}T00:00:00.000Z`,
|
|
sources: input.sources,
|
|
},
|
|
reference: {
|
|
kind: "guideline-band",
|
|
low: 0,
|
|
high: plateau,
|
|
label: `benefit plateaus by ${plateau.toLocaleString("en-US")} steps/day`,
|
|
source: "Paluch 2022",
|
|
},
|
|
noiseFloor,
|
|
deltaEligible: true,
|
|
deltaIdentity: "ACTIVITY_STEPS",
|
|
},
|
|
});
|
|
}
|