function Calendar(
data,
{
x = ([x]) => x,
y = ([, y]) => y,
title,
width = 400,
cellSize = Math.min(17, 400 / 7),
weekday = "monday",
formatDay = (i) => "SMTWTFS"[i],
formatMonth = "%b",
yFormat,
colors = d3.interpolatePiYG
} = {}
) {
const X = d3.map(data, x);
const Y = d3.map(data, y);
const I = d3.range(X.length);
const countDay = weekday === "sunday" ? (i) => i : (i) => (i + 6) % 7;
const timeWeek = weekday === "sunday" ? d3.utcSunday : d3.utcMonday;
const weekDays = weekday === "weekday" ? 5 : 7;
const height =
cellSize *
timeWeek.count(d3.utcYear(new Date("2023-01-01")), new Date("2023-12-30"));
const max = d3.quantile(Y, 0.9975, Math.abs);
const color = d3.scaleSequential([-max, +max], colors).unknown("none");
// Construct formats.
formatMonth = d3.utcFormat(formatMonth);
// Compute titles.
if (title === undefined) {
const formatDate = d3.utcFormat("%B %-d, %Y");
const formatValue = color.tickFormat(100, yFormat);
title = (i) => `${formatDate(X[i])}\n${formatValue(Y[i])}`;
} else if (title !== null) {
const T = d3.map(data, title);
title = (i) => T[i];
}
// Group the index by year, in reverse input order. (Assuming that the input is
// chronological, this will show years in reverse chronological order.)
const years = d3.groups(I, (i) => X[i].getUTCFullYear()).reverse();
function pathMonth(t) {
// t is the last day of the month
// count day returns 所处的一周内的天数
// Math.min保证了哪怕 countDay超过7,也能cap at 7
// Math.max保证了哪怕 小于0,也能cap at 0
const d = Math.max(0, Math.min(weekDays, countDay(t.getUTCDay())));
// 从d3.utcYear(t)到t,一共几周
const w = timeWeek.count(d3.utcYear(t), t);
// console.log(
// "相当于Y值:所处于的周,的第几天,可能从周日开始算,可能从周一开始算。取决于开始的选项",
// d
// );
// console.log("相当于X值:", w);
return `${
d === 0
? // 周一到了,从0开始,y= 0,x=已经过了几周
`M0,${w * cellSize}`
: d === weekDays
? // 到了一周的最后一天(7),则从下一周的0开始
`M0,${(w + 1) * cellSize}`
: // +1 是因为要画在底部
`M0,${(w + 1) * cellSize}H${d * cellSize}V${w * cellSize}`
}H${weekDays * cellSize}`;
}
const svg = d3
.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;")
.attr("font-family", "sans-serif")
.attr("font-size", 10);
const year = svg
.selectAll("g")
.data(years)
.join("g")
.attr("transform", (d, i) => `translate(40.5,${cellSize * 1.5})`);
year
.append("g")
.attr("text-anchor", "end")
.selectAll("text")
.data(weekday === "weekday" ? d3.range(1, 6) : d3.range(7))
.join("text")
.attr("y", -5)
.attr("x", (i) => countDay(i) * cellSize + cellSize * n)
.attr("dy", "0.31em")
.attr("dx", "0.1em")
.text(formatDay);
const cell = year
.append("g")
.selectAll("rect")
.data(
([, I]) => {
// console.log(I);
return I;
} // pass the data as it is
)
.join("rect")
.attr("width", cellSize - 3)
.attr("height", cellSize - 3)
// 和pathMonth一样
// countDay直接告诉你今天是一周内的第几天
.attr("x", (i) => {
if (i===0) {
console.log(X[i])
console.log(new Date("2023-01-01").getUTCDay())
console.log(d3.utcDay(new Date("2023-01-01")))
}
return countDay(X[i].getUTCDay()) * cellSize + 0.5
})
// 今年第一天:d3.utcYear(X[i])
// 当天X[i]
// 算出隔了几周,每有一周,就另起一行
.attr("y", (i) => timeWeek.count(d3.utcYear(X[i]), X[i]) * cellSize)
.attr("fill", (i) => color(Y[i]));
if (title) cell.append("title").text(title);
const month = year
.append("g")
.selectAll("g")
.data(([, I]) => {
return d3.utcMonths(d3.utcMonth(X[I[0]]), X[I[I.length - 1]]);
})
.join("g");
month
.filter((d, i) => {
// console.log(i);
return i;
})
.append("path")
.attr("fill", "none")
.attr("stroke", "red")
.attr("stroke-width", 3)
.attr("d", (d) => {
// console.log(d);
return pathMonth(d);
});
return Object.assign(svg.node(), { scales: { color } });
}