fuzzyworld

when-am-i / files / src / utils / lanes.ts

lanes.ts

Blame: the commit that last changed each line · file · history

ba77fd91 2026-06-19 Pavel Safronov1// Pack time intervals into non-overlapping horizontal lanes — the trick the
2// Wikipedia band-member timelines use to show concurrent roles as parallel
3// stripes instead of overpainting. Greedy: sweep by start time, reuse the first
4// lane whose last interval has already ended, else open a new lane.
5
6export interface Laned<T> {
7 item: T;
8 lane: number;
9}
10
11export function assignLanes<T>(
12 items: T[],
13 startOf: (item: T) => number,
14 endOf: (item: T) => number,
15): { lanes: Laned<T>[]; count: number } {
16 const ordered = items
17 .map((item, i) => ({ item, i }))
18 .sort((a, b) => {
19 const sa = startOf(a.item);
20 const sb = startOf(b.item);
21 if (sa < sb) return -1;
22 if (sa > sb) return 1;
23 return a.i - b.i; // stable for equal starts
24 });
25
26 const laneEnds: number[] = [];
27 const lanes: Laned<T>[] = [];
28 for (const { item } of ordered) {
29 const start = startOf(item);
30 // Half-open intervals: a lane is free if its last interval ended at or
31 // before this one starts.
32 let lane = laneEnds.findIndex((end) => end <= start);
33 if (lane === -1) {
34 lane = laneEnds.length;
35 laneEnds.push(endOf(item));
36 } else {
37 laneEnds[lane] = endOf(item);
38 }
39 lanes.push({ item, lane });
40 }
41
42 return { lanes, count: laneEnds.length };
43}