Skip to content

Instantly share code, notes, and snippets.

@dhoboy
Last active April 5, 2017 02:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dhoboy/08d39f3a37cbb15a24b536827facad23 to your computer and use it in GitHub Desktop.
Save dhoboy/08d39f3a37cbb15a24b536827facad23 to your computer and use it in GitHub Desktop.
China Air Quality Canvas 2
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
margin-left: 20px;
cursor: default;
}
#container {
display: flex;
}
.section {
display: flex;
flex-direction: column;
}
.tooltip {
width: 335px;
background-color: #f7f7f7;
padding: 5px 10px;
font-family: sans-serif;
border: 1px solid #bbbbbb;
box-shadow: 1px 1px 4px #bbbbbb;
}
.axis {
font-size: 10px;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.info {
margin: 5px 0 5px 0;
}
.line {
fill: none;
stroke: #a6cee3;
stroke-width: 2px;
stroke-linecap: round;
}
.title {
font-size: 21px;
font-weight: bold;
width: 100%;
margin: 10px auto;
text-align: center;
}
.yearLabel {
font-size: 16px;
margin: 6px 12px 0 0;
}
.beijingYear, .chengduYear, .guangzhouYear, .shanghaiYear, .shenyangYear {
display: flex;
margin-bottom: 10px;
margin-right: 10px;
}
#key {
display: flex;
justify-content: space-between;
padding-right: 7%;
}
.keyEntry {
flex-shrink: 0;
}
.keyEntry span {
font-size: 14px;
margin-left: 29px;
font-family: monospace;
}
.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="http://d3js.org/d3.v3.js"></script>
<script>
var cellSize = 4;
var canvasWidth = 212;
var canvasHeight = 28;
var coordsToDate = { "2008": {},
"2009": {},
"2010": {},
"2011": {},
"2012": {},
"2013": {},
"2014": {},
"2015": {},
"2016": {}
}; // maps canvas coordinates to date for mouse interaction
var m = { l: {top: 10, right: 25, bottom: 35, left: 35}, c: 14 }; // margin
var l = { width: 350 - m.l.left - m.l.right, height: 150 - m.l.top - m.l.bottom }; // linegraph
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 format = d3.time.format("%x");
var color = d3.scale.threshold()
.domain([51, 101, 151, 201, 301])
.range(["#1a9850", "#fee08b", "#f46d43", "#d73027", "#a50026", "#67001f"]);
var reverseColor = { "#1a9850": "Good", "#fee08b": "Moderate", "#f46d43": "Unhealthy for sensitive groups",
"#d73027": "Unhealthy", "#a50026": "Very Unhealthy", "#67001f": "Hazardous" };
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("http://dhoboy.github.io/china-air-files/" + 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("pre").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("pre")
.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; })
.on("mousemove", mousemove)
.on("mouseout", mouseout);
d3.select("div#chengdu").append("pre").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; })
.on("mousemove", mousemove)
.on("mouseout", mouseout);
d3.select("div#guangzhou").append("pre").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; })
.on("mousemove", mousemove)
.on("mouseout", mouseout);
d3.select("div#shanghai").append("pre").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; })
.on("mousemove", mousemove)
.on("mouseout", mouseout);
d3.select("div#shenyang").append("pre").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; })
.on("mousemove", mousemove)
.on("mouseout", mouseout);
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(300, 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");
function mousemove(key) {
var year = key.slice(-4);
var city = key.slice(0, key.length - 4);
city = city.charAt(0).toUpperCase() + city.slice(1);
var pos = d3.mouse(this);
var x = Math.round(pos[0]) + 1;
var y = Math.round(pos[1]) + 1;
var date = coordsToDate[year][x+","+y];
if (!date) {
return tooltip.style("visibility", "hidden");
}
tooltipInfo.text("");
var reading = d3.round(dailyAvg[key][date], 2) + " µg/cu PM2.5" + "\nAir Quality: " + reverseColor[color(dailyAvg[key][date])]; // valid 24 hour average
if (dailyAvg[key][date] == "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[key][date]) {
reading = "No Data\nAir Quality: No Data";
}
tooltipInfo.text(
"City: " + city + "\nDate: " + date + "\nReading: " + reading
);
var day = new Date(date);
if (!data[key]) {
return tooltip.style("visibility", "hidden");
}
var dayData = data[key].filter(function(d) {
var z = new Date(d["Date (LST)"].split(" ")[0]);
return day.getMonth() == z.getMonth() && day.getDate() == z.getDate();
}).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);
var tooltipY = d3.event.pageY - 150;
if (year == "2008" || year == "2009" || year == "2010") {
tooltipY = 60;
}
var tooltipX = d3.event.pageX + 40;
if (city == "Shanghai" || city == "Shenyang") {
tooltipX = tooltipX - 420;
}
return tooltip.style("top", tooltipY + "px")
.style("left", tooltipX + "px")
.style("visibility", "visible");
};
function mouseout() {
return tooltip.style("visibility", "hidden");
}
var categoryMap = { "good": "good", "moderate": "moderate", "sensitive": "unhealthy for sensitive groups", "unhealthy": "unhealthy", "very": "very unhealthy", "hazardous": "hazardous" };
var colorMap = { "good": "#1a9850", "moderate": "#fee08b", "sensitive": "#f46d43",
"unhealthy": "#d73027", "very": "#a50026", "hazardous": "#67001f"};
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, fill) {
// as you draw each day, record the pixel locations of that day per year
var year = key.slice(-4);
var x0 = x;
var x1 = x + 1;
var x2 = x + 2;
var x3 = x + 3;
var x4 = x + 4;
var y0 = y;
var y1 = y + 1;
var y2 = y + 2;
var y3 = y + 3;
var y4 = y + 4;
coordsToDate[year][x0+","+y0] = day;
coordsToDate[year][x0+","+y1] = day;
coordsToDate[year][x0+","+y2] = day;
coordsToDate[year][x0+","+y3] = day;
coordsToDate[year][x0+","+y4] = day;
coordsToDate[year][x1+","+y0] = day;
coordsToDate[year][x1+","+y1] = day;
coordsToDate[year][x1+","+y2] = day;
coordsToDate[year][x1+","+y3] = day;
coordsToDate[year][x1+","+y4] = day;
coordsToDate[year][x2+","+y0] = day;
coordsToDate[year][x2+","+y1] = day;
coordsToDate[year][x2+","+y2] = day;
coordsToDate[year][x2+","+y3] = day;
coordsToDate[year][x2+","+y4] = day;
coordsToDate[year][x3+","+y0] = day;
coordsToDate[year][x3+","+y1] = day;
coordsToDate[year][x3+","+y2] = day;
coordsToDate[year][x3+","+y3] = day;
coordsToDate[year][x3+","+y4] = day;
coordsToDate[year][x4+","+y0] = day;
coordsToDate[year][x4+","+y1] = day;
coordsToDate[year][x4+","+y2] = day;
coordsToDate[year][x4+","+y3] = day;
coordsToDate[year][x4+","+y4] = day;
if (dailyAvg[key] == "No Data") {
ctx.fillStyle = "#ffffff";
} else {
if (color(+dailyAvg[key][day])) {
ctx.fillStyle = color(dailyAvg[key][day]);
} else {
ctx.fillStyle = "#f0f0f0";
}
}
ctx.fillRect(x,y,cellSize,cellSize);
}
function drawKey(key, ctx) {
ctx.fillStyle = colorMap[key];
ctx.fillRect(0,0,20,20);
}
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment