mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
refactor(insights): tighten the mood page — consolidate subtexts, icon-ify explainers, unify palette
Lay the narrative takeaways two-up instead of a single full-width column so short one-liners (best daypart, logging streak, weekday peak) stop stacking into a long ladder of half-empty rows. Fold the low-density footnotes behind a focusable ring/info explainer icon with a tooltip: the per-pair paired-day/r/q stat under each discovered relation, the per-card paired-day/r line under each mood-correlation scatter, and the false-discovery footer + observational disclaimer that sat as two full-width rows under the discovered-relations list. The trigger is a real button carrying an aria-label so the explanation is keyboard- and screen-reader-reachable, not hover-only. Charts stay Recharts and visually identical — the mood distribution and mood-by-time-of-day charts already share one Dracula band palette (red/orange/yellow/green over the same thresholds, --dracula-fg ticks, --dracula-comment axes, --card/--border tooltips), so the surface palette is already consistent and left untouched. New explainer keys land in all six locales. (cherry picked from commit afc38491ebfae3beed6476a79439d2b87f33f823)
This commit is contained in:
1 parent
9cc739f79d
commit
89d51c5380
11 files changed
+153
-49
No files matched your search
+5
-2
@@ -2558,7 +2558,8 @@
|
||||
"weightTitle": "Stimmung vs. Gewicht",
|
||||
"bloodPressureTitle": "Stimmung vs. Blutdruck",
|
||||
"weightAxis": "Gewicht",
|
||||
"bloodPressureAxis": "Systolisch (mmHg)"
|
||||
"bloodPressureAxis": "Systolisch (mmHg)",
|
||||
"sourceLabel": "Wie das berechnet wurde"
|
||||
},
|
||||
"structuredTagsTitle": "Tag-Auswertung",
|
||||
"inTargetValue": "{pct} %",
|
||||
@@ -2629,7 +2630,9 @@
|
||||
"moodDown": "Höheres {factor} geht in deinen Daten tendenziell mit schlechterer Stimmung am Folgetag einher.",
|
||||
"outcomeUp": "Bessere Stimmung geht in deinen Daten tendenziell mit höherem {factor} am Folgetag einher.",
|
||||
"outcomeDown": "Bessere Stimmung geht in deinen Daten tendenziell mit niedrigerem {factor} am Folgetag einher.",
|
||||
"stat": "{n} gepaarte Tage · r = {r} · q = {q}"
|
||||
"stat": "{n} gepaarte Tage · r = {r} · q = {q}",
|
||||
"statLabel": "Wie das berechnet wurde",
|
||||
"explainerLabel": "Wie das berechnet wurde"
|
||||
}
|
||||
},
|
||||
"derived": {
|
||||
|
||||
+4
-1
@@ -2553,6 +2553,7 @@
|
||||
"strengthWeak": "Weak",
|
||||
"strengthNone": "None",
|
||||
"source": "{n} paired days · r = {r}",
|
||||
"sourceLabel": "How this was computed",
|
||||
"emptyTitle": "Not enough paired data yet",
|
||||
"emptyDescription": "Log mood and this metric on the same days to see a correlation.",
|
||||
"weightTitle": "Mood vs. weight",
|
||||
@@ -2629,7 +2630,9 @@
|
||||
"moodDown": "Higher {factor} tends to go with lower next-day mood in your data.",
|
||||
"outcomeUp": "Higher mood tends to go with higher next-day {factor} in your data.",
|
||||
"outcomeDown": "Higher mood tends to go with lower next-day {factor} in your data.",
|
||||
"stat": "{n} paired days · r = {r} · q = {q}"
|
||||
"stat": "{n} paired days · r = {r} · q = {q}",
|
||||
"statLabel": "How this was computed",
|
||||
"explainerLabel": "How this was computed"
|
||||
}
|
||||
},
|
||||
"derived": {
|
||||
|
||||
+5
-2
@@ -2558,7 +2558,8 @@
|
||||
"weightTitle": "Ánimo vs. peso",
|
||||
"bloodPressureTitle": "Ánimo vs. presión arterial",
|
||||
"weightAxis": "Peso",
|
||||
"bloodPressureAxis": "Sistólica (mmHg)"
|
||||
"bloodPressureAxis": "Sistólica (mmHg)",
|
||||
"sourceLabel": "Cómo se calculó esto"
|
||||
},
|
||||
"structuredTagsTitle": "Desglose de etiquetas",
|
||||
"inTargetValue": "{pct} %",
|
||||
@@ -2629,7 +2630,9 @@
|
||||
"moodDown": "Un mayor {factor} suele coincidir con peor ánimo al día siguiente en tus datos.",
|
||||
"outcomeUp": "Mejor ánimo suele coincidir con un {factor} más alto al día siguiente en tus datos.",
|
||||
"outcomeDown": "Mejor ánimo suele coincidir con un {factor} más bajo al día siguiente en tus datos.",
|
||||
"stat": "{n} días emparejados · r = {r} · q = {q}"
|
||||
"stat": "{n} días emparejados · r = {r} · q = {q}",
|
||||
"statLabel": "Cómo se calculó esto",
|
||||
"explainerLabel": "Cómo se calculó esto"
|
||||
}
|
||||
},
|
||||
"derived": {
|
||||
|
||||
+5
-2
@@ -2558,7 +2558,8 @@
|
||||
"weightTitle": "Humeur vs. poids",
|
||||
"bloodPressureTitle": "Humeur vs. tension artérielle",
|
||||
"weightAxis": "Poids",
|
||||
"bloodPressureAxis": "Systolique (mmHg)"
|
||||
"bloodPressureAxis": "Systolique (mmHg)",
|
||||
"sourceLabel": "Comment ceci est calculé"
|
||||
},
|
||||
"structuredTagsTitle": "Répartition des étiquettes",
|
||||
"inTargetValue": "{pct} %",
|
||||
@@ -2629,7 +2630,9 @@
|
||||
"moodDown": "Un {factor} plus élevé tend à coïncider avec une moins bonne humeur le lendemain dans vos données.",
|
||||
"outcomeUp": "Une meilleure humeur tend à coïncider avec un {factor} plus élevé le lendemain dans vos données.",
|
||||
"outcomeDown": "Une meilleure humeur tend à coïncider avec un {factor} plus bas le lendemain dans vos données.",
|
||||
"stat": "{n} jours appariés · r = {r} · q = {q}"
|
||||
"stat": "{n} jours appariés · r = {r} · q = {q}",
|
||||
"statLabel": "Comment ceci est calculé",
|
||||
"explainerLabel": "Comment ceci est calculé"
|
||||
}
|
||||
},
|
||||
"derived": {
|
||||
|
||||
+5
-2
@@ -2558,7 +2558,8 @@
|
||||
"weightTitle": "Umore vs. peso",
|
||||
"bloodPressureTitle": "Umore vs. pressione sanguigna",
|
||||
"weightAxis": "Peso",
|
||||
"bloodPressureAxis": "Sistolica (mmHg)"
|
||||
"bloodPressureAxis": "Sistolica (mmHg)",
|
||||
"sourceLabel": "Come è stato calcolato"
|
||||
},
|
||||
"structuredTagsTitle": "Analisi etichette",
|
||||
"inTargetValue": "{pct} %",
|
||||
@@ -2629,7 +2630,9 @@
|
||||
"moodDown": "Un {factor} più alto tende a coincidere con un umore peggiore il giorno dopo nei tuoi dati.",
|
||||
"outcomeUp": "Un umore migliore tende a coincidere con un {factor} più alto il giorno dopo nei tuoi dati.",
|
||||
"outcomeDown": "Un umore migliore tende a coincidere con un {factor} più basso il giorno dopo nei tuoi dati.",
|
||||
"stat": "{n} giorni appaiati · r = {r} · q = {q}"
|
||||
"stat": "{n} giorni appaiati · r = {r} · q = {q}",
|
||||
"statLabel": "Come è stato calcolato",
|
||||
"explainerLabel": "Come è stato calcolato"
|
||||
}
|
||||
},
|
||||
"derived": {
|
||||
|
||||
+5
-2
@@ -2558,7 +2558,8 @@
|
||||
"weightTitle": "Nastrój vs. waga",
|
||||
"bloodPressureTitle": "Nastrój vs. ciśnienie krwi",
|
||||
"weightAxis": "Waga",
|
||||
"bloodPressureAxis": "Skurczowe (mmHg)"
|
||||
"bloodPressureAxis": "Skurczowe (mmHg)",
|
||||
"sourceLabel": "Jak to obliczono"
|
||||
},
|
||||
"structuredTagsTitle": "Podział tagów",
|
||||
"inTargetValue": "{pct}%",
|
||||
@@ -2629,7 +2630,9 @@
|
||||
"moodDown": "Wyższy {factor} zwykle wiąże się z gorszym nastrojem następnego dnia w Twoich danych.",
|
||||
"outcomeUp": "Lepszy nastrój zwykle wiąże się z wyższym {factor} następnego dnia w Twoich danych.",
|
||||
"outcomeDown": "Lepszy nastrój zwykle wiąże się z niższym {factor} następnego dnia w Twoich danych.",
|
||||
"stat": "{n} sparowanych dni · r = {r} · q = {q}"
|
||||
"stat": "{n} sparowanych dni · r = {r} · q = {q}",
|
||||
"statLabel": "Jak to obliczono",
|
||||
"explainerLabel": "Jak to obliczono"
|
||||
}
|
||||
},
|
||||
"derived": {
|
||||
|
||||
@@ -96,9 +96,11 @@ describe("<MoodDiscoveredRelations>", () => {
|
||||
// factor label resolves to the localized measurement name
|
||||
expect(html).toContain("Time in Daylight");
|
||||
expect(html).toContain("higher next-day mood");
|
||||
// stat line carries n, r, q
|
||||
expect(html).toContain("r = 0.50");
|
||||
expect(html).toContain("q = 0.020");
|
||||
// v1.12.4 (C4) — the n/r/q detail moved off the visible sub-row into a
|
||||
// focusable explainer icon (its tooltip content is portalled on open, so
|
||||
// it is absent from the static markup). Assert the icon is wired per pair
|
||||
// instead of the inline stat string.
|
||||
expect(html).toContain('aria-label="How this was computed"');
|
||||
});
|
||||
|
||||
it("phrases a mood → next-day outcome pair", () => {
|
||||
@@ -111,7 +113,7 @@ describe("<MoodDiscoveredRelations>", () => {
|
||||
expect(html).toContain("lower next-day");
|
||||
});
|
||||
|
||||
it("filters to mood pairs and shows the honest full-family footer", () => {
|
||||
it("filters to mood pairs and folds the full-family footer into the header explainer", () => {
|
||||
const html = renderWith(
|
||||
[
|
||||
pair({ behaviour: "TIME_IN_DAYLIGHT", outcome: "MOOD" }),
|
||||
@@ -122,8 +124,16 @@ describe("<MoodDiscoveredRelations>", () => {
|
||||
expect((html.match(/data-slot="mood-discovered-pair"/g) ?? []).length).toBe(
|
||||
1,
|
||||
);
|
||||
// Footer's denominator is the full behaviour×outcome family the engine
|
||||
// tested, not the mood-only subset — so it must report 12, not 1 of 1.
|
||||
expect(html).toContain("from 12 day-to-day pairs");
|
||||
// v1.12.4 (C3) — the false-discovery footer (denominator = the full
|
||||
// behaviour×outcome family the engine tested, i.e. 12, not the mood-only
|
||||
// subset) + the observational disclaimer were two full-width footnote
|
||||
// rows; both fold into a single header explainer icon now. Its tooltip
|
||||
// content is portalled on open, so assert the trigger exists rather than
|
||||
// the (now hidden) footer string.
|
||||
expect(
|
||||
(html.match(/aria-label="How this was computed"/g) ?? []).length,
|
||||
).toBeGreaterThanOrEqual(1);
|
||||
// The two old inline footnote rows are gone from the static markup.
|
||||
expect(html).not.toContain("day-to-day pairs tested");
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import type { CorrelationResult } from "@/lib/analytics/correlations";
|
||||
import { MoodExplainerIcon } from "./mood-explainer-icon";
|
||||
|
||||
/**
|
||||
* v1.8.5 — mood × metric correlation cards (Apple "Life Factors" parity).
|
||||
@@ -96,9 +97,21 @@ function MoodCorrelationCard({
|
||||
{t(TITLE_KEY[kind])}
|
||||
</CardTitle>
|
||||
{hasResult && data.result && (
|
||||
<Badge variant="outline" className="shrink-0 text-[10px]">
|
||||
{t(STRENGTH_KEY[data.result.strength])}
|
||||
</Badge>
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
{t(STRENGTH_KEY[data.result.strength])}
|
||||
</Badge>
|
||||
{/* v1.12.4 (C4) — the "{n} paired days · r" line sat under the
|
||||
scatter as a half-empty row. Move it into an explainer icon
|
||||
in the header so the card footprint stays tight. */}
|
||||
<MoodExplainerIcon
|
||||
label={t("insights.mood.correlation.sourceLabel")}
|
||||
detail={t("insights.mood.correlation.source", {
|
||||
n: data.n,
|
||||
r: data.result.r.toFixed(2),
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -117,12 +130,6 @@ function MoodCorrelationCard({
|
||||
yAxis={{ dataKey: "y", name: t(Y_LABEL_KEY[kind]) }}
|
||||
height={180}
|
||||
/>
|
||||
<p className="text-muted-foreground text-[11px]">
|
||||
{t("insights.mood.correlation.source", {
|
||||
n: data.n,
|
||||
r: data.result.r.toFixed(2),
|
||||
})}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<EmptyState
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { MoodExplainerIcon } from "./mood-explainer-icon";
|
||||
|
||||
/**
|
||||
* v1.11.5 (F3) — FDR-controlled discovered mood relations.
|
||||
@@ -81,9 +82,21 @@ export function MoodDiscoveredRelations() {
|
||||
|
||||
return (
|
||||
<div className="space-y-2" data-slot="mood-discovered-relations">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("insights.mood.discovery.title")}
|
||||
</h3>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("insights.mood.discovery.title")}
|
||||
</h3>
|
||||
{/* v1.12.4 (C3) — the false-discovery footer + observational
|
||||
disclaimer were two full-width footnote rows. Fold both into a
|
||||
single explainer icon so the explanation is one hover/focus away
|
||||
instead of a low-density block under the list. */}
|
||||
<MoodExplainerIcon
|
||||
label={t("insights.mood.discovery.explainerLabel")}
|
||||
detail={`${t("insights.mood.discovery.footer", {
|
||||
tested: data.pairsTested,
|
||||
})} ${t("insights.mood.discovery.disclaimer")}`}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("insights.mood.discovery.description")}
|
||||
</p>
|
||||
@@ -108,28 +121,25 @@ export function MoodDiscoveredRelations() {
|
||||
data-mood-role={moodIsOutcome ? "outcome" : "behaviour"}
|
||||
data-direction={up ? "up" : "down"}
|
||||
>
|
||||
<span className="text-foreground">
|
||||
{t(sentenceKey, { factor: factorLabel })}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-[11px] tabular-nums">
|
||||
{t("insights.mood.discovery.stat", {
|
||||
n: pair.n,
|
||||
r: pair.r.toFixed(2),
|
||||
q: pair.qValue.toFixed(3),
|
||||
})}
|
||||
{/* v1.12.4 (C4) — the {n} paired days · r · q line was a
|
||||
low-density sub-row under each sentence. Move the numeric
|
||||
detail behind an explainer icon on the same line as the
|
||||
finding so the list stays one row per pair. */}
|
||||
<span className="text-foreground flex items-center gap-1.5">
|
||||
<span>{t(sentenceKey, { factor: factorLabel })}</span>
|
||||
<MoodExplainerIcon
|
||||
label={t("insights.mood.discovery.statLabel")}
|
||||
detail={t("insights.mood.discovery.stat", {
|
||||
n: pair.n,
|
||||
r: pair.r.toFixed(2),
|
||||
q: pair.qValue.toFixed(3),
|
||||
})}
|
||||
/>
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<p className="text-muted-foreground text-[11px]">
|
||||
{t("insights.mood.discovery.footer", {
|
||||
tested: data.pairsTested,
|
||||
})}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-[11px]">
|
||||
{t("insights.mood.discovery.disclaimer")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import { Info } from "lucide-react";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* v1.12.4 (C3/C4) — compact explainer affordance for the mood surface.
|
||||
*
|
||||
* Replaces the low-density inline "how this was computed" text rows
|
||||
* (paired-day counts, the false-discovery footer, the observational
|
||||
* disclaimer) with a single ring/info icon. The detail moves into a
|
||||
* hover/focus tooltip so the surface stops spending a full-width row on a
|
||||
* footnote. The trigger is a real focusable `button` carrying an
|
||||
* `aria-label`, so the explanation is reachable by keyboard and screen
|
||||
* readers — not hover-only. Self-contained `TooltipProvider` so callers
|
||||
* don't need an ambient one.
|
||||
*/
|
||||
export function MoodExplainerIcon({
|
||||
label,
|
||||
detail,
|
||||
className,
|
||||
}: {
|
||||
/** Accessible name for the trigger (what the icon explains). */
|
||||
label: string;
|
||||
/** The explanation shown in the tooltip. */
|
||||
detail: string;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<TooltipProvider delayDuration={150}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
type="button"
|
||||
aria-label={label}
|
||||
className={cn(
|
||||
"text-muted-foreground hover:text-foreground focus-visible:ring-ring inline-flex size-5 shrink-0 items-center justify-center rounded-full border border-current/40 transition-colors focus-visible:ring-2 focus-visible:outline-none",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Info className="size-3" aria-hidden="true" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[16rem] text-xs leading-snug">
|
||||
{detail}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
@@ -64,8 +64,12 @@ export function MoodNarrativeFeed({ items }: { items: MoodNarrativeItem[] }) {
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
// v1.12.4 (C2) — the takeaways are short one-liners, so a single
|
||||
// full-width column wasted horizontal space and stacked into a long
|
||||
// ladder. Lay them two-up on anything wider than a phone; a lone
|
||||
// takeaway still spans the row rather than orphaning half of it.
|
||||
<ul
|
||||
className="space-y-2"
|
||||
className="grid gap-2 sm:grid-cols-2"
|
||||
data-slot="mood-narrative-feed"
|
||||
aria-label={t("insights.mood.narrative.title")}
|
||||
>
|
||||
@@ -82,7 +86,7 @@ export function MoodNarrativeFeed({ items }: { items: MoodNarrativeItem[] }) {
|
||||
return (
|
||||
<li
|
||||
key={`${item.kind}-${index}`}
|
||||
className="bg-card border-border flex items-start gap-3 rounded-lg border p-3"
|
||||
className="bg-card border-border flex h-full items-start gap-3 rounded-lg border p-3"
|
||||
>
|
||||
<Icon className="text-muted-foreground mt-0.5 size-4 shrink-0" />
|
||||
<p className="text-foreground/90 text-sm leading-snug">
|
||||
|
||||
Reference in new issue
Block a user