block by renecnielsen 00ece1121142dddee837

d3 | reusable heatmap calendar

Full Screen

Calendar heatmap adapted into a reusable chart for quick testing

loading of CSV json 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.

source

forked from eesur‘s block: d3 | reusable heatmap calendar

index.html

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


d3_code_heatmap_cal.js

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

    };


}());

main.css

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