chore(repo): initialize planarchy workspace
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
// ─── Date helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
export function addDays(date: Date, days: number): Date {
|
||||
const d = new Date(date);
|
||||
d.setDate(d.getDate() + days);
|
||||
return d;
|
||||
}
|
||||
|
||||
export function daysBetween(a: Date, b: Date): number {
|
||||
return Math.round((b.getTime() - a.getTime()) / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a date to a left-pixel offset within the visible timeline.
|
||||
* Accounts for weekend-skipping when showWeekends is false.
|
||||
*/
|
||||
export function dateToLeft(
|
||||
date: Date,
|
||||
viewStart: Date,
|
||||
viewEnd: Date,
|
||||
cellWidth: number,
|
||||
showWeekends: boolean,
|
||||
): number {
|
||||
const clamped = date < viewStart ? viewStart : date > viewEnd ? viewEnd : date;
|
||||
if (showWeekends) {
|
||||
return daysBetween(viewStart, clamped) * cellWidth;
|
||||
}
|
||||
let count = 0;
|
||||
const cur = new Date(viewStart);
|
||||
cur.setHours(0, 0, 0, 0);
|
||||
const target = new Date(clamped);
|
||||
target.setHours(0, 0, 0, 0);
|
||||
while (cur < target) {
|
||||
const dow = cur.getDay();
|
||||
if (dow !== 0 && dow !== 6) count++;
|
||||
cur.setDate(cur.getDate() + 1);
|
||||
}
|
||||
return count * cellWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the pixel width of a date range within the visible timeline.
|
||||
* Accounts for weekend-skipping when showWeekends is false.
|
||||
*/
|
||||
export function dateRangeToWidth(
|
||||
start: Date,
|
||||
end: Date,
|
||||
viewStart: Date,
|
||||
viewEnd: Date,
|
||||
cellWidth: number,
|
||||
showWeekends: boolean,
|
||||
): number {
|
||||
let count = 0;
|
||||
const cur = new Date(start < viewStart ? viewStart : start);
|
||||
cur.setHours(0, 0, 0, 0);
|
||||
const endC = new Date(end > viewEnd ? viewEnd : end);
|
||||
endC.setHours(0, 0, 0, 0);
|
||||
while (cur <= endC) {
|
||||
const dow = cur.getDay();
|
||||
if (showWeekends || (dow !== 0 && dow !== 6)) count++;
|
||||
cur.setDate(cur.getDate() + 1);
|
||||
}
|
||||
return count * cellWidth;
|
||||
}
|
||||
|
||||
// ─── O(1) position cache ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Pre-computes a pixel-offset lookup table for the entire visible date range.
|
||||
* Returns `toLeft` / `toWidth` with O(1) lookups instead of O(n) loops.
|
||||
* Use inside a `useMemo` that depends on viewStart / viewDays / cellWidth / showWeekends.
|
||||
*/
|
||||
export function createDatePositionCache(
|
||||
viewStart: Date,
|
||||
viewDays: number,
|
||||
cellWidth: number,
|
||||
showWeekends: boolean,
|
||||
): { toLeft: (date: Date) => number; toWidth: (start: Date, end: Date) => number } {
|
||||
// offsetMap: day-start-timestamp → pixel left rankMap: same → 0-based visible-day index
|
||||
const offsetMap = new Map<number, number>();
|
||||
const rankMap = new Map<number, number>();
|
||||
|
||||
let rank = 0;
|
||||
const cur = new Date(viewStart);
|
||||
cur.setHours(0, 0, 0, 0);
|
||||
const viewStartT = cur.getTime();
|
||||
|
||||
for (let i = 0; i < viewDays; i++) {
|
||||
const dow = cur.getDay();
|
||||
if (showWeekends || (dow !== 0 && dow !== 6)) {
|
||||
const t = cur.getTime();
|
||||
offsetMap.set(t, rank * cellWidth);
|
||||
rankMap.set(t, rank);
|
||||
rank++;
|
||||
}
|
||||
cur.setDate(cur.getDate() + 1);
|
||||
}
|
||||
|
||||
const totalWidth = rank * cellWidth;
|
||||
const viewEndT = cur.getTime(); // timestamp of the day AFTER the last visible day
|
||||
|
||||
function toLeft(date: Date): number {
|
||||
const d = new Date(date);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
const t = d.getTime();
|
||||
if (t <= viewStartT) return 0;
|
||||
if (t >= viewEndT) return totalWidth;
|
||||
const cached = offsetMap.get(t);
|
||||
if (cached !== undefined) return cached;
|
||||
// Weekend when showWeekends=false: return the *next* visible day's offset.
|
||||
// This matches the original dateToLeft which counts strictly-before business days,
|
||||
// so Saturday gets the same offset as the following Monday.
|
||||
const next = new Date(d);
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
next.setDate(next.getDate() + 1);
|
||||
if (next.getTime() >= viewEndT) return totalWidth;
|
||||
const v = offsetMap.get(next.getTime());
|
||||
if (v !== undefined) return v;
|
||||
}
|
||||
return totalWidth;
|
||||
}
|
||||
|
||||
function toWidth(start: Date, end: Date): number {
|
||||
const sNorm = new Date(start < viewStart ? viewStart : start);
|
||||
sNorm.setHours(0, 0, 0, 0);
|
||||
const eNorm = new Date(end);
|
||||
eNorm.setHours(0, 0, 0, 0);
|
||||
|
||||
// Rank of the first visible day at-or-after sNorm
|
||||
let sRank: number;
|
||||
const sT = sNorm.getTime();
|
||||
const rS = rankMap.get(sT);
|
||||
if (rS !== undefined) {
|
||||
sRank = rS;
|
||||
} else {
|
||||
sRank = rank; // default: past end → 0 width
|
||||
const next = new Date(sNorm);
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
next.setDate(next.getDate() + 1);
|
||||
if (next.getTime() >= viewEndT) break;
|
||||
const r = rankMap.get(next.getTime());
|
||||
if (r !== undefined) { sRank = r; break; }
|
||||
}
|
||||
}
|
||||
|
||||
// Rank of the last visible day at-or-before eNorm
|
||||
let eRank: number;
|
||||
const eT = eNorm.getTime();
|
||||
if (eT >= viewEndT) {
|
||||
eRank = rank - 1; // clamp to last visible day
|
||||
} else {
|
||||
const rE = rankMap.get(eT);
|
||||
if (rE !== undefined) {
|
||||
eRank = rE;
|
||||
} else {
|
||||
eRank = sRank - 1; // default: no visible day → 0 width
|
||||
const prev = new Date(eNorm);
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
prev.setDate(prev.getDate() - 1);
|
||||
if (prev.getTime() < viewStartT) break;
|
||||
const r = rankMap.get(prev.getTime());
|
||||
if (r !== undefined) { eRank = r; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (eRank < sRank) return 0;
|
||||
return (eRank - sRank + 1) * cellWidth;
|
||||
}
|
||||
|
||||
return { toLeft, toWidth };
|
||||
}
|
||||
|
||||
// ─── Sub-lane computation ──────────────────────────────────────────────────────
|
||||
|
||||
export interface SubLaneAlloc {
|
||||
id: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Greedy lane assignment for overlapping allocations.
|
||||
* Returns a map of allocationId → lane index (0-based).
|
||||
* Allocations are sorted by startDate then assigned to the first lane
|
||||
* that doesn't overlap with the previous occupant.
|
||||
*/
|
||||
export function computeSubLanes(allocs: SubLaneAlloc[]): Map<string, number> {
|
||||
const sorted = [...allocs].sort(
|
||||
(a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime(),
|
||||
);
|
||||
|
||||
// laneEnds[i] = end date of the last allocation placed in lane i
|
||||
const laneEnds: Date[] = [];
|
||||
const result = new Map<string, number>();
|
||||
|
||||
for (const alloc of sorted) {
|
||||
const start = new Date(alloc.startDate);
|
||||
const end = new Date(alloc.endDate);
|
||||
|
||||
let placed = false;
|
||||
for (let i = 0; i < laneEnds.length; i++) {
|
||||
const laneEnd = laneEnds[i]!;
|
||||
if (start > laneEnd) {
|
||||
// Lane is free
|
||||
laneEnds[i] = end;
|
||||
result.set(alloc.id, i);
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!placed) {
|
||||
result.set(alloc.id, laneEnds.length);
|
||||
laneEnds.push(end);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user