Vertical layout for a calendar heatmap.
The month boundaries and the enclosed highlighted area are created using the
dateRangePath()
function defined in the date-range-path.js
. You can draw a path around any date range within a year by providing it a beginning and end date or a beginning date and a day-based offset, like a week or a month.
Fork from Mike Bostock’s Calendar View
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: sans-serif;
}
.label--year {
font-size: 12px;
}
.day {
fill: #fff;
stroke: #ccc;
}
.month {
fill: none;
stroke: #000;
stroke-width: 2px;
}
.highlight {
fill: none;
stroke: #404040;
stroke-width: 2px;
pointer-events: none;
}
.highlight--underlying {
fill: #ccc;
fill-opacity: 0.1;
stroke: #fff;
stroke-width: 4px;
pointer-events: none;
}
.q0 { fill: #a50026; }
.q1 { fill: #d73027; }
.q2 { fill: #f46d43; }
.q3 { fill: #fdae61; }
.q4 { fill: #fee090; }
.q5 { fill: #ffffbf; }
.q6 { fill: #e0f3f8; }
.q7 { fill: #abd9e9; }
.q8 { fill: #74add1; }
.q9 { fill: #4575b4; }
.q10 { fill: #313695; }
</style>
</head>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="date-range-path.js"></script>
<script>
var cellSize = 14;
var width = 114;
var height = 800;
var formatPercent = d3.format('.1%');
var formatDate = d3.timeFormat('%Y-%m-%d');
var parseDate = d3.timeParse('%Y-%m-%d');
var quantize = d3.scaleQuantize()
.domain([-.05, .05])
.range(d3.range(11).map(function(d) { return 'q' + d; }));
function daysInYear(year) {
var t0 = new Date(year, 0, 1);
var t1 = new Date(year + 1, 0, 1);
return d3.timeDay
.every(1)
.range(t0, t1);
}
function monthsInYear(year) {
var t0 = new Date(year, 0, 1);
var t1 = new Date(year + 1, 0, 1);
return d3.timeMonth
.every(1)
.range(t0, t1);
}
function weekOfYear(date) {
return d3.timeWeek
.count(d3.timeYear(date), date);
}
function parseRow(d) {
return {
date: parseDate(d.date),
pct_change: parseFloat(d.pct_change)
};
}
function monthOffset(t0) {
return new Date(t0.getFullYear(), t0.getMonth() + 1, 0);
}
function weekOffset(t0) {
return new Date(t0.getFullYear(), t0.getDate() + 7, 0);
}
function ready(error, data) {
if (error) throw error;
var dataByDate = d3.map(data, function(d) { return formatDate(d.date); });
function getPctChange(date) {
var t = formatDate(date);
if (dataByDate.has(t)) return dataByDate.get(t).pct_change;
return null;
}
var yearExtent = d3.extent(data, function(d) { return d.date.getFullYear(); });
var startYear = yearExtent[0];
var endYear = yearExtent[1];
var yearRange = d3.range(startYear, endYear);
var tx = (width - cellSize * 7) - 1;
var ty = ((height - cellSize * 53) / 2);
var svg = d3.select('body').selectAll('svg')
.data(yearRange)
.enter().append('svg')
.attr('width', width)
.attr('height', height)
.attr('class', 'calendar')
.append('g')
.attr('transform', 'translate(' + tx + ',' + ty + ')');
svg.append('text')
.attr('class', 'label label--year')
.attr('transform', 'translate(' + cellSize * 3.5 + ', -6)')
.style('text-anchor', 'middle')
.text(function(year) { return year; });
function dayClass(date) {
var pct_change = getPctChange(date);
var quantile = pct_change !== null ? quantize(pct_change) : '';
return 'day ' + quantile;
}
var day = svg.append('g').attr('class', 'days')
.selectAll('.day').data(daysInYear)
.enter().append('rect')
.attr('class', dayClass)
.attr('width', cellSize)
.attr('height', cellSize)
.attr('x', function(date) { return date.getDay() * cellSize; })
.attr('y', function(date) { return weekOfYear(date) * cellSize; });
function titleText(date) {
var pct_change = getPctChange(date);
return formatDate(date) + ': ' + formatPercent(pct_change);
}
day.append('title').text(titleText);
// Separate months
var monthPath = dateRangePath()
.cellSize(cellSize)
.offset(monthOffset)
.orientation('vertical')
.closed(false);
var month = svg.append('g').attr('class', 'months')
.selectAll('.month').data(monthsInYear)
.enter().append('path')
.attr('class', 'month')
.attr('d', monthPath);
// Highlight time when there were the biggest declines
var highlightPath = dateRangePath()
.cellSize(cellSize)
.orientation('vertical')
.closed(true);
var t0 = new Date(2008, 8, 8);
var t1 = new Date(2008, 11, 19);
const highlight = svg.filter(function(d) { return d === 2008; }).append('g');
highlight.append('path')
.attr('class', 'highlight--underlying')
.attr('d', highlightPath(t0, t1));
highlight.append('path')
.attr('class', 'highlight')
.attr('d', highlightPath(t0, t1));
}
d3.tsv('dow-jones.tsv', parseRow, ready);
</script>
</body>
</html>
all: dow-jones.tsv
csv/DJIA.csv:
echo 'Download from https://fred.stlouisfed.org/series/DJIA/downloaddata'
dow-jones.tsv: csv/DJIA.csv
Rscript make-dow-jones-tsv.R
function dateRangePath() {
var orientation = 'horizontal';
var cellSize = 10;
var offset = null;
var closed = true;
function path(t0, t1) {
if (offset) t1 = offset(t0);
var d0 = t0.getDay();
var d1 = t1.getDay();
var w0 = weekOfYear(t0);
var w1 = weekOfYear(t1);
var context = d3.path();
if (orientation === 'horizontal') {
if (closed) {
context.moveTo((w0 + 1) * cellSize, d0 * cellSize);
context.lineTo(w0 * cellSize, d0 * cellSize);
context.lineTo(w0 * cellSize, 7 * cellSize);
context.lineTo(w1 * cellSize, 7 * cellSize);
context.lineTo(w1 * cellSize, (d1 + 1) * cellSize);
context.lineTo((w1 + 1) * cellSize, (d1 + 1) * cellSize);
context.lineTo((w1 + 1) * cellSize, 0);
context.lineTo((w0 + 1) * cellSize, 0);
context.closePath();
} else {
context.moveTo((w0 + 1) * cellSize, d0 * cellSize);
if (d0 === 0) context.moveTo(w0 * cellSize,d0 * cellSize);
else context.lineTo(w0 * cellSize,d0 * cellSize);
context.lineTo(w0 * cellSize, 7 * cellSize);
context.moveTo(w1 * cellSize, 7 * cellSize);
context.lineTo(w1 * cellSize, (d1 + 1) * cellSize);
if (d1 === 6) context.moveTo((w1 + 1) * cellSize, (d1 + 1) * cellSize);
else context.lineTo((w1 + 1) * cellSize, (d1 + 1) * cellSize);
context.lineTo((w1 + 1) * cellSize, 0);
context.moveTo((w0 + 1) * cellSize, 0);
context.lineTo((w0 + 1) * cellSize, d0 * cellSize);
}
} else if (orientation === 'vertical') {
if (closed) {
context.moveTo(d0 * cellSize, (w0 + 1) * cellSize);
context.lineTo(d0 * cellSize, w0 * cellSize);
context.lineTo(7 * cellSize, w0 * cellSize);
context.lineTo(7 * cellSize, w1 * cellSize);
context.lineTo((d1 + 1) * cellSize, w1 * cellSize);
context.lineTo((d1 + 1) * cellSize, (w1 + 1) * cellSize);
context.lineTo(0, (w1 + 1) * cellSize);
context.lineTo(0, (w0 + 1) * cellSize);
context.closePath();
} else {
context.moveTo(d0 * cellSize, (w0 + 1) * cellSize);
if (d0 === 0) context.moveTo(d0 * cellSize, w0 * cellSize);
else context.lineTo(d0 * cellSize, w0 * cellSize);
context.lineTo(7 * cellSize, w0 * cellSize);
context.moveTo(7 * cellSize, w1 * cellSize);
context.lineTo((d1 + 1) * cellSize, w1 * cellSize);
if (d1 === 6) context.moveTo((d1 + 1) * cellSize, (w1 + 1) * cellSize);
else context.lineTo((d1 + 1) * cellSize, (w1 + 1) * cellSize);
context.lineTo(0, (w1 + 1) * cellSize);
context.moveTo(0, (w0 + 1) * cellSize);
context.lineTo(d0 * cellSize, (w0 + 1) * cellSize);
}
}
return context.toString();
}
path.orientation = function(_) {
if (!arguments.length) return orientation;
if (['horizontal', 'vertical'].indexOf(_) === -1) {
throw new Error('orientation must be either "horizonal" or "vertical"');
}
orientation = _;
return path;
};
path.closed = function(_) {
if (!arguments.length) return closed;
closed = _;
return path;
};
path.cellSize = function(_) {
if (!arguments.length) return cellSize;
cellSize = _;
return path;
};
path.offset = function(_) {
if (!arguments.length) return offset;
if (typeof(_) === 'number') {
offset = function(t0) {
return new Date(t0.getFullYear(), t0.getMonth(), t0.getDay() + _);
};
} else if (typeof(_) === 'function') {
offset = _;
} else {
throw new Error('offset must be either a number (of days) or a function taking the starting date');
}
return path;
};
function weekOfYear(date) {
return d3.timeWeek
.count(d3.timeYear(date), date);
}
return path;
}
#!/usr/bin/env Rscript
library(tidyverse)
library(lubridate)
read_csv('csv/DJIA.csv', col_types = cols(VALUE = 'n')) %>%
mutate(date = ymd(DATE)) %>%
arrange(date) %>%
mutate(pct_change = (VALUE - lag(VALUE))/lag(VALUE)) %>%
filter(year(date) > 2007,
year(date) < 2017,
!is.na(pct_change)) %>%
select(date, pct_change) %>%
write_tsv('dow-jones.tsv')