Skip to content

Instantly share code, notes, and snippets.

@alansmithy
Last active March 15, 2019 15:53
Show Gist options
  • Select an option

  • Save alansmithy/6fd2625d3ba2b6c9ad48 to your computer and use it in GitHub Desktop.

Select an option

Save alansmithy/6fd2625d3ba2b6c9ad48 to your computer and use it in GitHub Desktop.

Revisions

  1. alansmithy revised this gist Sep 11, 2015. No changes.
  2. alansmithy created this gist Sep 11, 2015.
    5 changes: 5 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,5 @@
    ###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](http://www.coolinfographics.com/blog/2012/9/4/2011-wisconsin-crash-calendar-interview.html).

    This particular implementation produces just one svg element with a discrete group element for every year referenced in the data set.
    53 changes: 53 additions & 0 deletions data.csv
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,53 @@
    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
    262 changes: 262 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,262 @@
    <!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>
    Binary file added thumbnail.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.