| 273 | * Each column is a week, rows are weekdays (Mon-Sun). |
| 274 | */ |
| 275 | export function printCalendarHeatmap( |
| 276 | data: CalendarDay[], |
| 277 | options?: { label?: string; title?: string }, |
| 278 | ) { |
| 279 | if (data.length === 0) return; |
| 280 | |
| 281 | // Build a value map |
| 282 | const valueMap = new Map<string, number>(); |
| 283 | let maxVal = 0; |
| 284 | for (const d of data) { |
| 285 | valueMap.set(d.day, d.value); |
| 286 | if (d.value > maxVal) maxVal = d.value; |
| 287 | } |
| 288 | |
| 289 | // Determine date range - pad to full weeks |
| 290 | const sorted = [...data].sort((a, b) => a.day.localeCompare(b.day)); |
| 291 | const firstDate = new Date(sorted[0].day); |
| 292 | const lastDate = new Date(sorted.at(-1).day); |
| 293 | |
| 294 | // Adjust to start on Monday |
| 295 | const startDay = firstDate.getDay(); // 0=Sun, 1=Mon, ... |
| 296 | const mondayOffset = startDay === 0 ? 6 : startDay - 1; |
| 297 | const start = new Date(firstDate); |
| 298 | start.setDate(start.getDate() - mondayOffset); |
| 299 | |
| 300 | // Adjust to end on Sunday |
| 301 | const endDay = lastDate.getDay(); |
| 302 | const sundayOffset = endDay === 0 ? 0 : 7 - endDay; |
| 303 | const end = new Date(lastDate); |
| 304 | end.setDate(end.getDate() + sundayOffset); |
| 305 | |
| 306 | // Build grid: 7 rows (Mon-Sun) x N weeks |
| 307 | const weeks: string[][] = []; |
| 308 | const current = new Date(start); |
| 309 | let weekCol: string[] = []; |
| 310 | |
| 311 | while (current <= end) { |
| 312 | const key = current.toISOString().slice(0, 10); |
| 313 | const val = valueMap.get(key) || 0; |
| 314 | |
| 315 | // Quantize to block level |
| 316 | let level: number; |
| 317 | if (val === 0) { |
| 318 | level = 0; |
| 319 | } else if (maxVal > 0) { |
| 320 | level = Math.ceil((val / maxVal) * 4); |
| 321 | if (level < 1) level = 1; |
| 322 | if (level > 4) level = 4; |
| 323 | } else { |
| 324 | level = 0; |
| 325 | } |
| 326 | |
| 327 | // Color the block |
| 328 | const block = HEATMAP_BLOCKS[level]; |
| 329 | const colored = level > 0 ? pc.green(block) : pc.dim(block); |
| 330 | |
| 331 | weekCol.push(colored); |
| 332 | |