Make a calendar with D3.js. To produce the calendar data, use makeMonth(month, year)
, where month
is zero-indexed.
Last active
November 26, 2019 01:20
-
-
Save HarryStevens/647b1e2665de8ae7320c48cd0dc66422 to your computer and use it in GitHub Desktop.
Calendar
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: gpl-3.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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/[email protected]/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