Timeline: draw stage-period ranges from activities, milestones on top
Each stage-transition activity now becomes a horizontal range on its lane: start = the activity's date, end = the next activity with a higher stage rank, or extending to today if still in effect. Milestone date-field dots overlay on top of the ranges. Activity dots at each range start carry a tooltip with the activity subject. Added computeStageRanges in lib/stageRank.ts and switched DateTimeline to take activities directly (deriving both the rank resolver and the ranges internally). Wide year-spanning timelines now show their full extent even when no milestone dates have been entered.
This commit is contained in:
@@ -48,3 +48,74 @@ export function buildStageRankAtDate(
|
||||
return last;
|
||||
};
|
||||
}
|
||||
|
||||
export interface StageRange {
|
||||
rank: number;
|
||||
/** ISO date this stage period begins (the stage-transition activity). */
|
||||
startDate: string;
|
||||
/**
|
||||
* ISO date the period ends — the next chronologically-later activity with
|
||||
* a higher stage rank. `null` means the period is open-ended (still in
|
||||
* effect today); the chart should extend it to the right edge.
|
||||
*/
|
||||
endDate: string | null;
|
||||
activityId: number;
|
||||
subject: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive per-activity stage ranges from the activity stream. Each
|
||||
* stage-transition activity (one with a non-empty `stage`) produces one
|
||||
* range: start = the activity's date, end = the date of the next
|
||||
* chronologically-following activity whose stage rank is *strictly higher*.
|
||||
* If no later activity has a higher rank, the range is open-ended.
|
||||
*
|
||||
* Multiple activities at the same stage each yield their own range; they'll
|
||||
* share the same end date if they sit between the same two higher-rank
|
||||
* boundaries.
|
||||
*/
|
||||
export function computeStageRanges(activities: ActivitySummary[]): StageRange[] {
|
||||
type Item = {
|
||||
t: number;
|
||||
rank: number;
|
||||
activityId: number;
|
||||
isoDate: string;
|
||||
subject: string | null;
|
||||
};
|
||||
const items: Item[] = [];
|
||||
for (const a of activities) {
|
||||
if (typeof a.stage !== "string" || a.stage.length === 0) continue;
|
||||
const r = STAGE_RANK[a.stage];
|
||||
if (typeof r !== "number" || r < 1 || r > 5) continue;
|
||||
const t = new Date(a.date).getTime();
|
||||
if (!Number.isFinite(t)) continue;
|
||||
items.push({
|
||||
t,
|
||||
rank: r,
|
||||
activityId: a.id,
|
||||
isoDate: a.date,
|
||||
subject: a.subject ?? null,
|
||||
});
|
||||
}
|
||||
items.sort((x, y) => x.t - y.t);
|
||||
|
||||
const ranges: StageRange[] = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const cur = items[i];
|
||||
let endIso: string | null = null;
|
||||
for (let j = i + 1; j < items.length; j++) {
|
||||
if (items[j].rank > cur.rank) {
|
||||
endIso = items[j].isoDate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ranges.push({
|
||||
rank: cur.rank,
|
||||
startDate: cur.isoDate,
|
||||
endDate: endIso,
|
||||
activityId: cur.activityId,
|
||||
subject: cur.subject,
|
||||
});
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user