|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
|
|
body { |
|
font: 10px sans-serif; |
|
shape-rendering: crispEdges; |
|
width: 1375px; |
|
} |
|
|
|
.day { |
|
fill: #f0f0f0; |
|
stroke: #ccc; |
|
} |
|
|
|
.month { |
|
fill: none; |
|
stroke: #000; |
|
stroke-width: 2px; |
|
} |
|
|
|
.yearLabel { |
|
font-size: 18px; |
|
} |
|
|
|
.tooltip { |
|
width: 350px; |
|
background-color: #f7f7f7; |
|
padding: 3px 12px; |
|
font-family: sans-serif; |
|
border: 1px solid #bbbbbb; |
|
box-shadow: 1px 1px 4px #bbbbbb; |
|
} |
|
|
|
.info { |
|
font-size: 12px; |
|
} |
|
|
|
.axis path, |
|
.axis line { |
|
fill: none; |
|
stroke: #000; |
|
shape-rendering: crispEdges; |
|
} |
|
|
|
.line { |
|
fill: none; |
|
stroke: #a6cee3; |
|
stroke-width: 2px; |
|
stroke-linecap: round; |
|
} |
|
|
|
h5 { |
|
font-size: 18px; |
|
margin-left: 10px; |
|
} |
|
</style> |
|
<body> |
|
<h5>Concentration of Particluate Matter less than 2.5 micrometers in diameter</h5> |
|
<script src="//d3js.org/d3.v3.min.js"></script> |
|
<script> |
|
/* namespacing */ |
|
var m = { l: {top: 10, right: 25, bottom: 35, left: 35}, c: 14 }; // margin |
|
var c = { width: 960, height: 136, cellSize: 17, format: d3.time.format("%x") }; // calendars |
|
var l = { width: 350 - m.l.left - m.l.right, height: 200 - m.l.top - m.l.bottom }; // linegraph |
|
|
|
var color = d3.scale.threshold() |
|
.domain([51, 101, 151, 201, 301]) |
|
.range(["#1a9850", "#fee08b", "#f46d43", "#d73027", "#a50026", "#67001f"]); |
|
|
|
var getYear = d3.time.format("%Y"); // getYear(new Date("datestring")) => 2008, etc |
|
var getMonth = d3.time.format("%m"); |
|
var getDay = d3.time.format("%d"); |
|
|
|
l.x = d3.scale.linear() // x is hours in the day |
|
.domain([0, 23]) |
|
.range([0, l.width]); |
|
|
|
l.y = d3.scale.linear() // y is value |
|
.domain([0, 1000]) |
|
.range([l.height, 0]); |
|
|
|
l.line = d3.svg.line() |
|
.x(function(d) { return l.x(d.hour) }) |
|
.y(function(d) { return l.y(d.value) }) |
|
.interpolate("linear"); |
|
|
|
l.xAxis = d3.svg.axis() |
|
.scale(l.x) |
|
.orient("bottom") |
|
.ticks(24); |
|
|
|
l.yAxis = d3.svg.axis() |
|
.scale(l.y) |
|
.orient("left") |
|
.ticks(10); |
|
|
|
var calendars = d3.select("body").selectAll("svg") |
|
.data(d3.range(2008, 2016)) |
|
.enter().append("svg") |
|
.attr("width", c.width) |
|
.attr("height", c.height + m.c) |
|
.append("g") |
|
.attr("transform", "translate(" + ((c.width - c.cellSize * 53) / 2) + "," + (c.height - c.cellSize * 7 - 1) + ")"); |
|
|
|
calendars.append("text") |
|
.attr("transform", "translate(-6," + c.cellSize * 3.5 + ")rotate(-90)") |
|
.attr("class", "yearLabel") |
|
.style("text-anchor", "middle") |
|
.text(function(d) { return d; }); |
|
|
|
var rect = calendars.selectAll(".day") |
|
.data(function(d) { return d3.time.days(new Date(d, 0, 1), new Date(d + 1, 0, 1)); }) |
|
.enter().append("rect") |
|
.attr("class", "day") |
|
.attr("width", c.cellSize) |
|
.attr("height", c.cellSize) |
|
.attr("x", function(d) { return d3.time.weekOfYear(d) * c.cellSize; }) |
|
.attr("y", function(d) { return d.getDay() * c.cellSize; }) |
|
.datum(c.format); |
|
|
|
calendars.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") |
|
.attr("d", monthPath); |
|
|
|
var tooltip = d3.select("body") |
|
.append("div") |
|
.attr("class", "tooltip") |
|
.style("position", "absolute") |
|
.style("z-index", "10") |
|
.style("visibility", "hidden"); |
|
|
|
var tooltipInfo = tooltip.append("pre") |
|
.attr("class", "info"); |
|
|
|
var tooltipGraph = tooltip.append("svg") |
|
.attr("width", l.width + m.l.right + m.l.left) |
|
.attr("height", l.height + m.l.top + m.l.bottom) |
|
.append("g") |
|
.attr("transform", "translate(" + m.l.left + "," + m.l.top + ")"); |
|
|
|
tooltipGraph.append("g") |
|
.attr("class", "x axis") |
|
.attr("transform", "translate(0," + l.height + ")") |
|
.call(l.xAxis) |
|
.append("text") |
|
.attr("transform", "translate(312, 32)") |
|
.style("text-anchor", "end") |
|
.text("Hour of the day"); |
|
|
|
tooltipGraph.append("g") |
|
.attr("class", "y axis") |
|
.call(l.yAxis) |
|
.append("text") |
|
.attr("transform", "rotate(-90)") |
|
.attr("y", 6) |
|
.attr("dy", ".71em") |
|
.style("text-anchor", "end") |
|
.text("PM2.5"); |
|
|
|
var data = {}, remaining = 8; // parallel loading csv code from here: https://groups.google.com/forum/#!msg/d3-js/3Y9VHkOOdCM/YnmOPopWUxQJ |
|
d3.csv("/d/4c8f05129838a63ec90930f8c46262f0/Beijing_2008_HourlyPM2.5_created20140325.csv", function(data_2008) { |
|
data.yr_2008 = data_2008; |
|
if (!--remaining) draw(); |
|
}) |
|
|
|
d3.csv("/d/4c8f05129838a63ec90930f8c46262f0/Beijing_2009_HourlyPM25_created20140709.csv", function(data_2009) { |
|
data.yr_2009 = data_2009; |
|
if (!--remaining) draw(); |
|
}) |
|
|
|
d3.csv("/d/4c8f05129838a63ec90930f8c46262f0/Beijing_2010_HourlyPM25_created20140709.csv", function(data_2010) { |
|
data.yr_2010 = data_2010; |
|
if (!--remaining) draw(); |
|
}) |
|
|
|
d3.csv("/d/4c8f05129838a63ec90930f8c46262f0/Beijing_2011_HourlyPM25_created20140709.csv", function(data_2011) { |
|
data.yr_2011 = data_2011; |
|
if (!--remaining) draw(); |
|
}) |
|
|
|
d3.csv("/d/4c8f05129838a63ec90930f8c46262f0/Beijing_2012_HourlyPM2.5_created20140325.csv", function(data_2012) { |
|
data.yr_2012 = data_2012; |
|
if (!--remaining) draw(); |
|
}) |
|
|
|
d3.csv("/d/4c8f05129838a63ec90930f8c46262f0/Beijing_2013_HourlyPM2.5_created20140325.csv", function(data_2013) { |
|
data.yr_2013 = data_2013; |
|
if (!--remaining) draw(); |
|
}) |
|
|
|
d3.csv("/d/4c8f05129838a63ec90930f8c46262f0/Beijing_2014_HourlyPM25_created20150203.csv", function(data_2014) { |
|
data.yr_2014 = data_2014; |
|
if (!--remaining) draw(); |
|
}) |
|
|
|
d3.csv("/d/4c8f05129838a63ec90930f8c46262f0/Beijing_2015_HourlyPM25_created20160201.csv", function(data_2015) { |
|
data.yr_2015 = data_2015; |
|
if (!--remaining) draw(); |
|
}) |
|
|
|
function draw() { |
|
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"; |
|
}); |
|
}); |
|
|
|
var flatData = d3.merge(data.map(function(d) { return d.value; })); |
|
|
|
var dailyAvg = d3.nest() |
|
.key(function(d) { |
|
return c.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(flatData); |
|
|
|
var key = { "#1a9850": "Good", "#fee08b": "Moderate", "#f46d43": "Unhealthy for sensitive groups", |
|
"#d73027": "Unhealthy", "#a50026": "Very Unhealthy", "#67001f": "Hazardous" }; |
|
|
|
var calBounds = { "cal2008": { top: 100 }, "cal2015": { top: 1165 } }; |
|
|
|
d3.keys(calBounds).forEach(function(cal) { |
|
calBounds[cal].bottom = calBounds[cal].top + 114; |
|
}); |
|
|
|
rect |
|
.style("fill", function(d) { // d here is the date sring for each day |
|
if (dailyAvg[d] && dailyAvg[d] !== "N/A") { |
|
return color(dailyAvg[d]); |
|
} |
|
}) |
|
.on("mouseover", function(d) { |
|
tooltipInfo.text("") |
|
var reading = d3.round(dailyAvg[d], 2) + " µg/cu PM2.5" + "\nAir Quality: " + key[color(dailyAvg[d])]; // valid 24 hour average |
|
|
|
if (dailyAvg[d] == "N/A") { // incomplete reading for the day, still can graph something on the hour by hour breakdown |
|
reading = "Data Incomplete\nAir Quality: Data Incomplete"; |
|
} |
|
|
|
if (!dailyAvg[d]) { |
|
reading = "No Data\nAir Quality: No Data"; |
|
} |
|
|
|
tooltipInfo.text( |
|
"Date: " + d + "\nReading: " + reading |
|
); |
|
|
|
var day = new Date(d); |
|
var yearKey = "yr_" + getYear(day); |
|
|
|
var dayData = data.filter(function(d) { |
|
return d.key == yearKey; |
|
}); |
|
|
|
dayData = dayData[0].value.filter(function(d) { |
|
var date = new Date(d["Date (LST)"].split(" ")[0]); |
|
return (getDay(date) == getDay(day)-1 && getMonth(date) == getMonth(day)); |
|
}).map(function(d) { |
|
return { hour: +d.Hour, value: +d.Value }; |
|
}); |
|
|
|
tooltip.select(".line").remove(); |
|
|
|
tooltipGraph.append("path") |
|
.datum(dayData) |
|
.attr("class", "line") |
|
.attr("d", l.line); |
|
|
|
return tooltip.style("visibility", "visible"); |
|
}) |
|
.on("mousemove", function() { |
|
var y = d3.event.pageY; |
|
var currentCal = ""; |
|
|
|
d3.keys(calBounds).forEach(function(cal) { |
|
if (y >= calBounds[cal].top && y <= calBounds[cal].bottom) { |
|
currentCal = cal; |
|
} |
|
}); |
|
|
|
var top = y - 150; |
|
if (currentCal == "cal2008") { |
|
top = 60; |
|
} |
|
if (currentCal == "cal2015") { |
|
top = 1005; |
|
} |
|
return tooltip.style("top", top + "px").style("left", d3.event.pageX + 40 + "px"); |
|
}) |
|
.on("mouseout", function() { |
|
return tooltip.style("visibility", "hidden") |
|
}) |
|
|
|
calendars.selectAll(".month").on("mouseover", function() { |
|
return tooltip.style("visibility", "visible") |
|
}) |
|
} |
|
|
|
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) * c.cellSize + "," + d0 * c.cellSize |
|
+ "H" + w0 * c.cellSize + "V" + 7 * c.cellSize |
|
+ "H" + w1 * c.cellSize + "V" + (d1 + 1) * c.cellSize |
|
+ "H" + (w1 + 1) * c.cellSize + "V" + 0 |
|
+ "H" + (w0 + 1) * c.cellSize + "Z"; |
|
} |
|
|
|
</script> |