Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@HarryStevens
Last active November 26, 2019 01:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save HarryStevens/647b1e2665de8ae7320c48cd0dc66422 to your computer and use it in GitHub Desktop.
Save HarryStevens/647b1e2665de8ae7320c48cd0dc66422 to your computer and use it in GitHub Desktop.
Calendar
license: gpl-3.0

Make a calendar with D3.js. To produce the calendar data, use makeMonth(month, year), where month is zero-indexed.

<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0;
}
.calendar {
font-family: "Helvetica Neue", sans-serif;
}
.calendar .month {
margin: 0px 20px;
display: inline-block;
width: calc(50% - 40px);
}
.calendar .month-name {
text-align: center;
font-weight: bold;
margin-bottom: 10px;
}
.calendar rect {
fill: none;
stroke: #eee;
}
.calendar text {
text-anchor: middle;
font-size: 14px;
}
.calendar .day.past text {
fill: #aaa;
}
.calendar .day.today rect {
fill: #222;
}
.calendar .day.today text {
fill: #fff;
}
.calendar .outline {
fill: none;
stroke: #888;
}
@media only screen and (max-width: 574px){
.calendar .month {
margin: 0px;
width: 100%;
}
}
</style>
</head>
<body>
<div class="calendar"></div>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://unpkg.com/arraygeous@0.0.6/build/arraygeous.min.js"></script>
<script>
const calendar = d3.select(".calendar");
const day = 86400000;
const now = new Date;
const month = now.getMonth();
const prevMonth = month === 0 ? 11 : month - 1;
const year = now.getFullYear();
const prevYear = month === 0 ? year - 1 : year;
let data = [];
makeMonth(prevMonth, prevYear);
makeMonth(month, year);
function makeMonth(month, year){
const monthDays = [];
let loopMonth = month;
let loopDay = 0;
let loopDate = new Date(year, loopMonth, loopDay);
let loopStartDay = loopDate.getDay();
while (loopMonth === month){
monthDays.push({date: loopDate, col: loopDate.getDay(), row: Math.floor((loopDate.getDate() + loopStartDay) / 7)});
loopDate = new Date(loopDate.getTime() + day);
loopMonth = loopDate.getMonth();
}
if (monthDays[0].date.getDate() > 1){
monthDays.shift();
}
if (monthDays[0].row > 0){
monthDays.forEach(d => {
--d.row;
return d;
});
}
data.push({month, days: monthDays});
}
const months = calendar.selectAll(".month")
.data(data)
.enter().append("div")
.attr("class", d => "month month-" + d.month);
months.append("div")
.attr("class", "month-name")
.text(d => getMonthName(d.month));
const svg = months.append("svg");
const g = svg.append("g");
const columns = d3.scaleBand()
.domain(d3.range(0, 7));
const rows = d3.scaleBand()
.domain(d3.range(0, 5));
const days = g.selectAll(".day")
.data(d => d.days)
.enter().append("g")
.attr("class", "day")
.classed("past", d => d.date.getTime() < now.getTime() - day)
.classed("today", d => d.date.getDate() === now.getDate() && d.date.getMonth() === month && d.date.getFullYear() === year);
const dayRects = days.append("rect");
const dayNums = days.append("text")
.attr("class", "num")
.text(d => d.date.getDate())
.attr("dy", 4.5);
const dayOfWeek = g.selectAll(".day-of-week")
.data(["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"])
.enter().append("text")
.attr("class", "day-of-week")
.attr("dy", -4)
.text(d => d);
const outlines = g.append("polygon")
.datum(d => data.filter(f => f.month === d.month)[0].days)
.attr("class", "outline");
redraw();
addEventListener("resize", redraw);
function redraw(){
const margin = {left: 1, right: 1, top: 16, bottom: 1};
const box = d3.select(".month").node().getBoundingClientRect();
const baseWidth = innerWidth <= 640 ? Math.min(innerWidth, box.width) : box.width;
const width = baseWidth - margin.left - margin.right;
const baseHeight = Math.max((baseWidth / 2), 250);
const height = baseHeight - margin.top - margin.bottom; // TODO: Figure this out w/r/t aspect ratio
svg
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
g
.attr("transform", "translate(" + [margin.left, margin.top] + ")");
columns
.range([0, width]);
rows
.range([0, height]);
data.forEach(datum => {
datum.days.forEach(d => {
d.x0 = columns(d.col);
d.x1 = d.x0 + columns.bandwidth();
d.y0 = rows(d.row);
d.y1 = d.y0 + rows.bandwidth();
d.v0 = [d.x0, d.y0];
return d;
});
return datum;
});
dayOfWeek
.attr("x", (d, i) => columns(i) + columns.bandwidth() / 2);
days
.attr("transform", d => `translate(${d.v0})`);
dayRects
.attr("width", columns.bandwidth())
.attr("height", rows.bandwidth());
dayNums
.attr("x", columns.bandwidth() / 2)
.attr("y", rows.bandwidth() / 2);
outlines
.attr("points", calcHull);
}
function getMonthName(n){
return ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"][n];
}
function calcHull(days){
const x0min = arr.min(days, d => d.x0),
x1max = arr.max(days, d => d.x1),
y0min = arr.min(days, d => d.y0),
y1max = arr.max(days, d => d.y1);
// Width of top row
const r0 = days.filter(f => f.row === 0),
r0x0min = arr.min(r0, d => d.x0),
r0x1max = arr.max(r0, d => d.x1);
// Width of bottom row
const r4 = days.filter(f => f.row === 4),
r4x1max = arr.max(r4, d => d.x1),
r4x0min = arr.min(r4, d => d.x0);
// The top
let points = [[r0x0min, y0min], [r0x1max, y0min]];
// The bottom right
if (r4x1max < x1max){
const r3y1 = days.filter(f => f.row === 3)[0].y1;
points.push([x1max, r3y1]);
points.push([r4x1max, r3y1]);
}
points.push([r4x1max, y1max]);
// The bottom left
points.push([r4x0min, y1max]);
// The top left
if (r0x0min > x0min){
const r1y0 = days.filter(f => f.row === 1)[0].y0;
points.push([x0min, r1y0]);
points.push([r0x0min, r1y0]);
}
return points;
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment