feat(insights): surface mood time-of-day, stability, and weight/BP correlations

Render the new mood analyses on the insights surface:

- a four-bar time-of-day chart (morning / afternoon / evening / night),
  shown only when the server marks the pattern reliable
- a stability tile beside the in-target tile, with the descriptive band
- the time-of-day takeaway in the narrative feed (new Clock icon, daypart
  label resolved from the bundle)

Also wire the mood x weight and mood x systolic-blood-pressure scatter
cards. Their Pearson coefficients and points were already computed and
returned by the endpoint but never rendered; they now sit alongside the
sleep / activity / pulse cards, each gated on the same minimum-pairs
threshold so a thin pairing renders an empty state rather than a noisy r.

New i18n keys land EN-first and propagate across de / es / fr / it / pl.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 12902b0cdbc6aa272b6da3c35ba3781f9216cf9f)
This commit is contained in:
Marc-André Bombeck committed 2026-06-02 16:45:14 +02:00
1 parent 141c083ff6
commit f65181bbf2
12 files changed
+405 -14

No files matched your search

+22 -2
View File
@@ -2412,7 +2412,11 @@
"strengthNone": "Keine",
"source": "{n} gepaarte Tage · r = {r}",
"emptyTitle": "Noch nicht genug gepaarte Daten",
"emptyDescription": "Erfasse Stimmung und diesen Wert an denselben Tagen, um einen Zusammenhang zu sehen."
"emptyDescription": "Erfasse Stimmung und diesen Wert an denselben Tagen, um einen Zusammenhang zu sehen.",
"weightTitle": "Stimmung vs. Gewicht",
"bloodPressureTitle": "Stimmung vs. Blutdruck",
"weightAxis": "Gewicht",
"bloodPressureAxis": "Systolisch (mmHg)"
},
"structuredTagsTitle": "Tag-Auswertung",
"inTargetValue": "{pct} %",
@@ -2428,7 +2432,23 @@
"tagLift": "Tage mit „{tag}“ liegen im Schnitt {delta} über deiner allgemeinen Stimmung.",
"tagDrop": "Tage mit „{tag}“ liegen {delta} unter deinem Durchschnitt.",
"inTarget": "Du warst an {pct} % der letzten 30 Tage in deinem guten Stimmungsbereich.",
"streak": "Du hast deine Stimmung {days} Tage in Folge erfasst – weiter so."
"streak": "Du hast deine Stimmung {days} Tage in Folge erfasst – weiter so.",
"timeOfDay": "Am {bucket} fühlst du dich am besten, im Schnitt {value}."
},
"timeOfDay": {
"title": "Stimmung nach Tageszeit",
"morning": "Morgen",
"afternoon": "Nachmittag",
"evening": "Abend",
"night": "Nacht",
"bestCaption": "Am {bucket} fühlst du dich tendenziell am besten."
},
"stability": {
"value": "{score}/100",
"bandVerySteady": "Deine Stimmung war sehr gleichmäßig",
"bandSteady": "Deine Stimmung war recht gleichmäßig",
"bandVariable": "Deine Stimmung war merklich schwankend",
"bandVeryVariable": "Deine Stimmung hat zuletzt stark geschwankt"
}
}
},
+22 -2
View File
@@ -2412,7 +2412,11 @@
"strengthNone": "None",
"source": "{n} paired days · r = {r}",
"emptyTitle": "Not enough paired data yet",
"emptyDescription": "Log mood and this metric on the same days to see a correlation."
"emptyDescription": "Log mood and this metric on the same days to see a correlation.",
"weightTitle": "Mood vs. weight",
"bloodPressureTitle": "Mood vs. blood pressure",
"weightAxis": "Weight",
"bloodPressureAxis": "Systolic (mmHg)"
},
"structuredTagsTitle": "Tag breakdown",
"inTargetValue": "{pct}%",
@@ -2428,7 +2432,23 @@
"tagLift": "Days tagged \"{tag}\" average {delta} above your overall mood.",
"tagDrop": "Days tagged \"{tag}\" run {delta} below your average.",
"inTarget": "You've been in your good-mood range {pct}% of the last 30 days.",
"streak": "You've logged your mood {days} days in a row — keep it up."
"streak": "You've logged your mood {days} days in a row — keep it up.",
"timeOfDay": "You feel best in the {bucket}, averaging {value}."
},
"timeOfDay": {
"title": "Mood by time of day",
"morning": "Morning",
"afternoon": "Afternoon",
"evening": "Evening",
"night": "Night",
"bestCaption": "You tend to feel best in the {bucket}."
},
"stability": {
"value": "{score}/100",
"bandVerySteady": "Your mood has been very steady",
"bandSteady": "Your mood has been fairly steady",
"bandVariable": "Your mood has been noticeably variable",
"bandVeryVariable": "Your mood has swung a lot lately"
}
}
},
+22 -2
View File
@@ -2412,7 +2412,11 @@
"strengthNone": "Ninguna",
"source": "{n} días emparejados · r = {r}",
"emptyTitle": "Aún no hay suficientes datos emparejados",
"emptyDescription": "Registra el ánimo y esta métrica los mismos días para ver una correlación."
"emptyDescription": "Registra el ánimo y esta métrica los mismos días para ver una correlación.",
"weightTitle": "Ánimo vs. peso",
"bloodPressureTitle": "Ánimo vs. presión arterial",
"weightAxis": "Peso",
"bloodPressureAxis": "Sistólica (mmHg)"
},
"structuredTagsTitle": "Desglose de etiquetas",
"inTargetValue": "{pct} %",
@@ -2428,7 +2432,23 @@
"tagLift": "Los días con la etiqueta «{tag}» promedian {delta} por encima de tu ánimo general.",
"tagDrop": "Los días con la etiqueta «{tag}» quedan {delta} por debajo de tu media.",
"inTarget": "Has estado en tu rango de buen ánimo el {pct} % de los últimos 30 días.",
"streak": "Has registrado tu ánimo {days} días seguidos: sigue así."
"streak": "Has registrado tu ánimo {days} días seguidos: sigue así.",
"timeOfDay": "Te sientes mejor por la {bucket}, con una media de {value}."
},
"timeOfDay": {
"title": "Estado de ánimo por hora del día",
"morning": "Mañana",
"afternoon": "Tarde",
"evening": "Noche",
"night": "Madrugada",
"bestCaption": "Sueles sentirte mejor por la {bucket}."
},
"stability": {
"value": "{score}/100",
"bandVerySteady": "Tu estado de ánimo ha sido muy estable",
"bandSteady": "Tu estado de ánimo ha sido bastante estable",
"bandVariable": "Tu estado de ánimo ha sido notablemente variable",
"bandVeryVariable": "Tu estado de ánimo ha fluctuado mucho últimamente"
}
}
},
+22 -2
View File
@@ -2412,7 +2412,11 @@
"strengthNone": "Aucune",
"source": "{n} jours appariés · r = {r}",
"emptyTitle": "Pas encore assez de données appariées",
"emptyDescription": "Enregistrez l'humeur et cette mesure les mêmes jours pour voir une corrélation."
"emptyDescription": "Enregistrez l'humeur et cette mesure les mêmes jours pour voir une corrélation.",
"weightTitle": "Humeur vs. poids",
"bloodPressureTitle": "Humeur vs. tension artérielle",
"weightAxis": "Poids",
"bloodPressureAxis": "Systolique (mmHg)"
},
"structuredTagsTitle": "Répartition des étiquettes",
"inTargetValue": "{pct} %",
@@ -2428,7 +2432,23 @@
"tagLift": "Les jours marqués « {tag} » dépassent votre humeur générale de {delta} en moyenne.",
"tagDrop": "Les jours marqués « {tag} » sont {delta} sous votre moyenne.",
"inTarget": "Vous avez été dans votre plage de bonne humeur {pct} % des 30 derniers jours.",
"streak": "Vous avez noté votre humeur {days} jours d'affilée — continuez."
"streak": "Vous avez noté votre humeur {days} jours d'affilée — continuez.",
"timeOfDay": "Vous vous sentez mieux le {bucket}, en moyenne {value}."
},
"timeOfDay": {
"title": "Humeur selon l'heure de la journée",
"morning": "Matin",
"afternoon": "Après-midi",
"evening": "Soir",
"night": "Nuit",
"bestCaption": "Vous vous sentez généralement mieux le {bucket}."
},
"stability": {
"value": "{score}/100",
"bandVerySteady": "Votre humeur a été très stable",
"bandSteady": "Votre humeur a été plutôt stable",
"bandVariable": "Votre humeur a été nettement variable",
"bandVeryVariable": "Votre humeur a beaucoup fluctué récemment"
}
}
},
+22 -2
View File
@@ -2412,7 +2412,11 @@
"strengthNone": "Nessuna",
"source": "{n} giorni appaiati · r = {r}",
"emptyTitle": "Non ci sono ancora abbastanza dati appaiati",
"emptyDescription": "Registra umore e questa metrica negli stessi giorni per vedere una correlazione."
"emptyDescription": "Registra umore e questa metrica negli stessi giorni per vedere una correlazione.",
"weightTitle": "Umore vs. peso",
"bloodPressureTitle": "Umore vs. pressione sanguigna",
"weightAxis": "Peso",
"bloodPressureAxis": "Sistolica (mmHg)"
},
"structuredTagsTitle": "Analisi etichette",
"inTargetValue": "{pct} %",
@@ -2428,7 +2432,23 @@
"tagLift": "I giorni con l'etichetta «{tag}» superano in media il tuo umore generale di {delta}.",
"tagDrop": "I giorni con l'etichetta «{tag}» restano {delta} sotto la tua media.",
"inTarget": "Sei stato nel tuo intervallo di buon umore il {pct} % degli ultimi 30 giorni.",
"streak": "Hai registrato il tuo umore per {days} giorni di fila — continua così."
"streak": "Hai registrato il tuo umore per {days} giorni di fila — continua così.",
"timeOfDay": "Ti senti meglio di {bucket}, con una media di {value}."
},
"timeOfDay": {
"title": "Umore per fascia oraria",
"morning": "Mattina",
"afternoon": "Pomeriggio",
"evening": "Sera",
"night": "Notte",
"bestCaption": "Tendi a sentirti meglio di {bucket}."
},
"stability": {
"value": "{score}/100",
"bandVerySteady": "Il tuo umore è stato molto stabile",
"bandSteady": "Il tuo umore è stato abbastanza stabile",
"bandVariable": "Il tuo umore è stato notevolmente variabile",
"bandVeryVariable": "Il tuo umore ha oscillato molto di recente"
}
}
},
+22 -2
View File
@@ -2412,7 +2412,11 @@
"strengthNone": "Brak",
"source": "{n} sparowanych dni · r = {r}",
"emptyTitle": "Za mało sparowanych danych",
"emptyDescription": "Rejestruj nastrój i ten wskaźnik w te same dni, aby zobaczyć korelację."
"emptyDescription": "Rejestruj nastrój i ten wskaźnik w te same dni, aby zobaczyć korelację.",
"weightTitle": "Nastrój vs. waga",
"bloodPressureTitle": "Nastrój vs. ciśnienie krwi",
"weightAxis": "Waga",
"bloodPressureAxis": "Skurczowe (mmHg)"
},
"structuredTagsTitle": "Podział tagów",
"inTargetValue": "{pct}%",
@@ -2428,7 +2432,23 @@
"tagLift": "Dni z tagiem „{tag}” są średnio o {delta} powyżej twojego ogólnego nastroju.",
"tagDrop": "Dni z tagiem „{tag}” są o {delta} poniżej twojej średniej.",
"inTarget": "Przez {pct}% ostatnich 30 dni byłeś w zakresie dobrego nastroju.",
"streak": "Rejestrujesz nastrój od {days} dni z rzędu — tak trzymaj."
"streak": "Rejestrujesz nastrój od {days} dni z rzędu — tak trzymaj.",
"timeOfDay": "Najlepiej czujesz się {bucket}, średnio {value}."
},
"timeOfDay": {
"title": "Nastrój według pory dnia",
"morning": "Rano",
"afternoon": "Popołudnie",
"evening": "Wieczór",
"night": "Noc",
"bestCaption": "Zwykle najlepiej czujesz się {bucket}."
},
"stability": {
"value": "{score}/100",
"bandVerySteady": "Twój nastrój był bardzo stabilny",
"bandSteady": "Twój nastrój był dość stabilny",
"bandVariable": "Twój nastrój był zauważalnie zmienny",
"bandVeryVariable": "Twój nastrój ostatnio mocno się wahał"
}
}
},
@@ -32,6 +32,13 @@ type Response = {
heatmap: { windowDays: number; cells: [] };
distribution: [];
weekday: [];
timeOfDay: {
buckets: [];
reliable: boolean;
best: null;
worst: null;
};
stability: null;
tags: [];
structuredTags: [];
narratives: Array<{
@@ -43,6 +50,8 @@ type Response = {
sleep: EmptyCorrelation;
steps: EmptyCorrelation;
pulse: EmptyCorrelation;
weight: EmptyCorrelation;
bloodPressureSystolic: EmptyCorrelation;
};
};
@@ -70,6 +79,8 @@ function baseData(inTargetPct: number | null): Response {
heatmap: { windowDays: 30, cells: [] },
distribution: [],
weekday: [],
timeOfDay: { buckets: [], reliable: false, best: null, worst: null },
stability: null,
tags: [],
structuredTags: [],
narratives: [
@@ -88,6 +99,8 @@ function baseData(inTargetPct: number | null): Response {
sleep: emptyCorrelation,
steps: emptyCorrelation,
pulse: emptyCorrelation,
weight: emptyCorrelation,
bloodPressureSystolic: emptyCorrelation,
},
};
}
@@ -45,24 +45,30 @@ export interface MoodMetricCorrelationData {
n: number;
}
type MetricKind = "sleep" | "steps" | "pulse";
type MetricKind = "sleep" | "steps" | "pulse" | "weight" | "bloodPressure";
const TITLE_KEY: Record<MetricKind, string> = {
sleep: "insights.mood.correlation.sleepTitle",
steps: "insights.mood.correlation.stepsTitle",
pulse: "insights.mood.correlation.pulseTitle",
weight: "insights.mood.correlation.weightTitle",
bloodPressure: "insights.mood.correlation.bloodPressureTitle",
};
const Y_LABEL_KEY: Record<MetricKind, string> = {
sleep: "insights.mood.correlation.sleepAxis",
steps: "insights.mood.correlation.stepsAxis",
pulse: "insights.mood.correlation.pulseAxis",
weight: "insights.mood.correlation.weightAxis",
bloodPressure: "insights.mood.correlation.bloodPressureAxis",
};
const FILL_BY_KIND: Record<MetricKind, string> = {
sleep: "var(--dracula-purple)",
steps: "var(--dracula-cyan)",
pulse: "var(--dracula-pink)",
weight: "var(--dracula-green)",
bloodPressure: "var(--dracula-orange)",
};
const STRENGTH_KEY: Record<CorrelationResult["strength"], string> = {
@@ -136,16 +142,22 @@ export function MoodCorrelationCards({
sleep,
steps,
pulse,
weight,
bloodPressureSystolic,
}: {
sleep: MoodMetricCorrelationData;
steps: MoodMetricCorrelationData;
pulse: MoodMetricCorrelationData;
weight: MoodMetricCorrelationData;
bloodPressureSystolic: MoodMetricCorrelationData;
}) {
return (
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
<MoodCorrelationCard kind="sleep" data={sleep} />
<MoodCorrelationCard kind="steps" data={steps} />
<MoodCorrelationCard kind="pulse" data={pulse} />
<MoodCorrelationCard kind="weight" data={weight} />
<MoodCorrelationCard kind="bloodPressure" data={bloodPressureSystolic} />
</div>
);
}
@@ -33,6 +33,14 @@ import {
type MoodNarrativeItem,
} from "./mood-narrative-feed";
import { MoodInTargetTile } from "./mood-in-target-tile";
import {
MoodTimeOfDayChart,
type MoodTimeOfDayPattern,
} from "./mood-time-of-day-chart";
import {
MoodStabilityTile,
type MoodStabilityData,
} from "./mood-stability-tile";
/**
* v1.8.5 — additional Mood Insights sections.
@@ -55,6 +63,8 @@ interface MoodInsightsResponse {
};
distribution: MoodDistributionRow[];
weekday: MoodWeekdayRow[];
timeOfDay: MoodTimeOfDayPattern;
stability: MoodStabilityData | null;
tags: MoodTagRow[];
structuredTags: MoodStructuredTagRow[];
narratives: MoodNarrativeItem[];
@@ -62,6 +72,8 @@ interface MoodInsightsResponse {
sleep: MoodMetricCorrelationData;
steps: MoodMetricCorrelationData;
pulse: MoodMetricCorrelationData;
weight: MoodMetricCorrelationData;
bloodPressureSystolic: MoodMetricCorrelationData;
};
}
@@ -122,9 +134,17 @@ export function MoodInsightsSections() {
? data.narratives.filter((item) => item.kind !== "in-target")
: data.narratives;
const hasStabilityTile = data.stability != null;
const hasInTargetTile = data.summary.inTargetPct != null;
return (
<div className="space-y-4">
<MoodInTargetTile pct={data.summary.inTargetPct} />
{(hasInTargetTile || hasStabilityTile) && (
<div className="grid gap-4 sm:grid-cols-2">
<MoodInTargetTile pct={data.summary.inTargetPct} />
<MoodStabilityTile stability={data.stability} />
</div>
)}
<MoodNarrativeFeed items={narratives} />
@@ -145,6 +165,12 @@ export function MoodInsightsSections() {
</SectionCard>
</div>
{data.timeOfDay.reliable && (
<SectionCard title={t("insights.mood.timeOfDay.title")}>
<MoodTimeOfDayChart pattern={data.timeOfDay} />
</SectionCard>
)}
{hasStructuredTags && (
<SectionCard title={t("insights.mood.structuredTagsTitle")}>
<MoodStructuredTagBreakdown tags={data.structuredTags} />
@@ -168,6 +194,8 @@ export function MoodInsightsSections() {
sleep={data.correlations.sleep}
steps={data.correlations.steps}
pulse={data.correlations.pulse}
weight={data.correlations.weight}
bloodPressureSystolic={data.correlations.bloodPressureSystolic}
/>
</div>
</div>
@@ -4,6 +4,7 @@ import {
ArrowDownRight,
ArrowUpRight,
CalendarDays,
Clock,
Flame,
Tag,
Target,
@@ -42,6 +43,7 @@ const ICONS: Record<string, ComponentType<{ className?: string }>> = {
"tag-drop": ArrowDownRight,
"in-target": Target,
streak: Flame,
"time-of-day": Clock,
};
function pickIcon(item: MoodNarrativeItem): ComponentType<{
@@ -76,6 +78,7 @@ export function MoodNarrativeFeed({ items }: { items: MoodNarrativeItem[] }) {
const vars: Record<string, string> = { ...item.vars };
if (vars.weekdayKey) vars.weekday = t(vars.weekdayKey);
if (vars.tagKey) vars.tag = t(vars.tagKey);
if (vars.bucketKey) vars.bucket = t(vars.bucketKey);
return (
<li
key={`${item.kind}-${index}`}
@@ -0,0 +1,66 @@
"use client";
import { Activity } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
import { useTranslations } from "@/lib/i18n/context";
/**
* v1.9.0 — mood-stability tile.
*
* Surfaces the day-to-day stability score (0..100, higher = steadier)
* computed server-side from the population standard deviation of the
* daily means, plus a descriptive, non-judgemental band. Some variation
* is healthy, so the band reads "steady" / "variable", never
* "good" / "bad". Renders nothing when the score is unavailable (a sparse
* logger below the minimum-days floor) so the page degrades gracefully.
*/
export type MoodStabilityBand =
| "verySteady"
| "steady"
| "variable"
| "veryVariable";
export interface MoodStabilityData {
score: number;
band: MoodStabilityBand;
days: number;
}
const BAND_KEY: Record<MoodStabilityBand, string> = {
verySteady: "insights.mood.stability.bandVerySteady",
steady: "insights.mood.stability.bandSteady",
variable: "insights.mood.stability.bandVariable",
veryVariable: "insights.mood.stability.bandVeryVariable",
};
export function MoodStabilityTile({
stability,
}: {
stability: MoodStabilityData | null;
}) {
const { t } = useTranslations();
if (stability == null) return null;
return (
<Card>
<CardContent className="flex items-center gap-4 py-4">
<div className="bg-muted text-muted-foreground flex size-10 shrink-0 items-center justify-center rounded-full">
<Activity className="size-5" />
</div>
<div className="min-w-0">
<p className="text-foreground text-2xl font-semibold tabular-nums">
{t("insights.mood.stability.value", {
score: String(stability.score),
})}
</p>
<p className="text-muted-foreground text-sm">
{t(BAND_KEY[stability.band])}
</p>
</div>
</CardContent>
</Card>
);
}
@@ -0,0 +1,149 @@
"use client";
import {
Bar,
BarChart,
Cell,
ReferenceLine,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { useTranslations } from "@/lib/i18n/context";
/**
* v1.9.0 — average mood per part of day.
*
* Four bars (morning / afternoon / evening / night) over the average
* mood logged in each part of the user's local day. The buckets are
* computed server-side in each entry's own timezone and gated on a
* spread floor (`reliable`); the parent only mounts this chart when the
* pattern is trustworthy, so a once-a-day logger never sees a misleading
* single bar dressed up as a daypart preference. Empty buckets render as
* a muted zero-height bar so the four-column rhythm stays intact.
*/
export type MoodTimeOfDayBucket =
| "morning"
| "afternoon"
| "evening"
| "night";
export interface MoodTimeOfDayRow {
bucket: MoodTimeOfDayBucket;
avgScore: number | null;
count: number;
}
export interface MoodTimeOfDayPattern {
buckets: MoodTimeOfDayRow[];
reliable: boolean;
best: MoodTimeOfDayBucket | null;
worst: MoodTimeOfDayBucket | null;
}
const LABEL_KEYS: Record<MoodTimeOfDayBucket, string> = {
morning: "insights.mood.timeOfDay.morning",
afternoon: "insights.mood.timeOfDay.afternoon",
evening: "insights.mood.timeOfDay.evening",
night: "insights.mood.timeOfDay.night",
};
function colorForScore(score: number): string {
if (score < 2) return "var(--dracula-red)";
if (score < 3) return "var(--dracula-orange)";
if (score < 3.5) return "var(--dracula-yellow)";
return "var(--dracula-green)";
}
export function MoodTimeOfDayChart({
pattern,
}: {
pattern: MoodTimeOfDayPattern;
}) {
const { t } = useTranslations();
const data = pattern.buckets.map((row) => ({
bucket: row.bucket,
label: t(LABEL_KEYS[row.bucket]),
value: row.avgScore ?? 0,
avgScore: row.avgScore,
count: row.count,
}));
const bestLabel = pattern.best ? t(LABEL_KEYS[pattern.best]) : null;
return (
<div className="space-y-2">
<div className="aspect-[3/2] min-h-[160px] w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={data}
margin={{ top: 8, right: 12, bottom: 8, left: 0 }}
>
<XAxis
dataKey="label"
tick={{ fontSize: 11, fill: "var(--dracula-fg)" }}
stroke="var(--dracula-comment)"
interval={0}
/>
<YAxis
domain={[1, 5]}
ticks={[1, 2, 3, 4, 5]}
tick={{ fontSize: 11, fill: "var(--dracula-fg)" }}
stroke="var(--dracula-comment)"
width={24}
/>
<ReferenceLine
y={3.5}
stroke="var(--dracula-green)"
strokeDasharray="3 3"
/>
<Tooltip
cursor={{ fill: "var(--secondary)", opacity: 0.4 }}
contentStyle={{
backgroundColor: "var(--card)",
border: "1px solid var(--border)",
borderRadius: "0.5rem",
fontSize: "0.75rem",
}}
itemStyle={{ color: "var(--dracula-fg)" }}
labelStyle={{ color: "var(--dracula-fg)" }}
formatter={(_value, _name, item) => {
const payload = item?.payload as
| { avgScore: number | null; count: number }
| undefined;
if (!payload || payload.avgScore == null) {
return [t("insights.mood.weekdayNoData"), ""];
}
return [
`${payload.avgScore.toFixed(1)} (${payload.count})`,
t("insights.mood.timeOfDay.title"),
];
}}
/>
<Bar dataKey="value" radius={[3, 3, 0, 0]}>
{data.map((row) => (
<Cell
key={row.bucket}
fill={
row.avgScore == null
? "var(--secondary)"
: colorForScore(row.avgScore)
}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
{bestLabel && (
<p className="text-muted-foreground text-xs">
{t("insights.mood.timeOfDay.bestCaption", { bucket: bestLabel })}
</p>
)}
</div>
);
}