mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(charts): "7-Tage-Trend" label + numeric trend indicator on every chart incl. mood
Marc reported v1.4.15 A4 didn't fully land: - DE label still read "7-Tage-Schnitt" / "7T-Schnitt" in places - mood tile showed no numeric trend delta even though other tiles did i18n rename: - de.json + en.json: trend7dShort, movingAverage7d, moodMA all read "7-Tage-Trend" / "7-day trend". The legacy "7T:" / "7d:" header on the avg7 line is preserved for tiles that haven't been migrated to the trend7Delta API. trend-delta fallback: - summaryToTrend7Delta() now falls back to slope30 (projected onto 7 days) when slope7 is null. Mood entries are typically logged less than daily, so slope7 needs ≥ 2 points within the trailing 7-day window — for sparser users that came back null and the tile silently dropped the delta indicator. slope30 still answers "is this metric drifting?" so we project that onto the 7-day window for label parity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
577d8dd29f
commit
4df6dac036
5 files changed
+68
-20
No files matched your search
+5
-4
@@ -587,13 +587,14 @@
|
||||
"bodyWater": "Körperwasser",
|
||||
"boneMass": "Knochenmasse",
|
||||
"spo2": "SpO₂",
|
||||
"movingAverage7d": "7T-Schnitt",
|
||||
"movingAverage7d": "7-Tage-Trend",
|
||||
"avg7dShort": "7T",
|
||||
"avg30dShort": "30T",
|
||||
"trend7dShort": "7T-Trend",
|
||||
"trend7dShort": "7-Tage-Trend",
|
||||
"trend": "Trend",
|
||||
"targetRanges": "Zielbereiche",
|
||||
"perWeek": "/Woche",
|
||||
"totalDelta": "Gesamt",
|
||||
"selectMedication": "Medikament wählen",
|
||||
"noComplianceData": "Keine Compliance-Daten verfügbar",
|
||||
"history": "Verlauf",
|
||||
@@ -603,7 +604,7 @@
|
||||
"colorRed": "Rot:",
|
||||
"mood": "Stimmung",
|
||||
"moodScore": "Stimmungswert",
|
||||
"moodMA": "7-Tage-Schnitt",
|
||||
"moodMA": "7-Tage-Trend",
|
||||
"moodLabel1": "Lausig",
|
||||
"moodLabel2": "Schlecht",
|
||||
"moodLabel3": "Okay",
|
||||
@@ -1102,7 +1103,7 @@
|
||||
"rawDataWarning": "Rohdaten aktiv: Die KI erhält zusätzlich anonymisierte Messpunkte der letzten 30 Tage für eine präzisere Auswertung.",
|
||||
"regenerateInsights": "Reports neu generieren",
|
||||
"regenerateSuccess": "Reports wurden neu generiert",
|
||||
"regenerateRateLimit": "Bitte warte — maximal 2 Analysen pro Stunde.",
|
||||
"regenerateRateLimit": "Bitte warte — du hast das stündliche Limit für Analysen erreicht.",
|
||||
"lastGeneratedAt": "Zuletzt generiert",
|
||||
"ai": {
|
||||
"title": "KI-Insights",
|
||||
|
||||
+5
-4
@@ -587,13 +587,14 @@
|
||||
"bodyWater": "Body water",
|
||||
"boneMass": "Bone mass",
|
||||
"spo2": "SpO₂",
|
||||
"movingAverage7d": "7d avg",
|
||||
"movingAverage7d": "7-day trend",
|
||||
"avg7dShort": "7d",
|
||||
"avg30dShort": "30d",
|
||||
"trend7dShort": "7d trend",
|
||||
"trend7dShort": "7-day trend",
|
||||
"trend": "Trend",
|
||||
"targetRanges": "Target ranges",
|
||||
"perWeek": "/week",
|
||||
"totalDelta": "Total",
|
||||
"selectMedication": "Select medication",
|
||||
"noComplianceData": "No compliance data available",
|
||||
"history": "History",
|
||||
@@ -603,7 +604,7 @@
|
||||
"colorRed": "Red:",
|
||||
"mood": "Mood",
|
||||
"moodScore": "Mood Score",
|
||||
"moodMA": "7-Day Average",
|
||||
"moodMA": "7-day trend",
|
||||
"moodLabel1": "Awful",
|
||||
"moodLabel2": "Bad",
|
||||
"moodLabel3": "Okay",
|
||||
@@ -1102,7 +1103,7 @@
|
||||
"rawDataWarning": "Raw mode enabled: AI receives additional anonymized points from the last 30 days for higher analysis accuracy.",
|
||||
"regenerateInsights": "Regenerate reports",
|
||||
"regenerateSuccess": "Reports regenerated successfully",
|
||||
"regenerateRateLimit": "Please wait — max 2 analyses per hour.",
|
||||
"regenerateRateLimit": "Please wait — you've hit the hourly limit for analyses.",
|
||||
"lastGeneratedAt": "Last generated",
|
||||
"ai": {
|
||||
"title": "AI Insights",
|
||||
|
||||
@@ -50,7 +50,7 @@ describe("<TrendCard> 7-day trend label", () => {
|
||||
/>,
|
||||
);
|
||||
// New label flips on for the trend variant.
|
||||
expect(html).toContain("7d trend");
|
||||
expect(html).toContain("7-day trend");
|
||||
// Legacy avg7 short label must NOT appear in the trend variant —
|
||||
// this is the whole point of the rename.
|
||||
expect(html).not.toContain(">7d:<");
|
||||
@@ -65,13 +65,13 @@ describe("<TrendCard> 7-day trend label", () => {
|
||||
/>,
|
||||
"de",
|
||||
);
|
||||
expect(html).toContain("7T-Trend");
|
||||
expect(html).toContain("7-Tage-Trend");
|
||||
});
|
||||
|
||||
it("keeps the legacy avg7 label when no delta is supplied (back-compat)", () => {
|
||||
const html = render(<TrendCard {...baseProps} />);
|
||||
expect(html).toContain("7d");
|
||||
expect(html).not.toContain("7d trend");
|
||||
expect(html).not.toContain("7-day trend");
|
||||
});
|
||||
|
||||
it("renders a positive delta with `+` sign and one decimal", () => {
|
||||
|
||||
@@ -19,8 +19,9 @@ describe("summaryToTrend7Delta()", () => {
|
||||
expect(summaryToTrend7Delta(undefined)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when slope7 is not computable (insufficient data)", () => {
|
||||
it("returns null when neither slope7 nor slope30 is computable", () => {
|
||||
expect(summaryToTrend7Delta({ slope7: null })).toBeNull();
|
||||
expect(summaryToTrend7Delta({ slope7: null, slope30: null })).toBeNull();
|
||||
});
|
||||
|
||||
it("projects slope (units per day) over a 7-day window", () => {
|
||||
@@ -36,4 +37,29 @@ describe("summaryToTrend7Delta()", () => {
|
||||
it("returns 0 for a stable slope (the tile then paints ±0)", () => {
|
||||
expect(summaryToTrend7Delta({ slope7: STABLE })).toBe(0);
|
||||
});
|
||||
|
||||
it("falls back to slope30 when slope7 is null (sparse metrics like mood)", () => {
|
||||
// v1.4.16 fix A4: mood-tile showed no trend number for users who
|
||||
// log mood < daily because slope7 needs ≥2 points within the
|
||||
// trailing 7-day window. slope30 still answers "is this metric
|
||||
// drifting?" so we project that onto 7 days for label parity.
|
||||
expect(
|
||||
summaryToTrend7Delta({ slope7: null, slope30: RISING_HALF_PER_DAY }),
|
||||
).toBe(3.5);
|
||||
expect(
|
||||
summaryToTrend7Delta({
|
||||
slope7: null,
|
||||
slope30: FALLING_TENTH_PER_DAY,
|
||||
}),
|
||||
).toBeCloseTo(-0.7, 3);
|
||||
});
|
||||
|
||||
it("prefers slope7 over slope30 when both are present", () => {
|
||||
expect(
|
||||
summaryToTrend7Delta({
|
||||
slope7: RISING_HALF_PER_DAY,
|
||||
slope30: FALLING_TENTH_PER_DAY,
|
||||
}),
|
||||
).toBe(3.5);
|
||||
});
|
||||
});
|
||||
@@ -12,9 +12,17 @@
|
||||
* delta = slope.slope * 7 // units per day → unit delta over 7d
|
||||
*
|
||||
* The trend-card then paints the delta with metric-aware sentiment
|
||||
* colour (up-good vs up-bad). Returning `null` when slope7 is not
|
||||
* available keeps the tile rendering the legacy avg label for back-
|
||||
* compat with call sites that haven't been migrated.
|
||||
* colour (up-good vs up-bad).
|
||||
*
|
||||
* v1.4.16 Fix A4: fall back to slope30 (projected onto 7 days) when
|
||||
* slope7 is unavailable. Some metrics (mood, sleep) have <2 entries
|
||||
* within the trailing 7-day window for users who don't log daily,
|
||||
* which used to surface as a missing delta on the dashboard tile —
|
||||
* Marc's complaint: "Bei Stimmung wird zum Beispiel kein Trend in
|
||||
* Zahlen dargestellt." A 30-day slope still answers the question
|
||||
* "is this metric drifting up or down?" — we just project it onto
|
||||
* a 7-day window for label consistency. Returning `null` is reserved
|
||||
* for the truly insufficient case where neither slope is available.
|
||||
*
|
||||
* Pure & deterministic — pinned by unit test.
|
||||
*/
|
||||
@@ -22,14 +30,26 @@ import type { TrendSlope } from "@/lib/analytics/trends";
|
||||
|
||||
export interface TrendCapableSummary {
|
||||
slope7: TrendSlope | null;
|
||||
slope30?: TrendSlope | null;
|
||||
}
|
||||
|
||||
export function summaryToTrend7Delta(
|
||||
summary: TrendCapableSummary | undefined | null,
|
||||
): number | null {
|
||||
const slope7 = summary?.slope7;
|
||||
if (!slope7) return null;
|
||||
// slope.slope is units per day (see lib/analytics/trends.ts) → 7 days
|
||||
// is the projected end-to-end delta of the regression line.
|
||||
return slope7.slope * 7;
|
||||
if (!summary) return null;
|
||||
// Primary: trailing-7-day linear-regression slope projected to a
|
||||
// 7-day window. slope.slope is units per day (see
|
||||
// lib/analytics/trends.ts).
|
||||
if (summary.slope7) {
|
||||
return summary.slope7.slope * 7;
|
||||
}
|
||||
// Fallback: slope30 projected onto 7 days. Same units (per day) so
|
||||
// we still multiply by 7. This keeps the delta visible for sparser
|
||||
// metrics (mood, sleep) that don't accumulate enough points in
|
||||
// every trailing 7-day window. The arrow + colour interpretation
|
||||
// stays correct because slope30 is computed the same way.
|
||||
if (summary.slope30) {
|
||||
return summary.slope30.slope * 7;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in new issue
Block a user