Skip to content

Instantly share code, notes, and snippets.

@alansmithy
Last active March 15, 2019 15:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save alansmithy/6fd2625d3ba2b6c9ad48 to your computer and use it in GitHub Desktop.
Save alansmithy/6fd2625d3ba2b6c9ad48 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
16/01/15 45
18/01/15 1
22/01/15 21
30/01/15 10
07/02/15 9
08/02/15 29
09/02/15 317
03/03/15 10
08/03/15 4
17/03/15 6
19/03/15 1
06/04/15 11
10/04/15 2
13/04/15 400
16/04/15 53
17/04/15 1
19/04/15 794
20/04/15 3
02/05/15 3
21/05/15 1
23/05/15 5
25/05/15 5
30/05/15 25
17/06/15 3
23/06/15 7
06/07/15 8
07/07/15 18
09/07/15 12
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>
@NiksJad
Copy link

NiksJad commented Jun 22, 2017

it showing wrong color code, I have added new script code

<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.valuebreaks.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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment