block by micahstubbs a2f0fbb9b4da06b34b94fdffc2290327

Air Quality Calendars II

Full Screen

Based on Daniel Overstreet’s China Air Quality Canvas.

Official State Department color recommendations have been replaced with selected colors from ColorBrewer’s RdYlGn diverging scale. Data is not fully validated or verified.


a small fork from syntagmatic‘s bl.ock Air Quality Calendars to ensure that the .csv dataset can be loaded on on the blockbuilder.org example viewer.

here’s a snapshot of the conversation from the #d3js slack group where @chrisv suggests this solution

index.html

<!DOCTYPE html>
<meta charset="utf-8">
<style>
  body {
    overflow: hidden;
    margin: 24px 12px;
    font-size: 14px;
    font-family: "Futura", "Helvetica Neue", Helvetica, Arial, sans-serif;
  }
  #container {
    display: flex;
  }
  .section {
    display: flex;
    flex-direction: column;
    margin-right: 10px;
  }
  .title {
    width: 100%;
    margin: 12px auto;
    text-align: center;
    font-size: 18px;
  }
  .yearLabel {
    margin-right: 10px;
    color: #666;
  }
  .beijingYear, .chengduYear, .guangzhouYear, .shanghaiYear, .shenyangYear {
    display: flex;
  }
  #beijing .title {
    margin-left: 20px;
  }
  #key {
    display: flex;
    min-width: 745px;
    max-width: 850px;
    justify-content: space-between;
    margin: 32px;
  }
  .keyEntry span {

    margin: 0 0 0 26px;
    color: #666;
  }
  .keyEntry canvas {
    position: absolute;
    margin-top: -1px;
  }
</style>
<body>
<div id="container">
  <div id="beijing" class="section"></div>
  <div id="chengdu" class="section"></div>
  <div id="guangzhou" class="section"></div>
  <div id="shanghai" class="section"></div>
  <div id="shenyang" class="section"></div>
</div>
<div id="key"></div>
<script src="https://d3js.org/d3.v3.js"></script>
<script>

var cellSize = 3;
var canvasWidth = 160;
var canvasHeight = 34;

var format = d3.time.format("%x");
var color = d3.scale.linear()
  .domain([18, 45, 91, 141, 191, 291, Infinity])
  .range(["#1a9850", "#d9ef8b", "#fee08b", "#fc8d59", "#d73027", "#a50026", "#a50026"]);

var colorMap = d3.scale.ordinal()
  .range(["#1a9850", "#d9ef8b", "#fee08b", "#fc8d59", "#d73027", "#a50026"]);

var cities = ['beijing', 'chengdu', 'guangzhou', 'shanghai', 'shenyang'];
var data = {}; 
var remaining = 30; 
var datasets = ["beijing2008", "beijing2009", "beijing2010", "beijing2011", "beijing2012",
                "beijing2013", "beijing2014", "beijing2015", "beijing2016", "chengdu2012",
                "chengdu2013", "chengdu2014", "chengdu2015", "chengdu2016", "guangzhou2011",
                "guangzhou2012", "guangzhou2013", "guangzhou2014", "guangzhou2015", "guangzhou2016", "shanghai2011", "shanghai2012", "shanghai2013", "shanghai2014", "shanghai2015", "shanghai2016", "shenyang2013", "shenyang2014", "shenyang2015", "shenyang2016"];

(function loadData() {
  datasets.forEach(function(dataset) {
 d3.csv("https://gist.githubusercontent.com/dhoboy/499741034a2edb844ba3171aa73298d2/raw/b486c6fbd63747fccc7b492741fc0b775f855d16/" + dataset + ".csv", function(err, d) {
      if (!err) {
        data[dataset] = d;
        --remaining;
      }
      if (!remaining) draw();
    }); 
  }); 
})();

function draw() {
  // turn into array to filter out invalid readings
  data = d3.entries(data);
  
  data.forEach(function(dataset) {
    dataset.value = dataset.value.filter(function(d) { // only want valid readings
      return d["QC Name"] == "Valid" && d["Value"] != "-999";
    });
  });

  // turn back into object for daily averages
  data = data.reduce(function(prev, next) {
    prev[next.key] = next.value;
    return prev;
  }, {});
  
  var dailyAvg = {};
  cities.forEach(function(city) {
    d3.range(2008,2017).forEach(function(year) { 
      if (data[city + year]) {
        dailyAvg[city + year] = d3.nest()
          .key(function(d) {
            return format(new Date(d.Year, d.Month - 1, d.Day));
          })
          .rollup(function(hourlyReadings) {
            if (hourlyReadings.length != 24) {
              return "N/A";
            }
            return d3.sum(hourlyReadings, function(d) { return +d.Value; }) / 24;
          })
        .map(data[city + year])  
      } else {
        dailyAvg[city + year] = "No Data";
      }
    });
  });

  d3.select("div#beijing").append("div").attr("class", "title").text("beijing");

  var beijingDivs = d3.select("div#beijing").selectAll(".beijingYear")
    .data(d3.keys(dailyAvg).filter(function(d) { return /beijing/.test(d)}))
    .enter()
    .append("div")
    .attr("class", "beijingYear");
  
  beijingDivs.append("div") 
    .attr("class", "yearLabel")
    .text(function(d) { 
      return /\d{4}/.exec(d)[0]; 
    })

  beijingDivs.append("canvas")
    .attr("width", canvasWidth)
    .attr("height", canvasHeight)
    .attr("id", function(d) { return d; });
  
  d3.select("div#chengdu").append("div").attr("class", "title").text("chengdu");

  var chengduDivs = d3.select("div#chengdu").selectAll(".chengduYear")
    .data(d3.keys(dailyAvg).filter(function(d) { return /chengdu/.test(d)}))
    .enter()
    .append("div")
    .attr("class", "chengduYear")
    .append("canvas")
    .attr("width", canvasWidth)
    .attr("height", canvasHeight)
    .attr("id", function(d) { return d; });

  d3.select("div#guangzhou").append("div").attr("class", "title").text("guangzhou");
  
  var guangzhouDivs = d3.select("div#guangzhou").selectAll(".guangzhouYear")
    .data(d3.keys(dailyAvg).filter(function(d) { return /guangzhou/.test(d)}))
    .enter()
    .append("div")
    .attr("class", "guangzhouYear")
    .append("canvas")
    .attr("width", canvasWidth)
    .attr("height", canvasHeight)
    .attr("id", function(d) { return d; });

  d3.select("div#shanghai").append("div").attr("class", "title").attr("class", "title").text("shanghai");
  
  var shanghaiDivs = d3.select("div#shanghai").selectAll(".shanghaiYear")
    .data(d3.keys(dailyAvg).filter(function(d) { return /shanghai/.test(d)}))
    .enter()
    .append("div")
    .attr("class", "shanghaiYear")
    .append("canvas")
    .attr("width", canvasWidth)
    .attr("height", canvasHeight)
    .attr("id", function(d) { return d; });

  d3.select("div#shenyang").append("div").attr("class", "title").text("shenyang");
  
  var shenyangDivs = d3.select("div#shenyang").selectAll(".shenyangYear")
    .data(d3.keys(dailyAvg).filter(function(d) { return /shenyang/.test(d)}))
    .enter()
    .append("div")
    .attr("class", "shenyangYear")
    .append("canvas")
    .attr("width", canvasWidth)
    .attr("height", canvasHeight)
    .attr("id", function(d) { return d; });

  var categoryMap = { "good": "good", "moderate": "moderate", "sensitive": "unhealthy for sensitive groups", "unhealthy": "unhealthy", "very": "very unhealthy", "hazardous": "hazardous" };

  ["good", "moderate", "sensitive", "unhealthy", "very", "hazardous"].forEach(colorMap);

  var keyEntries = d3.select("#key").selectAll(".keyEntry")
    .data(["good", "moderate", "sensitive", "unhealthy", "very", "hazardous"])
    .enter()
    .append("div")
    .attr("class", "keyEntry");

  keyEntries.append("canvas")
    .attr("width", 20)
    .attr("height", 20)
    .attr("id", function(d) { return d; });

  keyEntries.append("span")
    .text(function(d) { return categoryMap[d]; });

  var keyCtx = {};
  
  d3.keys(categoryMap).forEach(function(k) {
    keyCtx[k] = document.getElementById(k).getContext("2d");
    drawKey(k, keyCtx[k]);
  });

  var ctx = {};
  d3.keys(dailyAvg).forEach(function(k) {
    ctx[k] = document.getElementById(k).getContext("2d");
  });
  
  cities.forEach(function(city) {
    d3.range(2008,2017).forEach(function(year) {
      var monthsInYear = d3.time.months((new Date(year, 0, 1)), new Date(year+1, 0, 1));  
      var daysInYear = d3.time.days((new Date(year, 0, 1)), new Date(year+1, 0, 1));  
      daysInYear.forEach(function(day) {
        drawDay((d3.time.weekOfYear(day)*cellSize), (day.getDay() * cellSize), city + year, format(day), ctx[city + year]);
      });
    });
  });

  function drawDay(x, y, key, day, ctx) { 
    if (dailyAvg[key] == "No Data") {
      ctx.fillStyle = "#ffffff";
    } else {
      console.log(dailyAvg[key][day]);
      if (+dailyAvg[key][day] > -1) {
        ctx.fillStyle = color(dailyAvg[key][day]);
      } else { 
        ctx.fillStyle = "#e6e6e6";
      }
    }
    ctx.fillRect(x,y,cellSize,cellSize);
  }

  function drawKey(key, ctx) {
    ctx.fillStyle = colorMap(key);
    ctx.fillRect(0,0,20,20);
  }
}
</script>