Skip to content

Instantly share code, notes, and snippets.

@kaz-a
Last active May 29, 2019 21:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save kaz-a/1f3523453855630d5c1a0957c9af57e7 to your computer and use it in GitHub Desktop.
Save kaz-a/1f3523453855630d5c1a0957c9af57e7 to your computer and use it in GitHub Desktop.
PM2.5 dashboard

Real-time PM2.5 monitoring results using d3 and leaflet. Work in progress but viewable here

<!DOCTYPE html>
<html>
<head>
<title>NYCCAS Dashboard</title>
<meta charset="utf-8" http-equiv="X-UA-Compatible" content="IE=9" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://code.jquery.com/ui/1.8.10/themes/smoothness/jquery-ui.css" type="text/css">
<script type="text/javascript" src="https://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.10/jquery-ui.min.js"></script>
<link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Open+Sans:400,300' type='text/css'>
<!-- bootstrap -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" integrity="sha512-dTfge/zgoMYpP7QbHy4gWMEGsbsdZeCXz7irItjcC3sPUFtf0kuFbDz/ixG7ArTxmDjLXDmezHubeNikyKGVyQ==" crossorigin="anonymous">
<!-- fontawesome -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
<!-- leaflet -->
<link rel="stylesheet" type="text/css" href="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.css" />
<!-- my stylesheet -->
<link rel = "stylesheet" type="text/css" href="style.css">
</head>
<body>
<div class="container-fluid legend">
<h2>PM<sub>2.5</sub> in New York City</h2>
Fine particles (PM<sub>2.5</sub>) are tiny airborne solid and liquid particles less than 2.5 microns in diameter.
PM<sub>2.5</sub> is the most harmful urban air pollutant, small enough to penetrate deep into the lungs and enter the bloodstream, worsening lung and heart disease and leading to hospital admissions, premature deaths
and increasing risk of cancer.
<hr />
MAP LEGEND
<br /><br />
<p class="grey">Monitored at &nbsp; <i class="fa fa-circle"></i>&nbsp;Roof&nbsp;&nbsp;
<i class="fa fa-square"></i>&nbsp;Street</p>
<div class="symbols"></div>
</div>
<div class = "container-fluid site-specific">
<div class="buttons col-sm-12 col-md-12"></div>
<div id="map" class="col-xs-12 col-sm-12 col-md-6"></div>
<div class="content col-xs-12 col-sm-12 col-md-6">
<div id="chart"></div>
<div id="site" class="col-xs-12 media">
<div class="site col-xs-12 col-sm-6 col-md-6"></div>
<div class="site-photo media-right col-xs-12 col-sm-6 col-md-6" href="#"></div>
</div>
</div>
</div>
<div class="container-fluid message">
<div class="row">
<hr />
<h4>Preliminary Data</h4>
<p>Data displayed on this page is preliminary <br />and subject to change.
</p>
</div>
</div>
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0-alpha1/jquery.js"></script>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/queue-async/1.0.7/queue.min.js"></script>
<script src="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js" integrity="sha512-K1qjQ+NcF2TYO/eI3M6v8EiNYZfA95pQumfvcVrTHtwQVDG+aHRqLi/ETn2uB+1JqwYqVG3LIvdm9lj6imS/pQ==" crossorigin="anonymous"></script>
<script src="script.js"></script>
</html>
// set up leaflet
var map = L.map('map', {
zoomControl: false
}).setView([40.71, -74.00], 10);
new L.Control.Zoom({
position: 'topright'
}).addTo(map);
// initialize the SVG layer
map._initPathRoot();
// specify tile map service
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoiemFrc2Nsb3NldCIsImEiOiJjaWdzZGh5ZjMwMmN1dGhrbnN6ZjFtb2NjIn0.x6b7Ra4Jdtbv38M9_uM2vQ', {
attribution: 'Map data &copy; <a href="http://openstreetmap.org">OpenStreetMap</a>, Imagery © <a href="http://mapbox.com">Mapbox</a>',
maxZoom: 18,
id: 'zakscloset.o4bigbgi',
accessToken: 'pk.eyJ1IjoiemFrc2Nsb3NldCIsImEiOiJjaWdzZGh5ZjMwMmN1dGhrbnN6ZjFtb2NjIn0.x6b7Ra4Jdtbv38M9_uM2vQ'
}).addTo(map);
//Disable drag and zoom handlers.
//map.dragging.disable();
map.touchZoom.disable();
//map.doubleClickZoom.disable();
map.scrollWheelZoom.disable();
map.keyboard.disable();
// Disable tap handler, if present.
if (map.tap) map.tap.disable();
// draw map markers with d3
var svg = d3.select("#map").select("svg"),
g = svg.append("g");
// tooltip
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var data = d3.csv('https://raw.githubusercontent.com/kaz-a/nyccas_node/master/public/data/mock_data-rev.csv', ready); // fake data. remove asap
// var data = d3.json("http://a816-dohmeta.nyc.gov/MetadataLite/api/AirQuality?type=json", ready);
function ready(error, data) {
if (error) throw error;
console.log(data);
// Parse the date & time
var parseDate = d3.time.format("%m/%d/%Y %H:%M").parse;
// Add a LatLng object to each item in the dataset
data.forEach(function(d) {
d.LatLng = new L.LatLng(d.latitude, d.longitude);
//d.roll_pm25 = +d.roll_PM25;
d.startTime = d.startTime.replace(/(\b\d\b)/g,"0$1");
d.updateTime = d.updateTime.replace(/(t)/gi, " at ");
d.updateTime = d.updateTime.slice(0, -3);
d.updateTime = d.updateTime.replace(/(-)/g, "/");
d.updateTime = d.updateTime.substring(5);
d.siteID = +d.siteID;
d.roll_PM25 = (Math.round( d.roll_PM25 * 10 ) / 10);
d.current_PM25 = +d.current_PM25;
});
// create an array by starttime
var nestedByDate = d3.nest()
.key(function(d) {
return d.startTime;
})
.sortKeys(d3.descending)
.entries(data);
console.log("nestedByDate = ", nestedByDate);
var nestedBySite = d3.nest()
.key(function(d) {
return d.siteID;
})
.sortKeys(d3.ascending)
.entries(data);
console.log("nestedBySite = ", nestedBySite);
// get the latest date
var latest = nestedByDate[0];
console.log(latest);
latestDate = latest.key;
console.log("The last day/time of this dataset is: " + latestDate);
parsedLatestDate = parseDate(latestDate);
// for map legend
var circleColorMap = [
//good
{ "value": 4, "color": "#80ce6f", "condition": "GOOD" },
{ "value": 8, "color": "#5fb550" },
{ "value": 12, "color": "#3d9331" },
{ "value": "", "color": "fff" },
//moderate
{ "value": 19.9, "color": "#f4e39a", "condition": "MODERATE" },
{ "value": 27.8, "color": "#e5d06c" },
{ "value": 35.4, "color": "#c9b128" },
{ "value": "", "color": "fff" },
//usg
{ "value": 42.1, "color": "#e88f7e", "condition": "UNHEALTHY FOR SENSITIVE GROUP" },
{ "value": 48.8, "color": "#e27659" },
{ "value": 55.4, "color": "#dd653c" },
{ "value": "", "color": "fff" },
//unhealthy
{ "value": 87.1, "color": "#d6a1a1", "condition": "UNHEALTHY" },
{ "value": 118.8, "color": "#bf6b6b" },
{ "value": 150.4, "color": "#aa4d4d" },
{ "value": "", "color": "fff" },
//very unhealthy
{ "value": 183.8, "color": "#9a92cc", "condition": "VERY UNHEALTHY" },
{ "value": 217.2, "color": "#7f70b7" },
{ "value": 250.4, "color": "#654ea5" },
{ "value": "", "color": "fff" },
//hazardous
{ "value": 300, "color": "#9e7b8f", "condition": "HAZARDOUS" },
{ "value": 350, "color": "#7f5463" },
{ "value": 400, "color": "#6b2d3c" }
]
console.log("circleColorMap = ", circleColorMap);
// color scheme for rolling-24hr data
var colors = function(d){
//good
if ((d.roll_PM25 >= 0 ) && (d.roll_PM25 < circleColorMap[2].value)){
if ((d.roll_PM25 >= 0) && (d.roll_PM25 < circleColorMap[0].value)){
return circleColorMap[0].color;
} else if ((d.roll_PM25 >= (circleColorMap[0].value)) && (d.roll_PM25 < circleColorMap[1].value)){
return circleColorMap[1].color;
} else {
return circleColorMap[2].color;
};
//moderate
} else if ((d.roll_PM25 >= (circleColorMap[2].value)) && (d.roll_PM25 < circleColorMap[6].value)) {
if ((d.roll_PM25 >= (circleColorMap[2].value)) && (d.roll_PM25 < circleColorMap[4].value)){
return circleColorMap[4].color;
} else if ((d.roll_PM25 >= (circleColorMap[4].value)) && (d.roll_PM25 < circleColorMap[5].value)) {
return circleColorMap[5].color;
} else {
return circleColorMap[6].color;
};
//usg
} else if ((d.roll_PM25 >= (circleColorMap[6].value)) && (d.roll_PM25 < circleColorMap[10].value)) {
if ((d.roll_PM25 <= (circleColorMap[6].value)) && (d.roll_PM25 < circleColorMap[8].value)){
return circleColorMap[8].color;
} else if ((d.roll_PM25 <= (circleColorMap[8].value)) && (d.roll_PM25 < circleColorMap[9].value)) {
return circleColorMap[9].color;
} else {
return circleColorMap[10].color;
};
//unhealthy
} else if ((d.roll_PM25 >= (circleColorMap[10].value)) && (d.roll_PM25 < circleColorMap[14].value)) {
if((d.roll_PM25 >= (circleColorMap[10].value)) && (d.roll_PM25 < circleColorMap[12].value)){
return circleColorMap[12].color;
} else if ((d.roll_PM25 >= (circleColorMap[12].value)) && (d.roll_PM25 < circleColorMap[13].value)){
return circleColorMap[13].color;
} else {
return circleColorMap[14].color;
};
//very unhealthy
} else if ((d.roll_PM25 >= (circleColorMap[14].value)) && (d.roll_PM25 < circleColorMap[16].value)) {
if ((d.roll_PM25 >= (circleColorMap[14].value)) && (d.roll_PM25 < circleColorMap[16].value)) {
return circleColorMap[16].color;
} else if ((d.roll_PM25 >= (circleColorMap[16].value)) && (d.roll_PM25 < circleColorMap[17].value)) {
return circleColorMap[17].color;
} else {
return circleColorMap[18].color;
};
//hazardous
} else if (d.roll_PM25 >= (circleColorMap[18].value)) {
return circleColorMap[21].color;
} else {
return "grey";
};
};
// color scheme for current data
var currentColors = function(d){
//good
if ((d.current_PM25 >= 0 ) && (d.current_PM25 < circleColorMap[2].value)){
if ((d.current_PM25 >= 0) && (d.current_PM25 < circleColorMap[0].value)){
return circleColorMap[0].color;
} else if ((d.current_PM25 >= (circleColorMap[0].value)) && (d.current_PM25 < circleColorMap[1].value)){
return circleColorMap[1].color;
} else {
return circleColorMap[2].color;
};
//moderate
} else if ((d.current_PM25 >= (circleColorMap[2].value)) && (d.current_PM25 < circleColorMap[6].value)) {
if ((d.current_PM25 >= (circleColorMap[2].value)) && (d.current_PM25 < circleColorMap[4].value)){
return circleColorMap[4].color;
} else if ((d.current_PM25 >= (circleColorMap[4].value)) && (d.current_PM25 < circleColorMap[5].value)) {
return circleColorMap[5].color;
} else {
return circleColorMap[6].color;
};
//usg
} else if ((d.current_PM25 >= (circleColorMap[6].value)) && (d.current_PM25 < circleColorMap[10].value)) {
if ((d.current_PM25 <= (circleColorMap[6].value)) && (d.current_PM25 < circleColorMap[8].value)){
return circleColorMap[8].color;
} else if ((d.current_PM25 <= (circleColorMap[8].value)) && (d.current_PM25 < circleColorMap[9].value)) {
return circleColorMap[9].color;
} else {
return circleColorMap[10].color;
};
//unhealthy
} else if ((d.current_PM25 >= (circleColorMap[10].value)) && (d.current_PM25 < circleColorMap[14].value)) {
if((d.current_PM25 >= (circleColorMap[10].value)) && (d.current_PM25 < circleColorMap[12].value)){
return circleColorMap[12].color;
} else if ((d.current_PM25 >= (circleColorMap[12].value)) && (d.current_PM25 < circleColorMap[13].value)){
return circleColorMap[13].color;
} else {
return circleColorMap[14].color;
};
//very unhealthy
} else if ((d.current_PM25 >= (circleColorMap[14].value)) && (d.current_PM25 < circleColorMap[16].value)) {
if ((d.current_PM25 >= (circleColorMap[14].value)) && (d.current_PM25 < circleColorMap[16].value)) {
return circleColorMap[16].color;
} else if ((d.current_PM25 >= (circleColorMap[16].value)) && (d.current_PM25 < circleColorMap[17].value)) {
return circleColorMap[17].color;
} else {
return circleColorMap[18].color;
};
//hazardous
} else if (d.current_PM25 >= (circleColorMap[18].value)) {
return circleColorMap[21].color;
} else {
return "grey";
};
};
// buttons to switch between data
d3.select(".buttons").append("button")
.attr("class", "btn btn-default")
.text("CURRENT")
.on("click", function() { showCurrent(); });
d3.select(".buttons").append("button")
.attr("class", "btn btn-default")
.text("24-HR AVERAGE")
.on("click", function() { showRolling(); });
/////////////////////////////////////////////////////////////////////////////////////
// SETUP LINECHART //////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////
var margin = {top: 50, right: 20, bottom: 30, left: 50};
var graphic = d3.select("#chart");
var width = graphic.node().clientWidth - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
var xScale = d3.time.scale()
.range([0,width]);
var yScale = d3.scale.linear()
.range([height,0]);
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.innerTickSize(-height)
.tickPadding(10)
.outerTickSize(3)
.ticks(7)
.tickFormat(d3.time.format("%m/%d"));
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left")
.innerTickSize(-width)
.tickPadding(10)
.outerTickSize(3)
.tickFormat(d3.round);
var chartSvg = d3.select("#chart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var parseDate = d3.time.format("%m/%d/%Y %H:%M").parse;
data.forEach(function(d) {
d.roll_pm25 = +d.roll_PM25;
d.siteID = +d.siteID;
d.starttime = parseDate(d.startTime);
});
/////////////////////////////////////////////////////////////////////////////////////
// DRAW MAP MARKERS /////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////
// draw markers on the map
var circleWidth = 8;
var rectWidth = 12;
var pm25Circle = svg.selectAll("g>pm25Circle")
.data(latest.values);
var pm25Rect = svg.selectAll("g>pm25Rect")
.data(latest.values);
function removeCircle() {
pm25Circle.exit()
.transition()
.duration(500)
.style("opacity", "0")
.remove();
};
function removeRect() {
pm25Rect.exit()
.transition()
.duration(500)
.style("opacity", 0)
.remove();
};
function circleMouseOverRolling(d) {
d3.select(this).transition()
.ease("quad")
.duration("200")
.attr("r", circleWidth * 3)
tooltip.html("<h4>" + d.site + "<br />" + d.roll_PM25 + " &#181;g/m&sup3;</h4>")
.style("opacity", 0.8)
.style("left", (d3.event.pageX)+6 + "px")
.style("top", (d3.event.pageY)-80 + "px");
};
function circleMouseOverCurrent(d) {
d3.select(this).transition()
.ease("quad")
.duration("200")
.attr("r", circleWidth * 3)
tooltip.html("<h4>" + d.site + "<br />" + d.current_PM25 + " &#181;g/m&sup3;</h4>")
.style("opacity", 0.8)
.style("left", (d3.event.pageX)+6 + "px")
.style("top", (d3.event.pageY)-80 + "px");
};
function circleMouseOut(d) {
d3.select(this).transition()
.ease("quad")
.delay("100")
.duration("200")
.attr("r", circleWidth)
tooltip.style("opacity", 0); ;
};
function rectMouseOverRolling(d) {
d3.select(this).transition()
.ease("quad")
.duration("200")
.attr({ width: rectWidth * 3.5, height: rectWidth * 3.5 })
tooltip.html("<h4>" + d.site + "<br />" + d.roll_PM25 + " &#181;g/m&sup3;</h4>")
.style("opacity", 0.8)
.style("left", (d3.event.pageX)+6 + "px")
.style("top", (d3.event.pageY)-80 + "px");
};
function rectMouseOverCurrent(d) {
d3.select(this).transition()
.ease("quad")
.duration("200")
.attr({ width: rectWidth * 3.5, height: rectWidth * 3.5 })
tooltip.html("<h4>" + d.site + "<br />" + d.current_PM25 + " &#181;g/m&sup3;</h4>")
.style("opacity", 0.8)
.style("left", (d3.event.pageX)+6 + "px")
.style("top", (d3.event.pageY)-80 + "px");
};
function rectMouseOut(d) {
d3.select(this).transition()
.ease("quad")
.delay("100")
.duration("200")
.attr({ width: rectWidth, height: rectWidth });
};
// function to update the location of circles
function update() {
pm25Circle
.transition()
.duration(1000)
.attr("transform", function(d) {
return "translate(" +
map.latLngToLayerPoint(d.LatLng).x + "," +
map.latLngToLayerPoint(d.LatLng).y + ")";
})
pm25Rect
.transition()
.duration(1000)
.attr("transform", function(d) {
return "translate(" +
map.latLngToLayerPoint(d.LatLng).x + "," +
map.latLngToLayerPoint(d.LatLng).y + ")";
})
};
function removeMapSymbols(){
d3.selectAll("circle").transition().duration(600).style("opacity", 0).remove();
d3.selectAll("rect.pm25Circle").transition().duration(600).style("opacity", 0).remove();
d3.selectAll("rect.pm25Rect").transition().duration(600).style("opacity", 0).remove();
};
function showCurrent() {
removeMapSymbols();
hideSiteInfo();
unHighlightLegend();
pm25Circle.enter().append("g")
.append("circle")
.filter(function(d) { return d.location === "Roof"; })
.attr("class", "pm25Circle")
.attr("r", circleWidth)
.style("fill", currentColors)
.on("click", currentCircleClick)
.on("mouseover", circleMouseOverCurrent)
.on("mouseout", circleMouseOut);
pm25Rect.enter().append("g")
.append("rect")
.filter(function(d) { return d.location === "Street"; })
.attr("class", "pm25Circle")
.style("fill", currentColors)
.attr("width", rectWidth)
.attr("height", rectWidth)
.on("click", currentCircleClick)
.on("mouseover", rectMouseOverCurrent)
.on("mouseout", rectMouseOut);
map.on("viewreset", update);
update();
drawCurrentLine();
};
function showRolling() {
removeMapSymbols();
hideSiteInfo();
unHighlightLegend();
pm25Circle.enter().append("g")
.append("circle")
.filter(function(d) { return d.location === "Roof"; })
.attr("class", "pm25Circle")
.attr("r", circleWidth)
.style("fill", colors)
.on("click", rollingCircleClick)
.on("mouseover", circleMouseOverRolling)
.on("mouseout", circleMouseOut);
pm25Rect.enter().append("g")
.append("rect")
.filter(function(d) { return d.location === "Street"; })
.attr("class", "pm25Rect")
.style("fill", colors)
.attr("width", rectWidth)
.attr("height", rectWidth)
.on("click", rollingCircleClick)
.on("mouseover", rectMouseOverRolling)
.on("mouseout", rectMouseOut);
map.on("viewreset", update);
update();
drawRollingLine();
};
// default to show rolling 24-hr average data on page load
window.onload = showRolling();
/////////////////////////////////////////////////////////////////////////////////////
// DRAW LINE CHART///////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////
function removeLine() {
d3.selectAll("g>text").remove();
d3.selectAll(".g-group").remove();
d3.selectAll("path.minMaxMean").transition().duration(400).style("opacity", 0).remove();
d3.selectAll(".naaqsline").remove();
d3.selectAll(".x.axis").remove();
d3.selectAll(".y.axis").remove();
};
function drawRollingLine(){
removeLine();
chartSvg.append("text")
.attr("class", "chartTitle")
.attr("x", width/2)
.attr("y", 0 - (margin.top / 2))
.attr("text-anchor", "middle")
.html("Rolling 24 Hour Average PM2.5 (&#181;g/m&sup3;)");
// set the yScale max value to the naaqs value (35) if roll_PM25 value is less than the naaqs value
// otherwise, show the max roll_PM25 value
var min = d3.min(data, function(d) { return d.roll_PM25; });
var max = d3.max(data, function(d) { return d.roll_PM25 ? 40 : d.roll_PM25; });
xScale.domain(d3.extent(data, function(d) { return d.starttime; }));
yScale.domain([min, max]);
var lineGroup = chartSvg.selectAll(".g-group")
.data(nestedBySite)
.enter()
.append("g")
.attr("class", "g-group");
var line = d3.svg.line()
.x(function(d) { return xScale(d.starttime); })
.y(function(d) { return yScale(d.roll_PM25); });
lineGroup.append("path")
.attr("class", function(d) { return "g-line"; })
.attr("d", function(d) { return line(d.values); });
// add x and y axis
var xAxisGroup = chartSvg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
var yAxisGroup = chartSvg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.html("(&#181;g/m&sup3;)");
// add the min, max and mean lines
// nest data by starttime and add mean, max and min values to the array
var minMaxMean = d3.nest()
.key(function(d) { return d.starttime; })
.rollup(function(d) {
return {
starttime: d[0].starttime,
//location: d.location,
mean: d3.mean(d, function(g) { return g.roll_PM25; }),
max: d3.max(d, function(g) { return g.roll_PM25; }),
min: d3.min(d, function(g) { return g.roll_PM25; })
};
})
.entries(data)
// get the values only
.map(function(d){
return d.values;
});
console.log("minMaxMean = ", minMaxMean);
// the nest loses the sorting of the data, so need to re-sort
minMaxMean.sort(function(a,b){
return a.starttime - b.starttime;
});
// 3 lines for mean, min, and max
// draw mean line
chartSvg.append("path")
.attr("class", "minMaxMean")
.attr("d", d3.svg.line()
.x(function(d) { return xScale(d.starttime); })
.y(function(d) { return yScale(d.mean); })(minMaxMean)
)
.style("stroke","#ababab")
.style("fill", "none");
// draw max line
chartSvg.append("path")
.attr("class", "minMaxMean")
.attr("d", d3.svg.line()
.x(function(d) { return xScale(d.starttime); })
.y(function(d) { return yScale(d.max); })(minMaxMean)
)
.style("stroke","#ababab")
.style("fill", "none");
// draw min line
chartSvg.append("path")
.attr("class", "minMaxMean")
.attr("d", d3.svg.line()
.x(function(d) { return xScale(d.starttime); })
.y(function(d) { return yScale(d.min); })(minMaxMean)
)
.style("stroke","#ababab")
.style("fill", "none");
// text label for mean, min, and max lines
// label for mean line
chartSvg.append("text")
.attr("transform", "translate(" + (width+3) + "," + yScale(minMaxMean[0].mean) + ")")
.attr("dy", ".12em")
.attr("text-anchor", "end")
.style("fill", "#ababab")
.style("font-size", "10px")
.text("Mean");
// label for max line
chartSvg.append("text")
.attr("transform", "translate(" + (width+3) + "," + yScale(minMaxMean[0].max) + ")")
.attr("dy", ".12em")
.attr("text-anchor", "end")
.style("fill", "#ababab")
.style("font-size", "10px")
.text("Max");
// label for min line
chartSvg.append("text")
.attr("transform", "translate(" + (width+3) + "," + yScale(minMaxMean[0].min) + ")")
.attr("dy", ".12em")
.attr("text-anchor", "end")
.style("fill", "#ababab")
.style("font-size", "10px")
.text("Min");
// add the NAAQS line
var naaqsLine = { lineValue: 35, label: "National Ambient Air Quality Standard" };
var xExtent = d3.extent(data, function(d) { return d.starttime; });
var yExtent = d3.extent(data, function(d) { return d.roll_PM25; });
console.log(xExtent, yExtent);
chartSvg.append("svg:line")
.attr("x1", xScale(xExtent[0]))
.attr("y1", yScale(naaqsLine.lineValue))
.attr("x2", xScale(xExtent[1]))
.attr("y2", yScale(naaqsLine.lineValue))
.attr("class", "naaqsline");
chartSvg.append("text")
.attr("x", xScale(xExtent[1]))
.attr("y", yScale(naaqsLine.lineValue))
.attr("dy", "-0.5em")
.attr("text-anchor", "end")
.text(naaqsLine.label)
.attr("class", "naaqslinetext");
resizeChart();
};
function drawCurrentLine(){
removeLine();
chartSvg.append("text")
.attr("class", "chartTitle")
.attr("x", width/2)
.attr("y", 0 - (margin.top / 2))
.attr("text-anchor", "middle")
.html("Current PM2.5 (&#181;g/m&sup3;)");
// set the yScale max value to the naaqs value (35) if roll_PM25 value is less than the naaqs value
// otherwise, show the max roll_PM25 value
var min = d3.min(data, function(d) { return d.current_PM25; });
var max = d3.max(data, function(d) { return d.current_PM25 ? 40 : d.current_PM25; });
xScale.domain(d3.extent(data, function(d) { return d.starttime; }));
yScale.domain([min, max]);
var lineGroup = chartSvg.selectAll(".g-group")
.data(nestedBySite)
.enter()
.append("g")
.attr("class", "g-group");
var line = d3.svg.line()
.x(function(d) { return xScale(d.starttime); })
.y(function(d) { return yScale(d.current_PM25); });
lineGroup.append("path")
.attr("class", function(d) { return "g-line"; })
.attr("d", function(d) { return line(d.values); });
// add x and y axis
var xAxisGroup = chartSvg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
var yAxisGroup = chartSvg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.html("(&#181;g/m&sup3;)");
// add the min, max and mean lines
// nest data by starttime and add mean, max and min values to the array
var minMaxMean = d3.nest()
.key(function(d) { return d.starttime; })
.rollup(function(d) {
return {
starttime: d[0].starttime,
//location: d.location,
mean: d3.mean(d, function(g) { return g.current_PM25; }),
max: d3.max(d, function(g) { return g.current_PM25; }),
min: d3.min(d, function(g) { return g.current_PM25; })
};
})
.entries(data)
// get the values only
.map(function(d){
return d.values;
});
console.log("minMaxMean = ", minMaxMean);
// the nest loses the sorting of the data, so need to re-sort
minMaxMean.sort(function(a,b){
return a.starttime - b.starttime;
});
// 3 lines for mean, min, and max
// draw mean line
chartSvg.append("path")
.attr("class", "minMaxMean")
.attr("d", d3.svg.line()
.x(function(d) { return xScale(d.starttime); })
.y(function(d) { return yScale(d.mean); })(minMaxMean)
)
.style("stroke","#ababab")
.style("fill", "none");
// draw max line
chartSvg.append("path")
.attr("class", "minMaxMean")
.attr("d", d3.svg.line()
.x(function(d) { return xScale(d.starttime); })
.y(function(d) { return yScale(d.max); })(minMaxMean)
)
.style("stroke","#ababab")
.style("fill", "none");
// draw min line
chartSvg.append("path")
.attr("class", "minMaxMean")
.attr("d", d3.svg.line()
.x(function(d) { return xScale(d.starttime); })
.y(function(d) { return yScale(d.min); })(minMaxMean)
)
.style("stroke","#ababab")
.style("fill", "none");
// text label for mean, min, and max lines
// label for mean line
chartSvg.append("text")
.attr("transform", "translate(" + (width+3) + "," + yScale(minMaxMean[0].mean) + ")")
.attr("dy", ".12em")
.attr("text-anchor", "end")
.style("fill", "#ababab")
.style("font-size", "10px")
.text("Mean");
// label for max line
chartSvg.append("text")
.attr("transform", "translate(" + (width+3) + "," + yScale(minMaxMean[0].max) + ")")
.attr("dy", ".12em")
.attr("text-anchor", "end")
.style("fill", "#ababab")
.style("font-size", "10px")
.text("Max");
// label for min line
chartSvg.append("text")
.attr("transform", "translate(" + (width+3) + "," + yScale(minMaxMean[0].min) + ")")
.attr("dy", ".12em")
.attr("text-anchor", "end")
.style("fill", "#ababab")
.style("font-size", "10px")
.text("Min");
// add the NAAQS line
var naaqsLine = { lineValue: 35, label: "National Ambient Air Quality Standard" };
var xExtent = d3.extent(data, function(d) { return d.starttime; });
var yExtent = d3.extent(data, function(d) { return d.current_PM25; });
console.log(xExtent, yExtent);
chartSvg.append("svg:line")
.attr("x1", xScale(xExtent[0]))
.attr("y1", yScale(naaqsLine.lineValue))
.attr("x2", xScale(xExtent[1]))
.attr("y2", yScale(naaqsLine.lineValue))
.attr("class", "naaqsline");
chartSvg.append("text")
.attr("x", xScale(xExtent[1]))
.attr("y", yScale(naaqsLine.lineValue))
.attr("dy", "-0.5em")
.attr("text-anchor", "end")
.text(naaqsLine.label)
.attr("class", "naaqslinetext");
resizeChart();
};
// resize line chart
function resizeChart() {
d3.select(window).on("resize", resizeHandler);
function resizeHandler() {
width = graphic.node().clientWidth - margin.left - margin.right;
xScale.range([0, width]);
xAxisGroup.call(xAxis);
lineGroup.attr("path", function(d) { return xScale(d.startTime); });
};
};
// call this function when a map circle is clicked
function rollingCircleClick(id) {
highlightLine(id);
showSiteInfo(id);
// highlight a legend box that corresponds to a clicked map circle
var self = d3.select(this),
rects = d3.selectAll(".symbols>div>svg>rect");
// clear previous selection
rects.style({ "stroke": "none", "stroke-width": "1px" });
// loop and hightlight matches
rects.each(function(){
var r = d3.select(this);
if (r.style("fill") === self.style("fill")){
r.style({ "stroke": "red", "stroke-width": "3px" });
}
});
// highlight the selected map markers
var cir = d3.selectAll("svg>g>circle");
var sq = d3.selectAll("svg>g>rect");
// clear the selection
cir.classed("circleSelected", false);
sq.classed("circleSelected", false);
// highlight the selection
self.classed("circleSelected", true);
};
function currentCircleClick(id) {
highlightLine(id);
showSiteInfo(id);
// highlight a legend box that corresponds to a clicked map circle
var self = d3.select(this),
rects = d3.selectAll(".symbols>div>svg>rect");
// clear previous selection
rects.style({ "stroke": "none", "stroke-width": "1px" });
// loop and hightlight matches
rects.each(function(){
var r = d3.select(this);
if (r.style("fill") === self.style("fill")){
r.style({ "stroke": "red", "stroke-width": "3px" });
}
});
// highlight the selected map markers
var cir = d3.selectAll("svg>g>circle");
var sq = d3.selectAll("svg>g>rect");
// clear the selection
cir.classed("circleSelected", false);
sq.classed("circleSelected", false);
// highlight the selection
self.classed("circleSelected", true);
};
// highlight a line that corresponds to a clicked map circle
function highlightLine(id){
var lineGroup = d3.selectAll(".g-group");
lineGroup.classed("g-highlight", function(d) {
//console.log(d);
return d.key == id.siteID;
});
};
// remove highlight from legend box
function unHighlightLegend(id){
var selectedLegend = d3.selectAll(".symbols>div>svg>rect");
selectedLegend.style("stroke", "none");
};
// show site photo
function showSiteInfo(id){
// show site info
d3.select(".site").html("<h3>" + id.site + "</h3><span class='italic'>" + id.address + "</span><br /><br />" + "Measured at " + id.location +
"<br />Building Density: " + id.bldgarea_c + "<br />Traffic Density: " + id.traffic_cl + "<br >Data as of " + id.updateTime);
d3.select(".site-photo").html("<img src='assets/sites/" + id.siteID
+ ".jpg' class='media-object img-responsive' width='100%'><p class='small'>Image Source: Google StreetView</p>");
};
// remove site photo
function hideSiteInfo(){
d3.select(".site").html("");
d3.select(".site-photo").html("");
};
/////////////////////////////////////////////////////////////////////////////////////
// DRAW MAP LEGEND///////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////
// responsive legend container
var legend = d3.select(".symbols").append("div")
.classed("svg-container", true)
.append("svg")
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", "0 0 463.9 74.2")
.classed("svg-content-responsive", true);
// draw map legend boxes
legend.selectAll("rect")
.data(circleColorMap)
.enter()
.append("rect")
.attr("x", function (d, i) { return (i+1) * 18.6; })
.attr("y", 18.6)
.attr("class", "color-box")
.style("fill", function(d) { return d.color; })
.on("mouseover", function(d){
tooltip.html( function (e) {
return (d.value == "" ? "" : (d.value != 400 ? "<" + d.value + " &#181;g/m&sup3;" : ">" + d.value + " &#181;g/m&sup3;"));
})
.style("opacity", 0.8)
.style("left", (d3.event.pageX)-30 + "px")
.style("top", (d3.event.pageY)-40 + "px")
})
.on("mouseout", function(d) {
tooltip.style("opacity", 0);
});
//label map legend boxes
var legendLabel = legend.selectAll("text")
.data(circleColorMap)
.enter()
.append("text")
.attr("x", function (d, i) { return (i+1) * 18.6; })
.attr("y", 50)
.attr("class", "color-box-label")
//.text(function(d) { return d.condition; });
.text(function(d) {
if(d.condition == "UNHEALTHY FOR SENSITIVE GROUP"){
var tspan = d3.select(".color-box-label")
.append("tspan")
.attr("dy", 10)
.attr("x", 167.4)
.text("SENSITIVE GROUP");
return "UNHEALTHY FOR ";
return tspan;
} else {
return d.condition;
};
});
}; // end of d3 data load
/////////////////////////////////////////////////////////////////////////////////////
// ADD ANNUAL AVERAGE DATA OVERLAY///////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////
// add checkbox and label to the button div
var checkBox = d3.select(".buttons").append("form")
.attr("class", "form")
.append("input")
.attr("id", "annual")
.attr("type", "checkbox")
.style("margin-left", "5px");
d3.select(".buttons").append("label")
.attr("class", "annualLabel")
.style("margin-left", "25px")
.style("margin-top", "18px")
.html("<label for='annual'>ANNUAL AVERAGE</label>");
//set the annual average data shades
function getColor(d) {
return d >= 0 && d < 8 ? "#ddd266" :
d >= 8.1 && d < 8.6 ? "#dbba67" :
d >= 8.7 && d < 9.1 ? "#dbae67" :
d >= 9.2 && d < 9.6 ? "#bf9158" :
d >= 9.7 ? "#93774c" :
"grey";
};
// load annual average data
// note that the data values need to be divided by 1000
var annualData = "https://raw.githubusercontent.com/kaz-a/nyccas_inhouse/master/app/assets/data/aa6_pm300m1_GCSWGS1984_unprojected.geojson";
var legend = L.control({ position: "topleft" });
// draw annual average map legend
legend.onAdd = function (map) {
var div = L.DomUtil.create("div", "legend-annualAvg"),
annualLevel = [0, 8.1, 8.7, 9.2, 9.7],
labels = [];
// loop through intervals and generate a label with a colored square for each interval
for (var i = 0; i < annualLevel.length; i++) {
div.innerHTML +=
'<i style="background:' + getColor(annualLevel[i]) + '"></i> ' +
annualLevel[i] + (annualLevel[i + 1] ? '&ndash;' + annualLevel[i + 1] + '&#181;g/m&sup3;<br />' : '&#181;g/m&sup3;+');
}
return div;
};
$.getJSON(annualData, function (annualData) {
console.log(annualData);
//get unique annual average values (for legend)
var annualFeatures = annualData.features;
var uniqueValue = [];
var division = 1000; // gridcode values need to be divided by 1000
annualFeatures.forEach(function (x) {
if (!annualFeatures[x.properties.gridcode]) {
uniqueValue.push(x.properties.gridcode/division);
annualFeatures[x.properties.Annual] = true;
}
});
console.log(annualData);
console.log(uniqueValue);
// style properties for the grid cells
function style(feature) {
return {
fillColor: getColor(feature.properties.gridcode/division),
weight: 0.1,
opacity: 0.5,
color: "none",
dashArray: "1",
fillOpacity: 0.5
};
};
// add annual data
var addAnnualData = L.geoJson(annualData, { style: style });
// click event to overlay annual data and legend
$("#annual").click(function () {
if (this.checked) {
//console.log("checked!");
addAnnualData.addTo(map);
addAnnualData.bringToBack();
legend.addTo(map);
} else {
//console.log("unchecked!");
map.removeLayer(addAnnualData);
map.removeControl(legend);
};
});
});
body{
font-family: 'Open Sans', sans-serif;
font-weight: 300;
overflow: none;
}
.social {
top: 40px;
}
#map {
height: 500px;
}
.buttons {
padding: 0px;
}
button {
margin: 5px;
}
.content {
height: 500px;
}
.legend {
text-align: center;
width: 50%;
/*margin-top: 50px;
margin-bottom: 50px;*/
}
.symbols {
/*width: 70%;*/
}
.message {
text-align: center;
width: 50%;
margin-top: 50px;
margin-bottom: 50px;
}
.message h3 {
margin: 40px;
}
hr {
width: 55%;
border-top: 1px solid black;
}
#chart {
height: 300px;
/*border-top: 1px solid #ccc;*/
border-width: 70%;
}
.media-body {
width: auto;
}
.site-photo {
/*height: 200px;*/
padding: 20px;
}
.italic {
font-style: italic;
}
.small {
font-size: 10px;
}
.grey {
color: #999;
}
.large {
font-size: 30px;
}
.pm25Circle {
stroke: white;
stroke-width: 2px;
opacity: 1;
cursor: pointer;
}
.pm25Rect {
stroke: white;
stroke-width: 2px;
opacity: 1;
cursor: pointer;
}
.circleSelected {
stroke: red;
stroke-width: 4px;
opacity: 1;
}
div.tooltip {
position: absolute;
text-align: left;
width: auto;
height: auto;
padding: 8px;
font: 12px sans-serif;
background: white;
border: white 1px solid;
border-radius: 0px;
pointer-events: none;
}
.controller-button{
padding: 10px;
margin: 10px;
}
.info {
position: relative;
}
.info .overlay {
position: absolute;
top: 10;
left: 10;
pointer-events: none;
}
.button {
padding-bottom: 20px;
}
.weather {
float: right;
padding: 0;
}
.current-date {
font-weight: 300;
text-align: center;
}
.current-date span {
font-size: 21px;
}
h1, h2, h3 {
font-weight: 300;
}
.container-fluid {
padding: 0 !important;
}
.date {
padding: 30px;
}
.date p {
font-size: 14px;
margin: 10px 0 0;
}
.aqi-container {
text-align: center;
padding: 30px;
height: 225px;
}
.weather-container {
text-align: center;
padding: 30px;
height: 225px;
}
.current-aqi h1 {
font-size: 60px;
font-weight: 300;
display: inline;
}
.current-cond i {
font-size: 70px;
}
.aqi-qual {
font-size: 24px;
font-weight: 300;
margin-top: 36px;
line-height: 0;
}
.aqi-qual p {
font-size: 14px;
font-weight: 300;
margin-top: 24px;
/*line-height: 0;*/
}
.tempWind p {
margin-top: -10px;
}
.day {
/*background: linear-gradient( 180deg, #4750a4, #819e9a ); */
background-image: url("day.jpg");
background-repeat: no-repeat;
background-size: cover;
color: white;
}
.sunset {
/*background: linear-gradient( 180deg, #312b50, #f09c8d ); */
background-image: url("sunsets.jpg");
background-repeat: no-repeat;
background-size: cover;
color: white;
}
.night {
/*background: linear-gradient( 180deg, #061b63, #f1b999 ); */
background-image: url("night.jpg");
background-repeat: no-repeat;
background-size: cover;
color: white;
}
.axis line {
fill: none;
stroke: black;
stroke-dasharray: 2px 3px;
shape-rendering: crispEdges;
stroke-width: 1px;
}
.axis path {
display: none;
}
.axis text {
font-size: 12px;
pointer-events: none;
fill: #777;
}
.y.axis text {
text-anchor: end !important;
font-size:12px;
}
.g-line {
stroke: #ababab;
stroke-opacity: 0.2;
fill:none;
stroke-width: 1px;
}
.g-highlight path {
stroke: #53c2c4;
stroke-opacity: 1;
fill: none;
stroke-width: 3px;
}
.line-hover {
color: #53c2c4;
}
.tick line{
opacity: 0.1;
}
footer {
background: black;
}
footer .container {
padding: 3%;
}
.bold {
font-weight: 300;
font-size: 0.8em;
color: white;
}
ul {
list-style-type: none;
}
footer div div ul li {
padding: 3%;
font-weight: 100;
}
footer ul li a {
color: rgba(255,255,255,0.6);
}
footer ul li a:hover {
color: red;
text-decoration: none;
}
.leaflet-control-attribution a {
color: grey;
}
.color-box {
width: 18.6px;
height: 18.6px;
opacity: 0.8;
cursor: pointer;
}
.color-box-label {
font-size: 8px;
}
.naaqsline {
fill: none;
stroke: #000;
stroke-width: 0.2px;
stroke-dasharray: 5 5;
}
.naaqslinetext {
fill: #000;
stroke: none;
font-size: 10px;
font-weight: 100;
}
.min-max-mean_line {
fill: none;
stroke: red;
stroke-width: 1px;
}
.min-max-mean_line_text {
fill: #ababab;
stroke: none;
font-size: 8px;
font-weight: 100;
}
.overlay {
fill: none;
pointer-events: all;
}
.focus circle {
fill: none;
stroke: red;
}
.svg-container {
display: inline-block;
position: relative;
width: 100%;
padding-bottom: 18%; /* aspect ratio */
vertical-align: top;
overflow: hidden;
}
.svg-content-responsive {
display: inline-block;
position: absolute;
top: 10px;
left: 0;
}
form {
font-family: 'Open Sans', sans-serif;
font-size: 14px;
font-weight: 300;
width: 0px;
display: inline-block;
}
/*input#annual:checked:before {
content: "HIDE ANNUAL AVERAGE";
background: white;
border: 1px solid #53c2c4;
color: black;
display: inline-block;
}
*/
/*input#annual:before {
content: attr(value);
margin: -29px 0px;
display: inline-block;
white-space: nowrap;
cursor: pointer;
padding: 17px;
border: 1px solid black;
background: white;
color: black;
display: inline-block;
}*/
input#annual:before {
content: attr(value);
display: inline-block;
white-space: nowrap;
width: 16px;
height: 16px;
background: white;
border: 1px solid black;
cursor: pointer;
}
input#annual:checked:before {
/*content: "HIDE ANNUAL AVERAGE";*/
display: inline-block;
background: grey;
border: 1px solid grey;
color: black;
cursor: pointer;
}
.annualLabel {
font-weight: 300 !important;
display: inline-block;
}
.legend-annualAvg {
line-height: 12px;
color: black;
background: rgba(255, 255, 255, 0.7);
padding: 10px;
font-family: 'Open Sans', sans-serif;
font-weight: 300;
font-size: 10px;
}
.legend-annualAvg i {
width: 12px;
height: 12px;
float: left;
margin-right: 8px;
opacity: 0.7;
}
a.navbar-brand {
background-image: url("dohmh_logo.png");
background-size: cover;
width: 109px;
height: 70px;
margin-top: -28px;
}
.navbar-default {
background-color: transparent;
border-color: transparent;
}
.navbar-dohmh {
background-color: black;
border-radius: 0px;
}
.navbar-default .navbar-nav>li>a {
color: white;
}
.navbar-default .navbar-nav>li>a:focus {
color: white;
}
.navbar {
margin-bottom: 0px;
}
.navbar-default .navbar-toggle {
border-color: transparent;
}
.btn {
border-radius: 0px !important;
transition: linear, ease-in 0.3s !important;
}
.btn-default {
color: white !important;
background: grey !important;
border-color: grey !important;
padding: 16px !important;
float: left;
}
.btn-default:hover {
color: black !important;
background: white !important;
border-color: black !important;
}
.btn-default:focus {
outline: none !important;
text-decoration: none !important;
color: white !important;
background: #53c2c4 !important;
border: 1px #53c2c4 solid !important;
}
.btn-default:active {
color: white !important;
background: #53c2c4 !important;
border: 1px #53c2c4 solid !important;
}
a {
color: white !important;
text-decoration: none !important;
}
a:hover {
color: #53c2c4 !important;
text-decoration: none !important;
}
a:active {
color: #53c2c4 !important;
text-decoration: none !important;
}
a.active {
color: white !important;
text-decoration: none !important;
}
.leaflet-bar a, .leaflet-bar a:hover {
background: grey;
}
.leaflet-control-attribution a {
color: grey !important;
}
/*** Works on common browsers ***/
::selection {
background-color: #53c2c4;
}
/*** Mozilla based browsers ***/
::-moz-selection {
background-color: #53c2c4;
}
/***For Other Browsers ***/
::-o-selection {
background-color: #53c2c4;
}
::-ms-selection {
background-color: #53c2c4;
}
/*** For Webkit ***/
::-webkit-selection {
background-color: #53c2c4;
}
/* GalaxyS5 ipone5 iphone6 */
@media only screen
and (min-device-width : 320px)
and (max-device-width : 700px) {
.message {
margin-top: 230px;
}
.legend {
width: 80%;
}
}
/* Nexus5x, iphone6Plus */
@media only screen
and (min-device-width : 411px)
and (max-device-width : 730px) {
.message {
margin-top: 250px;
}
.legend {
width: 80%;
}
}
/* Nexus6P */
@media only screen
and (min-device-width : 435px)
and (max-device-width : 770px) {
.message {
margin-top: 250px;
}
.legend {
width: 80%;
}
}
/* ipad */
@media only screen
and (min-device-width : 768px)
and (max-device-width : 1020px) {
.message {
margin-top: 75px;
}
.legend {
width: 70%;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment