mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
feat(timeline): draw person-defined symptoms as value lines
A custom symptom could feed pattern discovery and the Coach but could not be looked at over time. The timeline now offers every symptom the record has logged, hidden ones included, beside the measurements and lab markers, and draws a chosen one as a value line. `values` takes `symptom:<definitionId>`, checked for its shape and capped like a lab marker; an id the record does not hold draws nothing. A day's value is its highest intensity on the record's local day; a point is the mean of the bucket's day values over the days logged, with the number of those days, and a day without an entry is not a zero. The line sits on a fixed 0 to 10 scale. Symptoms are read under the illness lane's gate, the module and sharing domain the symptom routes use. An `all` window reaches back to a chosen symptom's first entry. The response carries `symptomSeries` and `availableSymptoms`; the OpenAPI contract is regenerated. The page words a point's count as days and the value out of ten. The date-key schema moves to its own module so the symptom schemas can share it.
This commit is contained in:
1 parent
be7a65bdb9
commit
74e691cecd
39 files changed
+1871
-132
No files matched your search
@@ -514,6 +514,8 @@ export function fullTimeline(today: string) {
|
||||
availableSeries: FULL_AVAILABLE,
|
||||
labSeries: [],
|
||||
availableLabs: [],
|
||||
symptomSeries: [],
|
||||
availableSymptoms: [],
|
||||
notable: [{ date: "2026-01-04", kind: "extremeHigh" }],
|
||||
};
|
||||
}
|
||||
@@ -610,6 +612,50 @@ function labsLane(keys: readonly string[]) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Two of the person's own symptoms (#981): a headache logged over several
|
||||
* quarters, one quarter resting on two days, and a nausea hidden from entry
|
||||
* since, logged once. Their points are bucket means of day peaks on the
|
||||
* fixed 0 to 10 scale; only quarters are drawn here, the bucket an `all`
|
||||
* view of the full record uses.
|
||||
*/
|
||||
export const SYMPTOM_OPTIONS = [
|
||||
{
|
||||
key: "symptom:sd-headache",
|
||||
definitionId: "sd-headache",
|
||||
label: "Kopfschmerz",
|
||||
icon: "brain",
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
key: "symptom:sd-nausea",
|
||||
definitionId: "sd-nausea",
|
||||
label: "Übelkeit",
|
||||
icon: null,
|
||||
isActive: false,
|
||||
},
|
||||
];
|
||||
|
||||
const SYMPTOM_QUARTERS: Record<
|
||||
string,
|
||||
Array<{ t: string; mean: number; count: number }>
|
||||
> = {
|
||||
"symptom:sd-headache": [
|
||||
{ t: "2024-01-01", mean: 6.5, count: 9 },
|
||||
{ t: "2024-04-01", mean: 4, count: 2 },
|
||||
{ t: "2025-01-01", mean: 3.25, count: 5 },
|
||||
],
|
||||
"symptom:sd-nausea": [{ t: "2023-07-01", mean: 2, count: 1 }],
|
||||
};
|
||||
|
||||
function symptomLines(keys: readonly string[], bucket: Bucket) {
|
||||
return SYMPTOM_OPTIONS.filter((o) => keys.includes(o.key)).map((o) => ({
|
||||
...o,
|
||||
scale: { min: 0, max: 10 },
|
||||
points: bucket === "quarter" ? SYMPTOM_QUARTERS[o.key] : [],
|
||||
}));
|
||||
}
|
||||
|
||||
export function readyTimeline(today: string) {
|
||||
return {
|
||||
zoom: "all",
|
||||
@@ -733,6 +779,8 @@ export function readyTimeline(today: string) {
|
||||
availableSeries: ["BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA", "WEIGHT"],
|
||||
labSeries: [],
|
||||
availableLabs: [],
|
||||
symptomSeries: [],
|
||||
availableSymptoms: [],
|
||||
notable: [],
|
||||
};
|
||||
}
|
||||
@@ -935,6 +983,8 @@ export async function mockTimeline(
|
||||
dataFrom?: string;
|
||||
/** The record has lab markers to offer and draw (#1299). */
|
||||
labs?: boolean;
|
||||
/** The record has logged symptoms to offer and draw (#981). */
|
||||
symptoms?: boolean;
|
||||
} = {},
|
||||
): Promise<TimelineMockLog> {
|
||||
const log: TimelineMockLog = { timelineQueries: [], lifeEventPosts: [] };
|
||||
@@ -950,7 +1000,11 @@ export async function mockTimeline(
|
||||
.filter(Boolean);
|
||||
// A lab marker is a line of its own (`labSeries`), never a mean.
|
||||
const labKeys = named.filter((key) => key.startsWith("lab:"));
|
||||
const values = named.filter((key) => !key.startsWith("lab:"));
|
||||
// A symptom is a line of its own too (`symptomSeries`), on 0 to 10.
|
||||
const symptomKeys = named.filter((key) => key.startsWith("symptom:"));
|
||||
const values = named.filter(
|
||||
(key) => !key.startsWith("lab:") && !key.startsWith("symptom:"),
|
||||
);
|
||||
const labs =
|
||||
state === "full" && options.labs
|
||||
? {
|
||||
@@ -966,6 +1020,13 @@ export async function mockTimeline(
|
||||
const from = url.searchParams.get("from");
|
||||
const to = url.searchParams.get("to");
|
||||
const bucket = bucketFor(zoom, from, to);
|
||||
const symptoms =
|
||||
state === "full" && options.symptoms
|
||||
? {
|
||||
symptomSeries: symptomLines(symptomKeys, bucket),
|
||||
availableSymptoms: SYMPTOM_OPTIONS,
|
||||
}
|
||||
: {};
|
||||
const dataFrom =
|
||||
options.dataFrom ?? (state === "full" ? "2019-01-01" : "2024-05-01");
|
||||
if (options.onlyOld && (zoom === "quarter" || zoom === "year")) {
|
||||
@@ -988,6 +1049,7 @@ export async function mockTimeline(
|
||||
return json(route, 200, {
|
||||
...body,
|
||||
...labs,
|
||||
...symptoms,
|
||||
zoom,
|
||||
bucket,
|
||||
...(chosen
|
||||
|
||||
Reference in new issue
Block a user