fix(charts): mark only the chart's own point days in the day rug

On an index or band axis the rug placed every day that holds anything
by interpolating it between the two nearest points. A sparse series
beside a daily one squeezed a week of other days into one gap, and the
dots ran together into bars. A day now gets a dot on those axes only
when it is one of the chart's points; a time axis keeps every day of
its span.
This commit is contained in:
Marc-André Bombeck committed 2026-10-23 19:53:28 +02:00
1 parent 9d1147a052
commit d5ca594bd7
2 files changed
+72 -9

No files matched your search

@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
import {
chartPointDayKey,
rugFraction,
rugEntries,
rugPosition,
timeRugFraction,
} from "../chart-day";
@@ -191,3 +192,36 @@ describe("rug on a time axis drawn from real instants", () => {
expect(timeRugFraction("2026-03-06", points, days)).toBeNull();
});
});
describe("rugEntries", () => {
const points = ["2026-09-01", "2026-09-04", "2026-09-08", "2026-09-12"].map(
(day) => ({ timestamp: noon(day) }),
);
const everyDay = Array.from(
{ length: 12 },
(_, i) => `2026-09-${String(i + 1).padStart(2, "0")}`,
);
it("marks only the chart's own points on an index axis", () => {
// Days between two points have no place on an axis that spaces points;
// interpolated, a week of them ran together into a bar in one gap.
const entries = rugEntries(everyDay, points, "index");
expect(entries.map((e) => e.day)).toEqual([
"2026-09-01",
"2026-09-04",
"2026-09-08",
"2026-09-12",
]);
expect(entries.map((e) => e.at)).toEqual([0, 1 / 3, 2 / 3, 1]);
});
it("keeps every day of the span on a time axis", () => {
expect(rugEntries(everyDay, points, "time")).toHaveLength(12);
});
it("never puts two dots closer than one point step on an index axis", () => {
const entries = rugEntries(everyDay, points, "index");
const gaps = entries.slice(1).map((e, i) => e.at - entries[i]!.at);
expect(Math.min(...gaps)).toBeCloseTo(1 / (points.length - 1));
});
});
+38 -9
View File
@@ -168,6 +168,38 @@ export function timeRugFraction(
return Math.min(1, Math.max(0, (t - first) / (last - first)));
}
/**
* The rug's dots: each day that holds anything, where it sits along the plot.
*
* On an index or band axis a day has a place only if it is one of the
* chart's points, because the axis spaces points, not days. A day between
* two points used to be interpolated into the gap between them, so a sparse
* series (blood pressure taken twice a week beside a mood logged every day)
* squeezed a week of other days into one gap and their dots ran together
* into bars. A time axis spaces days and keeps every day of its span.
*/
export function rugEntries(
days: readonly string[],
points: readonly RugPoint[],
axis: ChartDayAxis,
pointDays?: ReadonlyArray<DateKey | null>,
): Array<{ day: string; at: number }> {
const onPoints =
axis === "time"
? null
: new Set(points.map((point) => chartPointDayKey(point.timestamp)));
return days
.filter((day) => onPoints === null || onPoints.has(day))
.map((day) => ({
day,
at:
axis === "time" && pointDays !== undefined
? timeRugFraction(day as DateKey, points, pointDays)
: rugFraction(day as DateKey, points, axis),
}))
.filter((entry): entry is { day: string; at: number } => entry.at !== null);
}
/**
* The row of day dots under a daily chart. `insetLeft` / `insetRight` are the
* pixels between the chart box and the first and last point (margin, axis,
@@ -216,15 +248,12 @@ export function DayRug({
const index = useDayIndex(from, to);
const data = index.data;
const notable = new Set(data?.notable ?? []);
const days = Object.keys(data?.days ?? {})
.filter((day) => isOpenableDay(day, today))
.map((day) => ({
day,
at: byDays
? timeRugFraction(day, points, pointDays)
: rugFraction(day, points, axis),
}))
.filter((entry): entry is { day: string; at: number } => entry.at !== null);
const days = rugEntries(
Object.keys(data?.days ?? {}).filter((day) => isOpenableDay(day, today)),
points,
axis,
byDays ? pointDays : undefined,
);
return (
<div
aria-hidden="true"