Public
Edited
Mar 14, 2024
5 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
calendar = {
const context = DOM.context2d(canvasWidth, canvasHeight);
context.font = "300 10px 'Roboto Condensed'";
context.textAlign = "center";
context.textBaseline = "middle";
context.fillStyle = "white";
context.fillRect(0, 0, canvasWidth, canvasHeight);

// Outer rectangle
context.lineWidth = 2;
context.strokeStyle = "black";
context.strokeRect(margin.left, margin.top, calendarWidth, calendarHeight);

// Vertical month dividers and labels
context.fillStyle = "black";
context.lineWidth = 2;
months.forEach((month) => {
const x = xPosition(month.start) - cellWidth;
const y = margin.top;
context.beginPath();
context.moveTo(x, y);
context.lineTo(x, y + calendarHeight);
context.stroke();
const x2 = xPosition(month.end) + cellWidth;
context.fillText(month.name, (x + x2) / 2, margin.top - cellHeight / 2);
});

// Extra divider before the year label
const yearLabelPosition = xPosition(middleOfYear) + cellWidth;
context.lineWidth = 2;
context.beginPath();
context.moveTo(yearLabelPosition, margin.top);
context.lineTo(yearLabelPosition, margin.top + calendarHeight);
context.stroke();

// One row for each year
years.forEach((year) => {
const yearLabel = year[0].getFullYear();
const y = yPosition(yearLabel);
context.lineWidth = yearLabel % 10 === 1 ? 2 : 1;
context.beginPath();
context.moveTo(margin.left, y - cellHeight / 2);
context.lineTo(margin.left + calendarWidth, y - cellHeight / 2);
context.stroke();
context.fillText(
yearLabel,
yearLabelPosition + yearCellWidth / 2 - 0.5,
y + 1.5
);

year.forEach((day) => {
const x = xPosition(day);
if (useDots && day.getDay() === 0) {
// On Sundays, draw dot instead of number
context.beginPath();
context.arc(x, y, 2, 0, 2 * Math.PI, false);
context.fillStyle = "black";
context.fill();
} else {
// Day of month (with slight offsets to keep it in the middle)
context.fillStyle = "black";
context.fillText(day.getDate(), x - 0.5, y + 1.5);
}
});
});

return html`<div style="max-width: ${width}px; max-height: ${screen.height}px; overflow: scroll">${context.canvas}</div>`;
}
Insert cell
minYear = 1801
Insert cell
numYears = 100
Insert cell
endYear = startYear + numYears
Insert cell
years = d3.range(startYear, endYear).map((year) => {
const startDay = new Date(year, 0, 1);
const endDay = new Date(year + 1, 0, 1);
return d3.timeDay.range(startDay, endDay);
})
Insert cell
today = new Date()
Insert cell
daysInLeapYear = 366
Insert cell
cellWidth = 14
Insert cell
cellHeight = 28
Insert cell
yearCellWidth = 42
Insert cell
calendarWidth = daysInLeapYear * cellWidth +
months.length * cellWidth +
yearCellWidth
Insert cell
calendarHeight = numYears * cellHeight
Insert cell
canvasWidth = margin.left + margin.right + calendarWidth
Insert cell
canvasHeight = margin.top + margin.bottom + calendarHeight
Insert cell
leapYear = 2000 // used to get consistent day-of-month positions
Insert cell
middleOfYear = new Date(leapYear, 5, 30)
Insert cell
months = d3.timeMonth
.range(new Date(leapYear, 0, 1), new Date(leapYear + 1, 0, 1))
.map((d) => ({
start: d,
end: d3.timeDay.offset(d3.timeMonth.offset(d, 1), -1),
name: d3.timeFormat("%B")(d)
}))
Insert cell
margin = ({ top: 50, right: 30, bottom: 30, left: 30 })
Insert cell
// Horizontal position of this date if it was in a leap year, with extra padding at start and end of each month
xPosition = function (date) {
const day = new Date(date.getTime());
day.setFullYear(leapYear);
const dayOfYear = d3.timeDay.count(d3.timeYear(day), day);
const position =
margin.left + dayOfYear * cellWidth + (day.getMonth() + 1) * cellWidth;
return day <= middleOfYear ? position : position + yearCellWidth;
}
Insert cell
yPosition = function (year) {
return margin.top + (year - startYear) * cellHeight + cellHeight / 2;
}
Insert cell
html`
<style>
@import url('https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@300&display=swap');
</style>
`
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more