+ {visibleBands.length > 0 ? (
+ // v1.4.27 R3d MB2 — band overlay positioning fix. The
+ // overlay used to inset `right: 18px` while the chart
+ // margin is `right: 8`, so the band rectangle drifted left
+ // of the plotted line by 10 px on every viewport. Pin the
+ // overlay to the same right edge the ComposedChart uses so
+ // the band tracks the line exactly.
+
-
-
- tzFmt.date(
- new Date(
- chartData?.[Math.round(value)]?.timestamp ?? Date.now(),
- ),
- )
- }
- // v1.4.29 — Recharts ignores `interval` on numeric
- // axes (`type="number"`). Hand explicit tick
- // positions through the `ticks` prop so the legacy
- // day-aware density policy stays effective on the
- // pulse chart.
- ticks={computeTickPositions(chartData ?? [], viewportWidth)}
- padding={{ left: 10, right: 10 }}
- tickMargin={10}
- />
-
- typeof value === "number"
- ? formatAxisValue(
- useDecimalAxis ? value : Math.round(value),
- )
- : String(value)
- }
- unit={
- showYAxisUnit && (yAxisUnit ?? unit)
- ? ` ${yAxisUnit ?? unit}`
- : undefined
- }
- />
- {visibleTargetZones.map((zone) => (
- (
+
))}
- {visibleTargetZones.map((zone) => (
-
+ ) : null}
+
+
+
+
+
- ))}
- {visibleTargetZones.map((zone) => (
-
+ tzFmt.date(
+ new Date(
+ chartData?.[Math.round(value)]?.timestamp ??
+ Date.now(),
+ ),
+ )
+ }
+ // v1.4.29 — Recharts ignores `interval` on numeric
+ // axes (`type="number"`). Hand explicit tick
+ // positions through the `ticks` prop so the legacy
+ // day-aware density policy stays effective on the
+ // pulse chart.
+ ticks={computeTickPositions(chartData ?? [], viewportWidth)}
+ padding={{ left: 10, right: 10 }}
+ tickMargin={10}
+ />
+
+ typeof value === "number"
+ ? formatAxisValue(
+ useDecimalAxis ? value : Math.round(value),
+ )
+ : String(value)
+ }
+ unit={
+ showYAxisUnit && (yAxisUnit ?? unit)
+ ? ` ${yAxisUnit ?? unit}`
: undefined
}
/>
- ))}
- {/* v1.4.20 phase B4 — storyboard annotations.
+ {visibleTargetZones.map((zone) => (
+
+ ))}
+ {visibleTargetZones.map((zone) => (
+
+ ))}
+ {visibleTargetZones.map((zone) => (
+
+ ))}
+ {/* v1.4.20 phase B4 — storyboard annotations.
Vertical reference lines pinned to a specific
visible pointIndex; the label sits above with a
short truncation on `=sm. Annotations off-window silently
drop via `ifOverflow="discard"`. */}
- {annotationPositions.map((annotation, i) => (
-
- ))}
- {/* v1.4.25 W6 — GLP-1 injection-day vertical markers.
+ {annotationPositions.map((annotation, i) => (
+
+ ))}
+ {/* v1.4.25 W6 — GLP-1 injection-day vertical markers.
Thin dashed line + a small filled dot at the x-axis
intersection so a row of markers reads as "injection
cadence" without crowding the canvas. Optional label
rendered only when the caller passed one; the
dashboard tile leaves it undefined (the date is
redundant — the chart's x-axis already shows it). */}
- {verticalMarkerPositions.map((marker, i) => (
-
- ))}
- {yDomain &&
- verticalMarkerPositions.map((marker, i) => (
- (
+
))}
- {/* v1.4.18 — personal-baseline reference line is now
+ {yDomain &&
+ verticalMarkerPositions.map((marker, i) => (
+
+ ))}
+ {/* v1.4.18 — personal-baseline reference line is now
opt-in via the Trend toggle. the maintainer rejected the
always-on dashed mean line; it now only paints when
the user actively shows the trend overlay (matching
@@ -2005,177 +2054,181 @@ export function HealthChart({
90-day rolling median per type, faint dashed line,
only the first type gets the inline label so
multi-type charts don't paint duplicate labels. */}
- {showTrend &&
- types.map((type, i) => {
- const baseline = personalBaselines.get(type);
- if (baseline == null) return null;
- return (
- {
+ const baseline = personalBaselines.get(type);
+ if (baseline == null) return null;
+ return (
+
+ );
+ })}
+ {
+ const {
+ active,
+ payload,
+ label: rechartsLabel,
+ } = props as unknown as {
+ active?: boolean;
+ payload?: Array<{
+ name?: string;
+ value?: number;
+ color?: string;
+ dataKey?: string;
+ payload?: ChartDataPoint;
+ }>;
+ label?: number;
+ };
+ if (!active || !payload?.length) return null;
+ const ts =
+ payload[0]?.payload?.timestamp ??
+ (typeof rechartsLabel === "number"
+ ? chartData?.[Math.round(rechartsLabel)]?.timestamp
+ : undefined);
+ const dateLabel = ts ? tzFmt.date(new Date(ts)) : "";
+ const rows: RichTooltipRow[] = [];
+ // Build a quick lookup of compare values for this
+ // hover-day so the current-period row can attach
+ // a "vs. last month / year" delta inline. Same-day
+ // current ↔ compare values come from the SAME
+ // payload object.
+ const hoverPoint = payload[0]?.payload as
+ ChartDataPoint | undefined;
+ for (const item of payload) {
+ if (typeof item.value !== "number") continue;
+ const dataKey = String(item.dataKey ?? "");
+ // Skip auxiliary lines (`*_ma`, `*_trend`,
+ // `*_compare`) — the comparison value is rendered
+ // inline as the delta on the current-period row,
+ // and ma / trend already appear as dashed overlays
+ // on the chart itself.
+ if (
+ dataKey.endsWith("_ma") ||
+ dataKey.endsWith("_trend") ||
+ dataKey.endsWith("_compare")
+ )
+ continue;
+ const baseline = personalBaselines.get(dataKey);
+ let delta: string | undefined;
+ // v1.4.16 phase B8 — prefer the comparison delta
+ // ("Δ −7 vs. last month") over the personal
+ // baseline delta when comparison is active and we
+ // have a prior value for this day. Otherwise fall
+ // back to the existing baseline-delta path.
+ const compareValue =
+ effectiveCompareBaseline !== "none" && hoverPoint
+ ? (hoverPoint[`${dataKey}_compare`] as
+ number | undefined)
+ : undefined;
+ if (
+ effectiveCompareBaseline !== "none" &&
+ typeof compareValue === "number" &&
+ Number.isFinite(compareValue)
+ ) {
+ const diff = item.value - compareValue;
+ if (Math.abs(diff) < 0.05) {
+ delta = t("charts.deltaUnchanged");
+ } else {
+ const sign = diff > 0 ? "+" : "−";
+ const formatted = `${sign}${fmt.number(
+ Math.abs(diff),
+ 1,
+ )}${unit ? ` ${unit}` : ""}`;
+ delta = t(
+ effectiveCompareBaseline === "lastMonth"
+ ? "comparison.deltaVs.lastMonth"
+ : "comparison.deltaVs.lastYear",
+ ).replace("{delta}", formatted);
+ }
+ } else if (baseline != null) {
+ const diff = item.value - baseline;
+ if (Math.abs(diff) < 0.05) {
+ delta = t("charts.deltaUnchanged");
+ } else {
+ const sign = diff > 0 ? "+" : "−";
+ const formatted = `${sign}${fmt.number(
+ Math.abs(diff),
+ 1,
+ )}${unit ? ` ${unit}` : ""}`;
+ delta = t("charts.deltaVsBaseline").replace(
+ "{delta}",
+ formatted,
+ );
+ }
}
- />
- );
- })}
- {
- const {
- active,
- payload,
- label: rechartsLabel,
- } = props as unknown as {
- active?: boolean;
- payload?: Array<{
- name?: string;
- value?: number;
- color?: string;
- dataKey?: string;
- payload?: ChartDataPoint;
- }>;
- label?: number;
- };
- if (!active || !payload?.length) return null;
- const ts =
- payload[0]?.payload?.timestamp ??
- (typeof rechartsLabel === "number"
- ? chartData?.[Math.round(rechartsLabel)]?.timestamp
- : undefined);
- const dateLabel = ts ? tzFmt.date(new Date(ts)) : "";
- const rows: RichTooltipRow[] = [];
- // Build a quick lookup of compare values for this
- // hover-day so the current-period row can attach
- // a "vs. last month / year" delta inline. Same-day
- // current ↔ compare values come from the SAME
- // payload object.
- const hoverPoint = payload[0]?.payload as
- ChartDataPoint | undefined;
- for (const item of payload) {
- if (typeof item.value !== "number") continue;
- const dataKey = String(item.dataKey ?? "");
- // Skip auxiliary lines (`*_ma`, `*_trend`,
- // `*_compare`) — the comparison value is rendered
- // inline as the delta on the current-period row,
- // and ma / trend already appear as dashed overlays
- // on the chart itself.
- if (
- dataKey.endsWith("_ma") ||
- dataKey.endsWith("_trend") ||
- dataKey.endsWith("_compare")
- )
- continue;
- const baseline = personalBaselines.get(dataKey);
- let delta: string | undefined;
- // v1.4.16 phase B8 — prefer the comparison delta
- // ("Δ −7 vs. last month") over the personal
- // baseline delta when comparison is active and we
- // have a prior value for this day. Otherwise fall
- // back to the existing baseline-delta path.
- const compareValue =
- effectiveCompareBaseline !== "none" && hoverPoint
- ? (hoverPoint[`${dataKey}_compare`] as
- number | undefined)
- : undefined;
- if (
- effectiveCompareBaseline !== "none" &&
- typeof compareValue === "number" &&
- Number.isFinite(compareValue)
- ) {
- const diff = item.value - compareValue;
- if (Math.abs(diff) < 0.05) {
- delta = t("charts.deltaUnchanged");
- } else {
- const sign = diff > 0 ? "+" : "−";
- const formatted = `${sign}${fmt.number(
- Math.abs(diff),
- 1,
- )}${unit ? ` ${unit}` : ""}`;
- delta = t(
- effectiveCompareBaseline === "lastMonth"
- ? "comparison.deltaVs.lastMonth"
- : "comparison.deltaVs.lastYear",
- ).replace("{delta}", formatted);
- }
- } else if (baseline != null) {
- const diff = item.value - baseline;
- if (Math.abs(diff) < 0.05) {
- delta = t("charts.deltaUnchanged");
- } else {
- const sign = diff > 0 ? "+" : "−";
- const formatted = `${sign}${fmt.number(
- Math.abs(diff),
- 1,
- )}${unit ? ` ${unit}` : ""}`;
- delta = t("charts.deltaVsBaseline").replace(
- "{delta}",
- formatted,
- );
- }
- }
- rows.push({
- name: item.name ?? dataKey,
- value: `${formatTooltipValue(item.value)}${
- unit ? ` ${unit}` : ""
- }`,
- color: item.color ?? "var(--dracula-purple)",
- delta,
- });
- // v1.4.16 phase B8 — also surface the prior-period
- // value as its own row so the user reads both
- // numbers (current AND last-month / last-year)
- // alongside the delta.
- if (
- effectiveCompareBaseline !== "none" &&
- typeof compareValue === "number" &&
- Number.isFinite(compareValue)
- ) {
rows.push({
- name: `${item.name ?? dataKey} · ${t(
- "comparison.tooltipPrior",
- )}`,
- value: `${formatTooltipValue(compareValue)}${
+ name: item.name ?? dataKey,
+ value: `${formatTooltipValue(item.value)}${
unit ? ` ${unit}` : ""
}`,
color: item.color ?? "var(--dracula-purple)",
+ delta,
});
+ // v1.4.16 phase B8 — also surface the prior-period
+ // value as its own row so the user reads both
+ // numbers (current AND last-month / last-year)
+ // alongside the delta.
+ if (
+ effectiveCompareBaseline !== "none" &&
+ typeof compareValue === "number" &&
+ Number.isFinite(compareValue)
+ ) {
+ rows.push({
+ name: `${item.name ?? dataKey} · ${t(
+ "comparison.tooltipPrior",
+ )}`,
+ value: `${formatTooltipValue(compareValue)}${
+ unit ? ` ${unit}` : ""
+ }`,
+ color: item.color ?? "var(--dracula-purple)",
+ });
+ }
}
- }
- if (rows.length === 0) return null;
- return (
-
- );
- }}
- />
- {showContextDetails && (
-
+ );
}}
/>
- )}
- {/* v1.8.5 — min–max range band. An Apple-Health-style
+ {showContextDetails && (
+
+ )}
+ {/* v1.8.5 — min–max range band. An Apple-Health-style
shaded area between each day's min and max, painted
behind the mean line so it reads as context, not a
second series. Gated on `!mini` (the dashboard
@@ -2185,69 +2238,69 @@ export function HealthChart({
weight, glucose, BP). The toggle rides the existing
target-range pref so the user controls both bands
from one switch. */}
- {!mini &&
- showBands &&
- types.map((type, i) => (
- (
+
+ ))}
+ {types.map((type, i) => (
+
))}
- {types.map((type, i) => (
-
- ))}
- {showMA &&
- types.map((type, i) => (
-
- ))}
- {showTrend &&
- types.map((type) => (
-
- ))}
- {/* v1.4.16 phase B8 — comparison overlay.
+ {showMA &&
+ types.map((type, i) => (
+
+ ))}
+ {showTrend &&
+ types.map((type) => (
+
+ ))}
+ {/* v1.4.16 phase B8 — comparison overlay.
A dimmed dashed line per type for the prior period
(lastMonth / lastYear), painted BENEATH the current
series via Recharts' source-order layering. Same
@@ -2255,48 +2308,66 @@ export function HealthChart({
stroke opacity (45%) and a thinner stroke (1.25)
so the user reads the current line first and the
overlay as orientation. */}
- {effectiveCompareBaseline !== "none" &&
- hasComparisonData &&
- types.map((type, i) => (
-
- ))}
-
-
-
- {/* Sparse-data caption. With fewer than three daily points the
+ {effectiveCompareBaseline !== "none" &&
+ hasComparisonData &&
+ types.map((type, i) => (
+
+ ))}
+
+
+
+ {/* Sparse-data caption. With fewer than three daily points the
chart still paints every reading (a single marker for one
day, a line for two), and this subtle note sets the
expectation that more days fill out the trend — rather than
withholding the data behind a placeholder card. */}
- {!mini && (chartData?.length ?? 0) < 3 ? (
-
- {t("charts.sparseDataCaption")}
-
+ {!mini && (chartData?.length ?? 0) < 3 ? (
+
+ {t("charts.sparseDataCaption")}
+
+ ) : null}
+
+ {/* The points the chart just drew, as a table. Reads the SAME
+ expression `