a calendar visualization based off of block http://bl.ocks.org/mbostock/4063318
Added data generator, transitions and click event
a calendar visualization based off of block http://bl.ocks.org/mbostock/4063318
Added data generator, transitions and click event
body{ | |
font-family: Helvetica; | |
} | |
#calendar-title{ | |
display: block; | |
position: relative; | |
width: 70%; | |
margin-left: 15%; | |
text-align: center; | |
border-bottom: thin solid black; | |
margin-top: 10px; | |
margin-bottom: 30px; | |
padding-bottom: 10px; | |
font-size: 20px; | |
} | |
#calendar-wrapper{ | |
position: relative; | |
display: block; | |
height: 300px; | |
width:90%; | |
margin-left: 5%; | |
} | |
#graph-picklist-wrapper{ | |
text-align: center; | |
} | |
#calendar-click-info{ | |
position: absolute; | |
display: inline-block; | |
width: 66%; | |
text-align: center; | |
font-size: 18px; | |
bottom: 30px; | |
} | |
#calendar-legend{ | |
position: absolute; | |
display: inline-block; | |
min-width:200px; | |
text-align: center; | |
font-size: 15px; | |
left:73%; | |
bottom: 30px; | |
} | |
.month-path{ | |
fill: none; | |
stroke: black; | |
stroke-width: 5px; | |
} | |
.day { | |
stroke: black; | |
stroke-width:1px; | |
} | |
.month-label{ | |
stroke:white; | |
} | |
.legend-box{ | |
position: relative; | |
display: inline-block; | |
height: 20px; | |
width: 20px; | |
margin: 5px 5px -4px 5px; | |
} | |
#legend-box-white{ | |
background-color: #b5cde1; | |
border:thin solid black; | |
} | |
#legend-box-light{ | |
background-color: #588EBB; | |
border:thin solid black; | |
} | |
#legend-box-dark{ | |
background-color: #386890; | |
border:thin solid black; | |
} |
var today = new Date(); | |
var animDuration =1000, | |
min = 0, | |
max=100; | |
d3.selection.prototype.moveToFront = function() { | |
return this.each(function(){ | |
this.parentNode.appendChild(this); | |
}); | |
}; | |
d3.selection.prototype.moveToBack = function() { | |
return this.each(function() { | |
var firstChild = this.parentNode.firstChild; | |
if (firstChild) { | |
this.parentNode.insertBefore(this, firstChild); | |
} | |
}); | |
}; | |
function numberWithCommas(x) { | |
x = x.toString(); | |
var pattern = /(-?\d+)(\d{3})/; | |
while (pattern.test(x)) | |
x = x.replace(pattern, "$1,$2"); | |
return x; | |
} | |
//http://stackoverflow.com/questions/10638529/how-to-parse-a-date-in-format-yyyymmdd-in-javascript | |
function dateParser(str) { | |
var y = str.substr(0,4), | |
m = str.substr(4,2) - 1, | |
d = str.substr(6,2); | |
var D = new Date(y,m,d); | |
return (D.getFullYear() == y && D.getMonth() == m && D.getDate() == d) ? D : 'invalid date'; | |
} | |
//http://stackoverflow.com/questions/1144783/replacing-all-occurrences-of-a-string-in-javascript | |
String.prototype.replaceAll = function(search, replacement) { | |
var target = this; | |
return target.replace(new RegExp(search, 'g'), replacement); | |
}; | |
//http://stackoverflow.com/questions/1527803/generating-random-numbers-in-javascript-in-a-specific-range | |
function getRandomInt(min, max) { | |
return Math.floor(Math.random() * (max - min + 1)) + min; | |
} | |
function prettyDate(dateObj){ | |
var newObj = new Date(dateObj); | |
var day = newObj.getDate(); | |
var monthIndex = newObj.getMonth(); | |
var year = newObj.getFullYear(); | |
var monthNames = ["January", "February", "March","April", "May", "June", "July","August", "September", "October","November", "December"]; | |
return monthNames[monthIndex]+" "+day+", "+year; | |
} | |
function generateAttendeeData(min,max){ | |
var returnArray=[]; | |
var datePointer = new Date(); | |
//off by 1 error here, not sure why | |
datePointer = new Date(datePointer.setDate(datePointer.getDate()+1)); | |
while(parseInt(datePointer.getFullYear()) === parseInt(today.getFullYear())){ | |
//Weekend values are less | |
if(datePointer.getDay() === 0 || datePointer.getDay() === 1){ | |
returnArray.push({ | |
date:datePointer, | |
hours:getRandomInt(0,getRandomInt(min,max/5)) | |
}); | |
} | |
else{ | |
returnArray.push({ | |
date:datePointer, | |
hours:getRandomInt(min,max) | |
}); | |
} | |
datePointer = new Date(datePointer.setDate(datePointer.getDate()-1)); | |
} | |
return returnArray; | |
} | |
function clickedDay(d){ | |
d3.selectAll(".day") | |
.transition() | |
.duration(animDuration/5) | |
.style("fill",function(d){ | |
var boxData = $("#box-id-"+dateParser(d).toDateString().replaceAll(" ","")).data(); | |
var sel = d3.select(this); | |
sel.moveToBack(); | |
return boxData.fill; | |
}) | |
.style("opacity",function(d){ | |
var boxData = $("#box-id-"+dateParser(d).toDateString().replaceAll(" ","")).data(); | |
return boxData.opacity; | |
}); | |
var sel =d3.select(this); | |
sel.moveToFront(); | |
d3.select("#box-id-"+dateParser(d).toDateString().replaceAll(" ","")) | |
.transition() | |
.duration(animDuration/3) | |
.style("fill","#82B446") | |
.style("opacity","1"); | |
var boxData = $("#box-id-"+dateParser(d).toDateString().replaceAll(" ","")).data(); | |
if(boxData.value !== undefined){ | |
$("#calendar-click-info").text(prettyDate(boxData.date)+" has a value of "+boxData.value+" units"); | |
} | |
else{ | |
$("#calendar-click-info").text("No information present for "+prettyDate(boxData.date)); | |
} | |
} | |
function animateColorTransition(color,nestData){ | |
d3.selectAll(".day") | |
.transition() | |
.duration(animDuration) | |
.delay(function(d,i){return i*20;}) | |
.style("fill", function(d) { | |
return (color(nestData[dateParser(d).toDateString()])!=="#NaNNaNNaN")? | |
color(nestData[dateParser(d).toDateString()]) : | |
"#000000"; | |
}) | |
.attr("data-fill",function(d){ | |
return (color(nestData[dateParser(d).toDateString()])!=="#NaNNaNNaN")? | |
color(nestData[dateParser(d).toDateString()]) : | |
"#000000"; | |
}) | |
.style("opacity",function(d){ | |
return (color(nestData[dateParser(d).toDateString()])!=="#NaNNaNNaN")? | |
"1": | |
"0.25"; | |
}) | |
.attr("data-opacity",function(d){ | |
return (color(nestData[dateParser(d).toDateString()])!=="#NaNNaNNaN")? | |
"1": | |
"0.25"; | |
}); | |
} | |
function renderCalendar(){ | |
var width = $("#calendar-wrapper").width(), | |
height = $("#calendar-wrapper").height(), | |
cellSize = parseInt($(window).width()/62), | |
week_days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'] | |
month = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; | |
var day = d3.time.format("%w"), | |
week = d3.time.format("%U"), | |
percent = d3.format(".1%"), | |
format = d3.time.format("%Y%m%d"); | |
parseDate = d3.time.format("%Y%m%d").parse; | |
var color = d3.scale.linear() | |
.range(["#b5cde1", '#386890']) | |
.domain([min, max]) | |
var svg = d3.select("#calendar-wrapper").selectAll("svg") | |
.data(d3.range(parseInt(today.getFullYear()), parseInt(today.getFullYear())+1)) | |
.enter().append("svg") | |
.attr("width", width) | |
.attr("height", height) | |
.append("g") | |
.attr("id","total-wrapper") | |
.attr("transform", "translate(" + ((width - cellSize * 53) / 2) + "," + (30) + ")"); | |
for (var i=0; i<7; i++) | |
{ | |
svg.append("text") | |
.attr("transform", "translate(8," + cellSize*(i+1) + ")") | |
.style("text-anchor", "end") | |
.attr("dy", "-.25em") | |
.style("stroke","black") | |
.style("font-size","12px") | |
.style("font-weight","100") | |
.text(function(d) { return week_days[i]; }); | |
} | |
var legendWrapper = d3.select("#total-wrapper") | |
.append("g") | |
.attr("id","legend-wrapper") | |
.attr("transform","translate(80,-10)"); | |
var legend = legendWrapper.selectAll(".legend") | |
.data(month) | |
.enter().append("g") | |
.attr("class", "legend") | |
.attr("transform", function(d, i) { return "translate(" + (((cellSize*4.3)*i)+(width/100)) + ",0)"; }); | |
legend.append("text") | |
.attr("class","month-label") | |
.attr("id", function(d,i){ return month[i] }) | |
.style("text-anchor", "end") | |
.style("font-size","12px") | |
.style("font-weight","100") | |
.style("stroke","black") | |
.text(function(d,i){ return month[i] }); | |
var generatedData = generateAttendeeData(min,max); | |
var nestData = d3.nest() | |
.key(function(d) {return d.date.toDateString();}) | |
.rollup(function(d){return d[0].hours}) | |
.map(generatedData); | |
var rectWrapper = d3.select("#total-wrapper") | |
.append("g") | |
.attr("id","blocks-wrapper") | |
.attr("transform","translate(30,0)"); | |
var rect = rectWrapper.selectAll(".day") | |
.data(function(d) { return d3.time.days(new Date(d, 0, 1), new Date(d, month.length, 1)); }) | |
.enter() | |
.append("rect") | |
.attr("class", "day") | |
.attr("width", cellSize) | |
.attr("height", cellSize) | |
.attr("x", function(d) { return week(d) * cellSize; }) | |
.attr("y", function(d) { return day(d) * cellSize; }) | |
.style("fill","#000000") | |
.style("opacity","0.25") | |
.attr("id",function(d){return "box-id-"+d.toDateString().replaceAll(" ","");}) | |
.attr("data-value", function(d){return nestData[d.toDateString()];}) | |
.attr("data-date",function(d){return d;}) | |
.attr("data-fill","#000000") | |
.attr("data-opacity","0.25") | |
.datum(format) | |
.on("click", clickedDay); | |
animateColorTransition(color,nestData); | |
//Draws the month boundaries | |
rectWrapper.selectAll(".month") | |
.data(function(d) { return d3.time.months(new Date(d, 0, 1), new Date(d, month.length, 1)); }) | |
.enter().append("path") | |
.attr("class", "month-path") | |
.attr("id", function(d,i){ return month[i] }) | |
.attr("d", monthPath); | |
//Thank you Mike Bostock | |
function monthPath(t0) { | |
var t1 = new Date(t0.getFullYear(), t0.getMonth() + 1, 0), | |
d0 = +day(t0), w0 = +week(t0), | |
d1 = +day(t1), w1 = +week(t1); | |
return "M" + (w0 + 1) * cellSize + "," + d0 * cellSize | |
+ "H" + w0 * cellSize + "V" + 7 * cellSize | |
+ "H" + w1 * cellSize + "V" + (d1 + 1) * cellSize | |
+ "H" + (w1 + 1) * cellSize + "V" + 0 | |
+ "H" + (w0 + 1) * cellSize + "Z"; | |
} | |
} | |
$( document ).ready(function() { | |
renderCalendar(); | |
$("#calendar-title").text("Basic Interactive D3 Calendar - "+today.getFullYear()); | |
}); |
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>D3 Calendar</title> | |
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script> | |
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.0/jquery.min.js"></script> | |
<link rel="stylesheet" type="text/css" href="calendar.css"/> | |
</head> | |
<body> | |
<div id="calendar-title">Basic D3 Interactive Calendar</div> | |
<div id="calendar-wrapper"> | |
<div id="calendar-click-info">Click on a Sqare For Details</div> | |
<div id="calendar-legend">Less | |
<div class="legend-box" id="legend-box-white"></div> | |
<div class="legend-box" id="legend-box-light"></div> | |
<div class="legend-box" id="legend-box-dark"></div> More | |
</div> | |
</div> | |
</body> | |
<script type="text/javascript" src="calendar.js"></script> | |
</html> |