Skip to content

Instantly share code, notes, and snippets.

@lqb2
Forked from alansmithy/README.md
Last active January 2, 2017 17:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lqb2/dda7b9813e976cfae54783904092af20 to your computer and use it in GitHub Desktop.
Save lqb2/dda7b9813e976cfae54783904092af20 to your computer and use it in GitHub Desktop.
Heatmap Calendar in d3js

###Calendar heatmap

A calendar heatmap using a simple csv file structure of date and value. These kinds of displays are good for highlighting patterns in time series data where there might be multiple time patterns present - i.e. daily/weekly/monthly/seasonal. A great implementation of this kind of graphic is the Wisconsin crash calendar.

This particular implementation produces just one svg element with a discrete group element for every year referenced in the data set.

date value
06/04/16 9
06/04/16 2
07/04/16 2
07/04/16 9
08/04/16 9
09/04/16 9
10/04/16 10
11/04/16 10
12/04/16 10
13/04/16 10
14/04/16 10
15/04/16 10
17/04/16 10
18/04/16 10
19/04/16 10
21/04/16 10
22/04/16 10
23/04/16 10
24/04/16 10
25/04/16 10
26/04/16 10
27/04/16 10
29/04/16 10
30/04/16 10
02/05/16 10
02/05/16 9
04/05/16 10
05/05/16 10
06/05/16 10
08/05/16 10
09/05/16 10
10/05/16 10
11/05/16 10
12/05/16 10
13/05/16 10
14/05/16 10
15/05/16 10
16/05/16 9
17/05/16 10
18/05/16 10
19/05/16 10
20/05/16 10
21/05/16 10
22/05/16 10
23/05/16 10
26/05/16 9
28/05/16 10
29/05/16 10
30/05/16 10
31/05/16 10
01/06/16 10
02/06/16 10
03/06/16 10
04/06/16 10
05/06/16 10
06/06/16 10
07/06/16 10
08/06/16 10
09/06/16 10
10/06/16 10
11/06/16 10
12/06/16 9
13/06/16 10
15/06/16 10
16/06/16 10
17/06/16 10
18/06/16 10
19/06/16 10
20/06/16 10
21/06/16 10
22/06/16 5
24/06/16 10
25/06/16 10
27/06/16 10
28/06/16 10
29/06/16 10
01/07/16 10
02/07/16 10
03/07/16 10
04/07/16 10
06/07/16 10
07/07/16 10
08/07/16 10
10/07/16 10
13/07/16 10
14/07/16 10
15/07/16 10
16/07/16 10
17/07/16 10
18/07/16 10
19/07/16 10
20/07/16 10
21/07/16 10
22/07/16 10
23/07/16 10
24/07/16 10
25/07/16 10
26/07/16 10
27/07/16 10
28/07/16 10
29/07/16 10
30/07/16 10
31/07/16 10
01/08/16 10
02/08/16 10
03/08/16 10
04/08/16 10
05/08/16 10
06/08/16 10
08/08/16 10
09/08/16 10
10/08/16 10
11/08/16 10
12/08/16 10
12/08/16 9
13/08/16 10
13/08/16 10
14/08/16 10
16/08/16 10
17/08/16 10
18/08/16 10
19/08/16 10
20/08/16 10
22/08/16 10
23/08/16 10
27/08/16 10
28/08/16 10
30/08/16 10
31/08/16 10
01/09/16 10
02/09/16 10
03/09/16 10
04/09/16 10
05/09/16 10
06/09/16 10
07/09/16 10
08/09/16 10
09/09/16 10
10/09/16 10
10/09/16 10
11/09/16 10
12/09/16 10
13/09/16 10
14/09/16 10
15/09/16 10
16/09/16 10
17/09/16 10
18/09/16 10
19/09/16 10
20/09/16 10
21/09/16 10
22/09/16 10
23/09/16 10
24/09/16 10
25/09/16 10
26/09/16 10
27/09/16 10
28/09/16 10
29/09/16 10
04/10/16 10
05/10/16 10
07/10/16 10
08/10/16 10
09/10/16 10
11/10/16 10
12/10/16 9
13/10/16 10
14/10/16 10
15/10/16 10
16/10/16 10
17/10/16 10
18/10/16 10
19/10/16 10
20/10/16 10
21/10/16 10
22/10/16 10
23/10/16 10
24/10/16 10
25/10/16 10
26/10/16 10
27/10/16 10
28/10/16 10
29/10/16 10
30/10/16 10
31/10/16 10
01/11/16 10
02/11/16 10
03/11/16 10
04/11/16 10
05/11/16 10
06/11/16 10
07/11/16 10
08/11/16 10
09/11/16 10
10/11/16 10
11/11/16 10
13/11/16 10
14/11/16 10
15/11/16 10
16/11/16 10
16/11/16 1
17/11/16 10
18/11/16 10
19/11/16 10
20/11/16 9
21/11/16 10
22/11/16 10
23/11/16 10
24/11/16 10
25/11/16 10
26/11/16 10
27/11/16 10
28/11/16 10
29/11/16 10
30/11/16 10
01/12/16 10
02/12/16 9
03/12/16 10
04/12/16 10
05/12/16 10
06/12/16 10
07/12/16 10
08/12/16 10
09/12/16 10
10/12/16 10
11/12/16 10
12/12/16 10
13/12/16 10
14/12/16 10
15/12/16 10
16/12/16 10
10/07/15 30
15/07/15 1
16/07/15 6
27/07/15 13
02/08/15 4
03/08/15 6
06/08/15 225
11/08/15 60
15/08/15 49
16/08/15 1
17/08/15 9
18/08/15 6
21/08/15 1
23/08/15 1
24/08/15 6
25/08/15 1
26/08/15 50
27/08/15 9
28/08/15 204
30/08/15 37
31/08/15 8
01/09/15 4
02/09/15 2
04/09/15 70
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<title>Data Calendar</title>
<style>
.month {
fill: none;
stroke: #000;
stroke-width: 2px;
}
.day {
fill: #fff;
stroke: #ccc;
}
text {
font-family:sans-serif;
font-size:1.5em;
}
.dayLabel {
fill:#aaa;
font-size:0.8em;
}
.monthLabel {
text-anchor:middle;
font-size:0.8em;
fill:#aaa;
}
.yearLabel {
fill:#aaa;
font-size:1.2em;
}
.key {font-size:0.5em;}
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>
<script>
var title="Migrant deaths in the Mediterranean";
var units=" dead or missing";
var breaks=[10,25,50,100];
var colours=["#ffffd4","#fed98e","#fe9929","#d95f0e","#993404"];
//general layout information
var cellSize = 17;
var xOffset=20;
var yOffset=60;
var calY=50;//offset of calendar in each group
var calX=25;
var width = 960;
var height = 163;
var parseDate = d3.time.format("%d/%m/%y").parse;
format = d3.time.format("%d-%m-%Y");
toolDate = d3.time.format("%d/%b/%y");
d3.csv("data.csv", function(error, data) {
//set up an array of all the dates in the data which we need to work out the range of the data
var dates = new Array();
var values = new Array();
//parse the data
data.forEach(function(d) {
dates.push(parseDate(d.date));
values.push(d.value);
d.date=parseDate(d.date);
d.value=d.value;
d.year=d.date.getFullYear();//extract the year from the data
});
var yearlyData = d3.nest()
.key(function(d){return d.year;})
.entries(data);
var svg = d3.select("body").append("svg")
.attr("width","90%")
.attr("viewBox","0 0 "+(xOffset+width)+" 540")
//title
svg.append("text")
.attr("x",xOffset)
.attr("y",20)
.text(title);
//create an SVG group for each year
var cals = svg.selectAll("g")
.data(yearlyData)
.enter()
.append("g")
.attr("id",function(d){
return d.key;
})
.attr("transform",function(d,i){
return "translate(0,"+(yOffset+(i*(height+calY)))+")";
})
var labels = cals.append("text")
.attr("class","yearLabel")
.attr("x",xOffset)
.attr("y",15)
.text(function(d){return d.key});
//create a daily rectangle for each year
var rects = cals.append("g")
.attr("id","alldays")
.selectAll(".day")
.data(function(d) { return d3.time.days(new Date(parseInt(d.key), 0, 1), new Date(parseInt(d.key) + 1, 0, 1)); })
.enter().append("rect")
.attr("id",function(d) {
return "_"+format(d);
//return toolDate(d.date)+":\n"+d.value+" dead or missing";
})
.attr("class", "day")
.attr("width", cellSize)
.attr("height", cellSize)
.attr("x", function(d) {
return xOffset+calX+(d3.time.weekOfYear(d) * cellSize);
})
.attr("y", function(d) { return calY+(d.getDay() * cellSize); })
.datum(format);
//create day labels
var days = ['Su','Mo','Tu','We','Th','Fr','Sa'];
var dayLabels=cals.append("g").attr("id","dayLabels")
days.forEach(function(d,i) {
dayLabels.append("text")
.attr("class","dayLabel")
.attr("x",xOffset)
.attr("y",function(d) { return calY+(i * cellSize); })
.attr("dy","0.9em")
.text(d);
})
//let's draw the data on
var dataRects = cals.append("g")
.attr("id","dataDays")
.selectAll(".dataday")
.data(function(d){
return d.values;
})
.enter()
.append("rect")
.attr("id",function(d) {
return format(d.date)+":"+d.value;
})
.attr("stroke","#ccc")
.attr("width",cellSize)
.attr("height",cellSize)
.attr("x", function(d){return xOffset+calX+(d3.time.weekOfYear(d.date) * cellSize);})
.attr("y", function(d) { return calY+(d.date.getDay() * cellSize); })
.attr("fill", function(d) {
if (d.value<breaks[0]) {
return colours[0];
}
for (i=0;i<breaks.length+1;i++){
if (d.value>=breaks[i]&&d.value<breaks[i+1]){
return colours[i];
}
}
if (d.value>breaks.length-1){
return colours[breaks.length]
}
})
//append a title element to give basic mouseover info
dataRects.append("title")
.text(function(d) { return toolDate(d.date)+":\n"+d.value+units; });
//add montly outlines for calendar
cals.append("g")
.attr("id","monthOutlines")
.selectAll(".month")
.data(function(d) {
return d3.time.months(new Date(parseInt(d.key), 0, 1),
new Date(parseInt(d.key) + 1, 0, 1));
})
.enter().append("path")
.attr("class", "month")
.attr("transform","translate("+(xOffset+calX)+","+calY+")")
.attr("d", monthPath);
//retreive the bounding boxes of the outlines
var BB = new Array();
var mp = document.getElementById("monthOutlines").childNodes;
for (var i=0;i<mp.length;i++){
BB.push(mp[i].getBBox());
}
var monthX = new Array();
BB.forEach(function(d,i){
boxCentre = d.width/2;
monthX.push(xOffset+calX+d.x+boxCentre);
})
//create centred month labels around the bounding box of each month path
//create day labels
var months = ['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'];
var monthLabels=cals.append("g").attr("id","monthLabels")
months.forEach(function(d,i) {
monthLabels.append("text")
.attr("class","monthLabel")
.attr("x",monthX[i])
.attr("y",calY/1.2)
.text(d);
})
//create key
var key = svg.append("g")
.attr("id","key")
.attr("class","key")
.attr("transform",function(d){
return "translate("+xOffset+","+(yOffset-(cellSize*1.5))+")";
});
key.selectAll("rect")
.data(colours)
.enter()
.append("rect")
.attr("width",cellSize)
.attr("height",cellSize)
.attr("x",function(d,i){
return i*130;
})
.attr("fill",function(d){
return d;
});
key.selectAll("text")
.data(colours)
.enter()
.append("text")
.attr("x",function(d,i){
return cellSize+5+(i*130);
})
.attr("y","1em")
.text(function(d,i){
if (i<colours.length-1){
return "up to "+breaks[i];
} else {
return "over "+breaks[i-1];
}
});
});//end data load
//pure Bostock - compute and return monthly path data for any year
function monthPath(t0) {
var t1 = new Date(t0.getFullYear(), t0.getMonth() + 1, 0),
d0 = t0.getDay(), w0 = d3.time.weekOfYear(t0),
d1 = t1.getDay(), w1 = d3.time.weekOfYear(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";
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment