block by armollica 66bd3a498c133205635abd9c8b545e6a

Vertical Calendar View

Full Screen

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

index.html

<!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>

Makefile

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

date-range-path.js

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;
}

make-dow-jones-tsv.R

#!/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')