function CalendarGrid(data, {
x = (d) => d,
xUnit = d3.utcDay,
xFormat = d3.utcFormat('%d %b'),
xScale = undefined,
xDomain = undefined,
y = (d) => undefined,
yDomain = undefined,
c = (d) => '#000',
cFormat = d3.format('.3s'),
cScale = undefined,
title = undefined,
cellSize = 20,
cellPadding = 0.1,
rx = 2,
width = 'auto',
height = 'auto',
margin = {top: 30, right: 10, bottom: 5, left: 60},
responsive = true,
} = {}) {
xDomain = xDomain || d3.extent(data.map(d => x(d)));
const xUnique = xUnit.range(xDomain[0], d3.timeDay.offset(xDomain[1], 1));
const yUnique = yDomain || [... new Set(data.map(d => y(d)))];
const contentSize = {
x: margin.left + margin.right + cellSize * (xUnique.length + 1),
y: margin.top + margin.bottom + cellSize * (yUnique.length + 1),
}
if (width === 'auto') width = contentSize.x;
if (height === 'auto') height = contentSize.y;
const viewBox = responsive ? contentSize : {x: width, y: height};
const svg = d3.create('svg').attr("viewBox", `0 0 ${viewBox.x} ${viewBox.y}`).attr('width', width).attr('height', height);
if (xScale === undefined) {
xScale = d3.scaleTime()
.range([margin.left, margin.left + cellSize * xUnique.length])
.domain(xDomain)
}
const yScale = d3.scaleBand()
.range([margin.top, margin.top + cellSize * yUnique.length])
.domain(yUnique)
.round(true)
.padding(cellPadding);
if (cScale === undefined) {
// Assume a symmetric diverging color scale
const cMax = d3.quantile(data.map(d => c(d)), 0.975, Math.abs);
cScale = d3.scaleDiverging().domain([-cMax, 0, cMax]).interpolator(d3.interpolatePiYG);
}
if (title === undefined) {
// Title = x, y, value
title = (d) => `${xFormat(x(d))}, ${y(d)}: ${cFormat(c(d))}`
}
// Draw our cells
const cellsGroup = svg.append('g').attr('id','cells');
cellsGroup.selectAll('g')
.data(data, d => `${x(d)}-${y(d)}`)
.join(
(enter) => {
const g = enter.append('g');
const cell = g.append('rect')
.attr('x', d => xScale(x(d)))
.attr('y', d => yScale(y(d)))
.attr('width', yScale.bandwidth())
.attr('height', yScale.bandwidth())
.attr('rx', rx)
.attr('fill', d=> cScale(c(d)))
cell.append('title').text(title)
return g;
}
)
// Draw the X axis labels.
const xLabelGroup = svg.append('g').attr('id','xLabels')
const xLabels =
xUnit === d3.utcDay ? d3.utcMonday.range(...xDomain) : // Mondays for daily data
xUnit === d3.utcWeek ? d3.utcMonth.range(...xDomain).map(d3.utcMonday) : // First monday of the month
xUnit === d3.utcMonday ? d3.utcMonth.range(...xDomain).map(d3.utcMonday) : // First monday of the month
xUnit === d3.utcMonth ? d3.utcYear.range(...xDomain) : // First monday of the year
xUnit === d3.utcYear ? d3.utcYear.range(...xDomain, 10) : // First monday of the decade
d3.utcYear.range(...xDomain);
xLabelGroup.selectAll('g').data(xLabels)
.join(
(enter) => {
const g = enter.append('g')
g.append('path').attr('d', d => d3.line()([
[xScale(d) + yScale.step() / 2, margin.top - 2],
[xScale(d) + yScale.step() / 2, margin.top - 10]]))
.attr('stroke','black')
;
g.append('text')
.text(d => xFormat(d))
.attr('x', d => xScale(d) + (yScale.step() / 2))
.attr('y', margin.top - 15)
.attr('alignment-baseline', 'bottom')
.attr('text-anchor', 'middle')
.attr('font-family', 'sans-serif')
.attr('font-size', '0.8rem');
return g;
}
)
const yLabelGroup = svg.append('g').attr('id','yLabels');
yLabelGroup.selectAll('text').data(yUnique).join(
(enter) => {
const t = enter.append('text')
.text(d => d)
.attr('x', margin.left - 5)
.attr('y', d => yScale(d) + (yScale.step() / 2))
.attr('alignment-baseline', 'middle')
.attr('text-anchor', 'end')
.attr('font-family', 'sans-serif')
.attr('font-size', '0.8rem');
return t;
}
)
return Object.assign(svg.node(), {scales: {x: xScale, y: yScale, c: cScale}});
}