mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
fix(timeline): a name right of its own bar, one band per pair of rows, one line per notable
A closed period's name could not stand right of its own bar: the room check counted the bar it names as the next mark ahead, so "Away" went above its bar and sat on top of the life event named on the row above. The room ahead now counts only what starts to the right. A row's names and the names above the next row's bars share one band a few pixels tall, so whatever is placed on one is written into the other as well. The phone chronicle listed "First value of a measure" twice on a day when two metrics began on it, under one React key; it lists one line per kind of notable a day.
This commit is contained in:
1 parent
b274ee51ad
commit
27715c06e0
4 files changed
+64
-15
No files matched your search
@@ -256,3 +256,18 @@ describe("standingChips", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("notable lines", () => {
|
||||
it("lists one line per kind on a day, however many metrics share it", () => {
|
||||
const tl = fullTimeline();
|
||||
tl.notable = [
|
||||
{ date: "2026-08-30", kind: "firstValue" },
|
||||
{ date: "2026-08-30", kind: "firstValue" },
|
||||
{ date: "2026-08-30", kind: "extremeHigh" },
|
||||
] as typeof tl.notable;
|
||||
const lines = chronicleEntries(tl, TODAY).filter(
|
||||
(e) => e.kind === "notable" && e.date === "2026-08-30",
|
||||
);
|
||||
expect(lines).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
@@ -763,6 +763,12 @@ describe("label room (v1.42 final)", () => {
|
||||
expect(long[1].endsWith("…")).toBe(true);
|
||||
});
|
||||
|
||||
it("names a closed period right of its own bar when nothing follows it", () => {
|
||||
const occ = new LineOccupancy();
|
||||
occ.add("1:on", 100, 200);
|
||||
expect(placeText(occ, "1:on", 206, "Away", 1000, 4)).toBe("Away");
|
||||
});
|
||||
|
||||
it("keeps a gap before the next mark, cuts a name short, or leaves it out", () => {
|
||||
const occ = new LineOccupancy();
|
||||
occ.add("0:on", 200, 210);
|
||||
@@ -819,20 +825,20 @@ describe("label room (v1.42 final)", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("never lets two names of one lane line overlap or crowd each other", () => {
|
||||
it("never lets two names of one band overlap, on a row or across two", () => {
|
||||
for (const width of [390, 700, 1100, 1600]) {
|
||||
const layout = layoutAt(width);
|
||||
for (const lane of layout.lanes) {
|
||||
const byLine = new Map<number, ReturnType<typeof boxes>>();
|
||||
for (const box of boxes(lane)) {
|
||||
const list = byLine.get(box.y) ?? [];
|
||||
list.push(box);
|
||||
byLine.set(box.y, list);
|
||||
}
|
||||
for (const list of byLine.values()) {
|
||||
list.sort((p, q) => p.a - q.a);
|
||||
for (let i = 1; i < list.length; i++) {
|
||||
expect(list[i].a).toBeGreaterThanOrEqual(list[i - 1].b);
|
||||
// Two names closer than a text line apart vertically share a band
|
||||
// (a row's names and the names above the next row's bars): they
|
||||
// must not overlap sideways either.
|
||||
const all = boxes(lane);
|
||||
for (let i = 0; i < all.length; i++) {
|
||||
for (let j = i + 1; j < all.length; j++) {
|
||||
const p = all[i];
|
||||
const q = all[j];
|
||||
if (Math.abs(p.y - q.y) >= 12) continue;
|
||||
expect(p.b <= q.a || q.b <= p.a).toBe(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,7 +211,13 @@ export function chronicleEntries(
|
||||
}
|
||||
}
|
||||
}
|
||||
// One line per kind of notable on a day: two metrics seen for the first
|
||||
// time on one day are one "First value" line, not two identical ones.
|
||||
const notables = new Set<string>();
|
||||
for (const n of timeline.notable) {
|
||||
const key = `${n.date}:${n.kind}`;
|
||||
if (notables.has(key)) continue;
|
||||
notables.add(key);
|
||||
out.push({ kind: "notable", date: n.date, notable: n.kind });
|
||||
}
|
||||
const laneRank = (e: ChronicleEntry) =>
|
||||
|
||||
@@ -242,14 +242,15 @@ export class LineOccupancy {
|
||||
|
||||
/**
|
||||
* How far right a text starting at `a` may run on `line`: up to `margin`
|
||||
* before the next thing there. `a` itself when `a` is already too close to
|
||||
* something, Infinity when nothing lies to its right.
|
||||
* before the next thing that starts to its right. `a` itself when `a` lies
|
||||
* inside something, Infinity when nothing lies to its right. What ends
|
||||
* before `a` (the mark the text names) is the caller's `fits` to judge.
|
||||
*/
|
||||
room(line: string, a: number, margin = LABEL_MARGIN): number {
|
||||
let limit = Infinity;
|
||||
for (const [c, d] of this.lines.get(line) ?? []) {
|
||||
if (a < d + margin && a > c - margin) return a;
|
||||
if (c - margin >= a) limit = Math.min(limit, c - margin);
|
||||
if (c <= a && a < d) return a;
|
||||
if (c > a) limit = Math.min(limit, c - margin);
|
||||
}
|
||||
return limit;
|
||||
}
|
||||
@@ -261,6 +262,23 @@ export const NEIGHBOUR_GAP = 12;
|
||||
/** A label cut shorter than this many characters is left out instead. */
|
||||
export const MIN_LABEL_CHARS = 6;
|
||||
|
||||
/**
|
||||
* A name on one row's line and a name above the next row's bar sit a few
|
||||
* pixels apart vertically: the two lines share a band. Whatever is placed on
|
||||
* one is written into the other too, so the two never stack two names over
|
||||
* each other ("Moved into a bigger flat" over "Away").
|
||||
*/
|
||||
export function shareBand(
|
||||
occ: LineOccupancy,
|
||||
row: number,
|
||||
where: "on" | "above",
|
||||
a: number,
|
||||
b: number,
|
||||
): void {
|
||||
if (where === "on") occ.add(`${row + 1}:above`, a, b);
|
||||
else if (row > 0) occ.add(`${row - 1}:on`, a, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Put `text` on `line` from `a`: whole when it keeps {@link NEIGHBOUR_GAP}
|
||||
* from the next thing to its right and `margin` from everything else, else
|
||||
@@ -816,6 +834,7 @@ function placeMedicationLabels(
|
||||
margin,
|
||||
);
|
||||
if (placed === null) return false;
|
||||
shareBand(occ, row, where, a, a + estimateTextWidth(placed));
|
||||
labels.push({
|
||||
itemId: id,
|
||||
text: placed,
|
||||
@@ -942,6 +961,7 @@ function placeLabels(
|
||||
if (a < scale.x0) return false;
|
||||
const placed = placeText(occ, line(s.row, "above"), a, text, scale.x1 - 2);
|
||||
if (placed === null) return false;
|
||||
shareBand(occ, s.row, "above", a, a + estimateTextWidth(placed));
|
||||
labels.push({
|
||||
itemId: s.item.id,
|
||||
text: placed,
|
||||
@@ -973,6 +993,7 @@ function placeLabels(
|
||||
BESIDE_MARGIN,
|
||||
);
|
||||
if (placed === null) return false;
|
||||
shareBand(occ, row, "on", a, a + estimateTextWidth(placed));
|
||||
labels.push({ itemId: id, text: placed, x: a, y: y + 4, strong });
|
||||
return true;
|
||||
};
|
||||
@@ -989,6 +1010,7 @@ function placeLabels(
|
||||
const a = b - estimateTextWidth(text);
|
||||
if (!fitsCard(a, b)) return false;
|
||||
if (!occ.tryAdd(line(row, "on"), a, b, BESIDE_MARGIN)) return false;
|
||||
shareBand(occ, row, "on", a, b);
|
||||
labels.push({ itemId: id, text, x: a, y: y + 4, strong });
|
||||
return true;
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user