Files
UI/packages/skin/ui/src/heatmap.ts
T

47 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { h } from '@ui-arch/frame';
import type { HeatmapProps } from './types.js';
// 缺省:最近 12 周、格子 11px、周一为首行——与国内日历习惯一致
const WEEKS = 12;
const CELL = 11;
const DAY = 86_400_000;
const LEVELS = 4; // 非零分四档,加上「零」共五档
/*** 补零与日期键:热力图的键必须与外部数据同一格式(YYYY-MM-DD) ***/
const pad = (n: number): string => String(n).padStart(2, '0');
const keyOf = (d: Date): string => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
/*** 日历热力图:按周列 × 星期行铺格子,颜色深浅表示当天数值;不画坐标轴,靠 title 看具体日期与数量 ***/
export function heatmap(props: HeatmapProps): HTMLElement {
const weeks = props.weeks ?? WEEKS;
const cell = props.cellSize ?? CELL;
const counts = new Map<string, number>();
for (const item of props.values) counts.set(item.date, (counts.get(item.date) ?? 0) + item.count); // 同一天多条求和,不覆盖
const max = Math.max(1, ...counts.values()); // 分母:至少 1,避免全零时除零
// 末列对齐到「本周」:先取今天,再回退到本周末,保证最后一列是完整 7 行
const today = new Date();
today.setHours(0, 0, 0, 0);
const end = new Date(today.getTime() + (6 - ((today.getDay() + 6) % 7)) * DAY); // 周一起算的第 6 天(周日)
const start = new Date(end.getTime() - (weeks * 7 - 1) * DAY);
const grid = h('div', { class: 'ua-heat-grid', role: 'img', 'aria-label': props.ariaLabel ?? `最近 ${weeks} 周活跃度` });
grid.style.gridTemplateRows = `repeat(7, ${cell}px)`; // 七行对应周一到周日,行高与格子边长同源
for (let i = 0; i < weeks * 7; i += 1) {
const d = new Date(start.getTime() + i * DAY);
const n = counts.get(keyOf(d)) ?? 0;
const level = n === 0 ? 0 : Math.min(LEVELS, Math.ceil((n / max) * LEVELS)); // 归一成四档,最大那天必落在最高档
const box = h('span', { class: `ua-heat-cell is-${level}` });
box.style.width = `${cell}px`;
box.style.height = `${cell}px`;
box.title = props.formatValue ? props.formatValue(n, keyOf(d)) : `${keyOf(d)} · ${n}`; // 悬停看明细:热力图本身不标数字
grid.append(box);
}
const legend = h(
'div',
{ class: 'ua-heat-legend' },
h('span', { class: 'ua-heat-hint' }, props.legendText ?? '少'),
...[0, 1, 2, 3, 4].map((lv) => { const sw = h('span', { class: `ua-heat-cell is-${lv}` }); sw.style.width = `${cell}px`; sw.style.height = `${cell}px`; return sw; }), // 图例色块与网格同尺寸:两处各写一个尺寸会差出一号
h('span', { class: 'ua-heat-hint' }, '多'),
);
return h('div', { class: 'ua-heat' }, grid, legend);
}