Last active
June 8, 2017 19:03
-
-
Save kaz-a/3d3f4f2a05f8029d060e66cb03e4ce4b to your computer and use it in GitHub Desktop.
EPHT Report Tiles
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<html> | |
<head> | |
<title>EPHT Report</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 href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet"> | |
<script src="https://use.fontawesome.com/5434c9593b.js"></script> | |
<link rel="stylesheet" href="css/main.css"> | |
<link rel="stylesheet" href="css/indicator.css"> | |
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> | |
<style> | |
body{ | |
font-family: 'Open Sans', sans-serif; | |
overflow: none; | |
} | |
.center{ | |
text-align: center; | |
} | |
.tile { | |
height: 400px; | |
border: white 10px solid; | |
padding: 20px; | |
} | |
.indicator_tile { | |
font-family: 'Open Sans', sans-serif; | |
} | |
.indicator_name { | |
font-weight: bold; | |
font-size: 15px; | |
padding: 10px 30px; | |
} | |
.neighborhood_value, .borough_value, .nyc_value { | |
margin: 20px 0 0 0; | |
} | |
.neighborhood_value>span, .borough_value>span, .nyc_value>span { | |
font-size: 25px; | |
font-weight: thin; | |
} | |
.svg { | |
padding: 60px 40px; | |
height: 150px; | |
width: 100%; | |
} | |
.barSvg { | |
padding: 0 40px; | |
height: 30px; | |
width: 100%; | |
} | |
div.tooltip { | |
position: absolute; | |
text-align: left; | |
width: auto; | |
height: auto; | |
padding: 8px; | |
font: 12px sans-serif; | |
background: black; | |
border-radius: 0px; | |
pointer-events: none; | |
color: white; | |
} | |
</style> | |
</head> | |
<body> | |
<div class='indicator_tile'> | |
</div> | |
</body> | |
<script | |
src="https://code.jquery.com/jquery-3.2.1.min.js" | |
integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" | |
crossorigin="anonymous"></script> | |
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" | |
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" | |
crossorigin="anonymous"></script> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<script> | |
function getUrl(reportId, geoTypeId, geoEntityId){ | |
var baseUrl = "http://a816-dohmeta.nyc.gov/MetadataLite/api/Report/GetReportData?"; | |
// need to map the name to the id - actually, mapping the address to the name would be better imo because we're searching by address | |
// var reportId = 78; // convert user selection "Asthma and the Environment" to report_id: 78 | |
// var geoTypeId = 3; // constant - always 3 | |
// var geoEntityId = 201; // convert user input "Greenpoint" to geo_entity_id: 201 | |
var url = baseUrl + "report_id=" + reportId + "&geo_type_id=" + geoTypeId + "&geo_entity_id=" + geoEntityId; | |
return url; | |
} | |
// sample inputs - replace these with user input | |
var geoType = 3; | |
var topicInput = 78; | |
var neighborhoodCode = 201; | |
var tabInput = "Indoor Air Quality"; | |
console.log(topicInput, neighborhoodCode, tabInput); | |
var json = getUrl(topicInput, geoType, neighborhoodCode); | |
var data = d3.json(json, ready); | |
// create a function that returns the data as the indicatorData object | |
function getIndicatorData(data){ | |
var reportContent = data.report.report_content; | |
var dataByTabItem = {}; | |
reportContent.forEach(function(i){ | |
var reportTopic = i.report_topic; | |
var reportTopicData = i.report_topic_data; | |
dataByTabItem[reportTopic] = reportTopicData; | |
}) | |
var indicatorData = dataByTabItem[tabInput]; | |
console.log(dataByTabItem); | |
return indicatorData; | |
} | |
// using the data from getIndicatorData, create a tile with indicator name, values, rank (color) and a trendchart | |
function createIndicatorTile(data){ | |
var indicatorData = getIndicatorData(data); | |
console.log("indicatorData:", indicatorData); | |
indicatorData.forEach(function(d){ | |
var tile = d3.select(".indicator_tile").append("div").attr("class", "tile center col-xs-12 col-sm-12 col-md-4"); | |
// indicator name | |
tile.append("div").attr("class", "indicator_name row").html(d.indicator_name); | |
// area values | |
var vals = tile.append("div").attr("class", "indicator_vals row"); | |
vals.append("div").attr("class", "neighborhood_value col-xs-4 col-sm-4 col-md-4").html("Neighborhood<br><span>" + Math.round(d.unmodified_data_value_geo_entity) + "</span>"); | |
vals.append("div").attr("class", "borough_value col-xs-4 col-sm-4 col-md-4").html("Borough<br><span>" + Math.round(d.unmodified_data_value_borough) + "</span>"); | |
vals.append("div").attr("class", "nyc_value col-xs-4 col-sm-4 col-md-4").html("NYC<br><span>" + Math.round(d.data_value_nyc) + "</span>"); | |
// rank color | |
var better = "#97ddda", middle = "#eded6e", worse = "#e8d1d1"; | |
var better_dark = "#2e8d92", middle_dark = "#d4ab00", worse_dark = "#ba8c8c"; | |
tile.style("background", function(){ | |
return d.data_value_rank === 1 ? better : d.data_value_rank === 2 ? middle : worse; | |
}); | |
// chart div | |
var chartDiv = tile.append("div").attr("class", "chart row"); | |
var elem = d3.select(".chart").node(); | |
var w = elem.getBoundingClientRect().width; | |
var h = 100; | |
// bar chart | |
var barChartDiv = chartDiv.append("div").attr("class", "bar_chart"); | |
var barElem = d3.select(".bar_chart").node(); | |
var bar_w = barElem.getBoundingClientRect().width; | |
var bar_h = 40; | |
var barSvg = barChartDiv.append("svg").attr("class", "barSvg"), | |
margin = {top: 0, right: 40, bottom: 10, left: 40}, | |
width = bar_w - margin.left - margin.right, | |
height = bar_h - margin.top - margin.bottom, | |
g = barSvg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
var x = d3.scaleBand().rangeRound([0, width]).padding(0.1), | |
y = d3.scaleLinear().rangeRound([height, 0]); | |
// create the area values dataset | |
// we want the data to be: | |
// --------------------- | |
// area val | |
// ------------ ---- | |
// NYC 4.9 | |
// borough 4.3 | |
// neighborhood 5.0 | |
// --------------------- | |
// ---> so barData should be: | |
// [{ area: "nyc", value: 4.9 }, { area: "borough", value: 4.3 }, { area: "neighborhood", value: 5.0 }] | |
// console.log(d); | |
var barData = function(x, y){ | |
var barVals = {}; | |
barVals["area"] = x; | |
barVals["value"] = y; | |
return barVals; | |
}; | |
for(var key in d){ | |
var nycVal = barData("nyc", d.data_value_nyc); | |
var boroughVal = barData("borough", d.unmodified_data_value_borough); | |
var neighborhoodVal = barData("neighborhood", d.unmodified_data_value_geo_entity); | |
} | |
var barData = [ neighborhoodVal, boroughVal, nycVal ]; | |
console.log('barData', barData); | |
x.domain(barData.map(function(d) { return d.area; })); | |
y.domain([0, d3.max(barData, function(d) { return d.value; })]); | |
barSvg.selectAll(".bar") | |
.data(barData) | |
.enter().append("rect") | |
.attr("class", "bar") | |
.attr("x", function(d) { return x(d.area); }) | |
.attr("y", function(d) { return y(d.value); }) | |
.attr("fill", function(){ | |
return d.data_value_rank === 1 ? better_dark : d.data_value_rank === 2 ? middle_dark : worse_dark; | |
}) | |
.attr("width", x.bandwidth()) | |
.attr("height", function(d) { return height - y(d.value); }); | |
// rank text | |
var rank = d.data_value_rank; | |
var rankTextDiv = chartDiv.append("div").attr("class", "rank_text"); | |
var rankText = rankTextDiv.append("text").text(function(){ | |
return rank === 1 ? "Better than other neighborhoods" : | |
rank === 2 ? "About the same as other neighborhoods" : | |
"Worse than other neighborhoods"; | |
}); | |
rankText.style("color", function(){ | |
return rank === 1 ? better_dark : rank === 2 ? middle_dark : worse_dark; | |
}) | |
// trend line chart | |
var trendData = d.trend_over_time; | |
// console.log("trendData:", trendData); | |
var formatYear = d3.timeParse("%Y-%m-%d"); | |
var lineChartDiv = chartDiv.append("div").attr("class", "line_chart"); | |
var lineElem = d3.select(".line_chart").node(); | |
var w = lineElem.getBoundingClientRect().width; | |
var h = 40; | |
var svg = lineChartDiv.append("svg").attr("class", "svg"), | |
margin = {top: 0, right: 40, bottom: 0, left: 40}, | |
width = w - margin.left - margin.right, | |
height = h - margin.top - margin.bottom, | |
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
console.log("chart height: ", height, "chart width:", width); | |
var tooltip = d3.select("body").append("div") | |
.attr("class", "tooltip") | |
.style("opacity", 0); | |
// if there is 1 or less data points, show a message instead | |
if(trendData.length <= 1){ | |
svg.append("text") | |
.attr("x", "50%") | |
.attr("y", "30%") | |
.attr("text-anchor", "middle") | |
.attr("font-family","FontAwesome") | |
.attr("font-size", function(d) { return "30px";} ) | |
.html(function(d) { return ""; }); // frown face :^( we can remove this | |
svg.append("text").text("Insufficient data for trendline") | |
.attr("x", "50%").attr("y", "100%").attr("text-anchor", "middle"); | |
} else { | |
trendData.forEach(function(d){ | |
year = d.year; | |
var x = d3.scaleTime().rangeRound([0, width]); | |
var y = d3.scaleLinear().rangeRound([height, 0]); | |
var xAxis = d3.axisBottom().scale(x); | |
var yAxis = d3.axisLeft().scale(y); | |
var nestedData = d3.nest() | |
.key(function(d) { return d.start_period; }) | |
.entries(trendData); | |
// console.log("nestedData:", nestedData); | |
drawChart(); | |
function drawChart() { | |
var line = d3.line() | |
.x( function(d) { return x(formatYear(d.key)) } ) | |
.y( function(d) { return y(d.values[0].data_value) } ); | |
x.domain( d3.extent( nestedData, function(d) { return formatYear(d.key) } ) ); | |
y.domain( [ 0, d3.max( nestedData, function(d) { return d.values[0].data_value } ) ]); | |
var trendLine = svg.selectAll(".trendLine") | |
.data(nestedData) | |
.enter() | |
.append("g") | |
.attr("class", "trendLine") | |
.attr("id", function(d) { return d.key } ); | |
trendLine.append("path") | |
.attr("class", "line") | |
.attr("fill", "none") | |
.attr("stroke", "#000") | |
.attr("stroke-linejoin", "round") | |
.attr("stroke-linecap", "round") | |
.attr("stroke-width", 1.5) | |
.attr("d", function(d) { return line(nestedData); } ); | |
var circleWidth = 4; | |
var circle = trendLine.selectAll("circle") | |
.data(nestedData) | |
.enter() | |
.append("circle") | |
.attr("r", circleWidth) | |
.attr("cx", function(d) { return x(formatYear(d.key)); }) | |
.attr("cy", function(d) { return y(d.values[0].data_value); }) | |
.attr("fill", function(){ | |
return rank === 1 ? better : rank === 2 ? middle : worse; | |
}) | |
.attr("stroke", "#000") | |
.attr("opacity", 1) // change to 0 for hiding datapoint circles | |
.style("cursor", "pointer") | |
.on("mouseover", function(d) { | |
d3.select(this).style("opacity", 1) | |
.attr("fill", function(){ | |
return rank === 1 ? better_dark : rank === 2 ? middle_dark : worse_dark; | |
}) | |
.attr("r", circleWidth * 2) | |
tooltip.text(d.values[0].year + ": " + d.values[0].data_value) | |
.style("opacity", 0.7) | |
.style("left", (d3.event.pageX)-30 + "px") | |
.style("top", (d3.event.pageY)-50 + "px"); | |
}) | |
.on("mouseout", function(d) { | |
d3.select(this).style("opacity", 1) // change to 0 for hiding datapoint circles | |
.attr("fill", function(){ | |
return rank === 1 ? better : rank === 2 ? middle : worse; | |
}) | |
.attr("r", circleWidth); | |
tooltip.style("opacity", 0); | |
}); | |
var circleLabel = trendLine.selectAll("text") | |
.data(nestedData) | |
.enter() | |
.append("text") | |
.filter(function(d, i) { return i === 0 || i === (nestedData.length - 1) }) | |
.text(function(d) { return d.values[0].year; }) | |
.attr("x", function(d) { return x(formatYear(d.key)); }) | |
.attr("y", function(d) { return y(d.values[0].data_value) + 14; }) | |
.attr("text-anchor", function(d, i){ return i === 0 ? "left" : "end"; }) | |
.attr("font-size", "10px") | |
.attr("fill", "#000"); | |
} | |
}); | |
} | |
}) | |
} | |
// render tiles | |
function ready(error, data) { | |
if (error) throw error; | |
createIndicatorTile(data); | |
} | |
</script> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment