Calendar heatmap adapted into a reusable chart for quick testing
loading of
CSVjson data, which is then quantized into a diverging color scale. The values are visualized as coloured cells per day. Days are arranged into columns by week, then grouped by month and years.
forked from eesur‘s block: d3 | reusable heatmap calendar
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>d3 | reusable heatmap calendar</title>
<meta name="author" content="Sundar Singh | eesur.com">
<link rel="stylesheet" href="main.css">
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.min.js" charset="utf-8"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js"></script>
</head>
<body>
<article>
<header>
<span id="info">Info</span>
</header>
<section id="heatmap"></section>
</article>
<script> d3.eesur = {}; //namespace </script>
<script src="d3_code_heatmap_cal.js"></script>
<script>
// *****************************************
// render chart
// *****************************************
(function() {
'use strict';
var nestedData;
var parseDate = d3.time.format('%Y-%m-%d').parse;
// create chart
var heatChart = d3.eesur.heatmap()
.colourRangeStart('#e1f4fd')
.colourRangeEnd('#00447c')
.height(800)
.startYear('2011')
.endYear('2016')
.on('_hover', function (d, i) {
var f = d3.time.format('%B %d %Y');
d3.select('#info')
.text(function () {
return f(d) + ': ' + nestedData[d];
});
});
// apply after nesting data
d3.json('heatmap_data.json', function(error, data) {
if (error) return console.warn(error);
nestedData = d3.nest()
.key(function (d) { return parseDate(d.date.split(' ')[0]); })
.rollup(function (n) {
return d3.sum(n, function (d) {
return d.amount; // key
});
})
.map(data);
// console.log(nestedData);
// render chart
d3.select('#heatmap')
.datum(nestedData)
.call(heatChart);
});
}());
d3.select(self.frameElement).style('height', '900px');
</script>
</body>
</html>
(function() {
'use strict';
// *****************************************
// reusable heat-map chart
// *****************************************
d3.eesur.heatmap = function module() {
// input vars for getter setters
var startYear = 2013,
endYear = 2016,
colourRangeStart = '#00aeef',
colourRangeEnd = '#d62728',
width = 950,
height = 475;
var dispatch = d3.dispatch('_hover');
function exports(_selection) {
_selection.each(function(nestedData) {
var colour = d3.scale.linear()
.range([colourRangeStart, colourRangeEnd]);
var margin = {top: 20, right: 30, bottom: 20, left: 20};
// update width and height to use margins for axis
width = width - margin.left - margin.right;
height = height - margin.top - margin.bottom;
var years = d3.range(startYear, endYear).reverse(),
sizeByYear = height/years.length+1,
sizeByDay = d3.min([sizeByYear/8,width/54]),
day = function(d) { return (d.getDay() + 6) % 7; },
week = d3.time.format('%W'),
date = d3.time.format('%b %d');
var svg = d3.select(this)
.append('svg')
.attr({
class: 'chart',
width: width + margin.left + margin.right,
height: height + margin.top + margin.bottom
})
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
var year = svg.selectAll('.year')
.data(years)
.enter().append('g')
.attr('class', 'year')
.attr('transform', function(d,i) { return 'translate(30,' + i * sizeByYear + ')'; });
year.append('text')
.attr({
class: 'year-title',
transform: 'translate(-38,' + sizeByDay * 3.5 + ')rotate(-90)',
'text-anchor': 'middle',
'font-weight': 'bold'
})
.text(function(d) { return d; });
var rect = year.selectAll('.day')
.data(function (d) {
return (d === moment().year()) ? d3.time.days(new Date(d, 0, 1), new Date(d , moment().month(), moment().date())) : d3.time.days(new Date(d, 0, 1), new Date(d + 1, 0, 1));
})
.enter().append('rect')
.attr({
class: 'day',
width: sizeByDay,
height: sizeByDay,
x: function(d) { return week(d) * sizeByDay; },
y: function(d) { return day(d) * sizeByDay; }
});
year.selectAll('.month')
.data(function (d) {
return d3.time.months(new Date(d, 0, 1), new Date(d + 1, 0, 1));
})
.enter().append('path')
.attr({
class: 'month',
d: monthPath
});
// day and week titles
var chartTitles = (function() {
var weekDays = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'],
month = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
var titlesDays = svg.selectAll('.year')
.selectAll('.titles-day')
.data(weekDays)
.enter().append('g')
.attr('class', 'titles-day')
.attr('transform', function (d, i) {
return 'translate(-5,' + sizeByDay*(i+1) + ')';
});
titlesDays.append('text')
.attr('class', function (d,i) { return weekDays[i]; })
.style('text-anchor', 'end')
.attr('dy', '-.25em')
.text(function (d, i) { return weekDays[i]; });
var titlesMonth = svg.selectAll('.year')
.selectAll('.titles-month')
.data(month)
.enter().append('g')
.attr('class', 'titles-month')
.attr('transform', function (d, i) {
return 'translate(' + (((i+1) * (width/12) )-30) + ',-5)';
});
titlesMonth.append('text')
.attr('class', function (d,i) { return month[i]; })
.style('text-anchor', 'end')
.text(function (d,i) { return month[i]; });
})();
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) * sizeByDay + ',' + d0 * sizeByDay + 'H' + w0 * sizeByDay + 'V' + 7 * sizeByDay + 'H' + w1 * sizeByDay + 'V' + (d1 + 1) * sizeByDay + 'H' + (w1 + 1) * sizeByDay + 'V' + 0 + 'H' + (w0 + 1) * sizeByDay + 'Z';
}
// apply the heatmap colours
colour.domain(d3.extent(d3.values(nestedData)));
rect.filter(function (d) {
return d in nestedData;
})
.style('fill', function (d) {
return colour(nestedData[d]);
})
.on('mouseover', dispatch._hover);
});
}
// overrides getter/setters
exports.startYear = function(value) {
if (!arguments.length) return startYear;
startYear = value;
return this;
};
exports.endYear = function(value) {
if (!arguments.length) return endYear;
endYear = value;
return this;
};
exports.colourRangeStart = function(value) {
if (!arguments.length) return colourRangeStart;
colourRangeStart = value;
return this;
};
exports.colourRangeEnd = function(value) {
if (!arguments.length) return colourRangeEnd;
colourRangeEnd = value;
return this;
};
exports.width = function(value) {
if (!arguments.length) return width;
width = value;
return this;
};
exports.height = function(value) {
if (!arguments.length) return height;
height = value;
return this;
};
d3.rebind(exports, dispatch, 'on');
return exports;
};
}());
@import url(http://fonts.googleapis.com/css?family=Oswald:300,400,700);
body {
font-family: Oswald, Consolas, monaco, monospace;
line-height: 1.5;
font-weight: 300;
}
.day {
fill: #fff;
stroke: white;
stroke-width: .2px;
}
.month {
fill: none;
stroke: white;
stroke-width: .5px;
}
.year-title {
font-size: 12px;
letter-spacing: 10px;
fill: #00aeef;
}
#info {
position: absolute;
top: 10px;
left: 40px;
font-size: 13px;
}
header {
position: fixed;
left: 0;
top: 0;
height: 33px;
width: 100%;
background: #ffff00;
opacity: 0.9;
z-index: 22;
background: #00aeef;
color: white;
}
#heatmap {
padding-top: 30px;
}
svg text {
font-size: 11px;
text-transform: uppercase;
fill: #00aeef;
}