Created
March 15, 2018 13:20
-
-
Save Zhenmao/8b2fa8d5228406dbec77801f7204da39 to your computer and use it in GitHub Desktop.
TBI-related Emergency Department Visits & Deaths
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> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<title></title> | |
<link href="https://fonts.googleapis.com/css?family=Inconsolata:400,700" rel="stylesheet"> | |
<style> | |
#container { | |
font-family: 'Inconsolata', monospace; | |
font-size: 14px; | |
display: flex; | |
flex-direction: column; | |
justify-content: center; | |
align-items: center; | |
} | |
.axis text { | |
font-family: 'Inconsolata', monospace; | |
font-size: 14px; | |
} | |
.axis .domain { | |
display: none; | |
} | |
.axis line { | |
display: none; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="container"> | |
<div><h2>TBI-related Emergency Department Visits & Deaths</h2></div> | |
<div><h2>Age Group: <span id="age-group-label"></span></h2></div> | |
<div id="chart"></div> | |
</div> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<script> | |
var margin = {top: 20, right: 160, bottom: 20, left: 160}, | |
width = 600 - margin.left - margin.right, | |
height = 600 - margin.top - margin.bottom, | |
padding = 0.5, // Separation between nodes | |
radius = 5, // Node radius | |
duration = 3000, // Transition time between age groups | |
colors = ["#e67e22", "#e74c3c", "#3498db", "#2ecc71", "#9b59b6", "#f1c40f"]; | |
var alpha = 0.4, // Force simulation alpha target for smooth animation | |
strength = 0.1; // Position force strength | |
var simulation = d3.forceSimulation() | |
.velocityDecay(0.5) | |
.alphaDecay(0.04) | |
.force("x", d3.forceX().strength(strength).x(getClusterX)) | |
.force("y", d3.forceY().strength(strength).y(getClusterY)) | |
.force("collision", d3.forceCollide().radius(radius + padding).iterations(8)) | |
.on("tick", ticked); | |
simulation.stop(); | |
var timeout, | |
currentSchedule = 0; | |
// Process data | |
var series = [ | |
{ short: "visit", long: "Emergency Department Visits" }, | |
{ short: "death", long: "Deaths" } | |
]; | |
var ageGroups = ["0-4", "5-14", "15-24", "25-44", "45-64", "≥ 65"]; | |
var visitsCounts = [ | |
{ "Age Group": "0–4", "Motor Vehicle Traffic": 14655, "Falls": 250413, "Assault": 1513, "Struck by/Against": 53761, "Unknown": 10225, "All Other Causes": 13222 }, | |
{ "Age Group": "5–14", "Motor Vehicle Traffic": 18110, "Falls": 101790, "Assault": 16612, "Struck by/Against": 101112, "Unknown": 20763, "All Other Causes": 31355 }, | |
{ "Age Group": "15–24", "Motor Vehicle Traffic": 76602, "Falls": 77951, "Assault": 81822, "Struck by/Against": 71031, "Unknown": 22722, "All Other Causes": 34486 }, | |
{ "Age Group": "25–44", "Motor Vehicle Traffic": 75122, "Falls": 80867, "Assault": 75527, "Struck by/Against": 49505, "Unknown": 22855, "All Other Causes": 36933 }, | |
{ "Age Group": "45–64", "Motor Vehicle Traffic": 46923, "Falls": 95824, "Assault": 28206, "Struck by/Against": 36925, "Unknown": 18804, "All Other Causes": 15843 }, | |
{ "Age Group": "≥ 65", "Motor Vehicle Traffic": 10359, "Falls": 174544, "Assault": 4068, "Struck by/Against": 12815, "Unknown": 5216, "All Other Causes": 6285 } | |
]; | |
var visitsCategories = Object.keys(visitsCounts[0]).slice(1); | |
var visitsPercentages = countsToPercentages(visitsCounts, visitsCategories); | |
var visits = percentagesToSequences(visitsPercentages, visitsCategories); | |
var deathsCounts = [ | |
{ "Age Group": "0–4", "Motor Vehicle Traffic": 278, "Falls": 37, "Assault": 408, "Struck by/Against": 32, "Self-Inflicted": 0, "All Other Causes": 196 }, | |
{ "Age Group": "5–14", "Motor Vehicle Traffic": 488, "Falls": 21, "Assault": 131, "Struck by/Against": 19, "Self-Inflicted": 58, "All Other Causes": 158 }, | |
{ "Age Group": "15–24", "Motor Vehicle Traffic": 3670, "Falls": 139, "Assault": 1515, "Struck by/Against": 28, "Self-Inflicted": 1834, "All Other Causes": 551 }, | |
{ "Age Group": "25–44", "Motor Vehicle Traffic": 4310, "Falls": 548, "Assault": 2151, "Struck by/Against": 88, "Self-Inflicted": 4587, "All Other Causes": 1186 }, | |
{ "Age Group": "45–64", "Motor Vehicle Traffic": 3230, "Falls": 2077, "Assault": 1142, "Struck by/Against": 126, "Self-Inflicted": 5601, "All Other Causes": 1710 }, | |
{ "Age Group": "≥65", "Motor Vehicle Traffic": 1651, "Falls": 9444, "Assault": 357, "Struck by/Against": 79, "Self-Inflicted": 3362, "All Other Causes": 2483 } | |
]; | |
var deathsCategories = Object.keys(deathsCounts[0]).slice(1); | |
var deathsPercentages = countsToPercentages(deathsCounts, deathsCategories); | |
var deaths = percentagesToSequences(deathsPercentages, deathsCategories); | |
// Combine both datasets | |
var data = visits.map(function (d) { | |
return { | |
series: "visit", | |
schedules: d | |
}; | |
}).concat(deaths.map(function (d) { | |
return { | |
series: "death", | |
schedules: d | |
}; | |
})); | |
// Set up scales and axes | |
var x = d3.scalePoint() | |
.domain(series.map(function (d) { | |
return d.short; | |
})) | |
.range([0, width]) | |
.padding(0.5); | |
var xAxis = d3.axisTop(x) | |
.tickFormat(function (d) { | |
return series.find(function (e) { | |
return e.short === d; | |
}).long; | |
}); | |
var yVisits = d3.scalePoint() | |
.domain(visitsCategories) | |
.range([0, height]) | |
.padding(0.5); | |
var yVisitsAxis = d3.axisLeft(yVisits); | |
var yDeaths = d3.scalePoint() | |
.domain(deathsCategories) | |
.range([0, height]) | |
.padding(0.5); | |
var yDeathsAxis = d3.axisRight(yDeaths); | |
var colorVisits = d3.scaleOrdinal() | |
.domain(visitsCategories) | |
.range(colors); | |
var colorDeaths = d3.scaleOrdinal() | |
.domain(deathsCategories) | |
.range(colors); | |
// Set up SVG containers | |
var g = 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 + ")"); | |
// Draw axes | |
g.append("g") | |
.attr("class", "x axis") | |
.call(xAxis); | |
g.append("g") | |
.attr("class", "y axis") | |
.call(yVisitsAxis); | |
g.append("g") | |
.attr("class", "y axis") | |
.attr("transform", "translate(" + width + ",0)") | |
.call(yDeathsAxis); | |
// Add age group label | |
var ageGroupLabel = d3.select("#age-group-label") | |
.text(ageGroups[currentSchedule]); | |
// Add percentage labels | |
var visitsPercentagesLabels = g.append("g") | |
.selectAll(".percentage-label") | |
.data(visitsCategories) | |
.enter().append("text") | |
.attr("class", "percentage-label") | |
.attr("x", -9) | |
.attr("y", function(d) { | |
return yVisits(d); | |
}) | |
.attr("dy", "1.8em") | |
.style("text-anchor", "end") | |
.text(function(d) { | |
return visitsPercentages[currentSchedule][d] + "%"; | |
}); | |
var deathsPercentagesLabels = g.append("g") | |
.selectAll(".percentage-label") | |
.data(deathsCategories) | |
.enter().append("text") | |
.attr("class", "percentage-label") | |
.attr("x", width + 9) | |
.attr("y", function (d) { | |
return yDeaths(d); | |
}) | |
.attr("dy", "1.8em") | |
.style("text-anchor", "start") | |
.text(function (d) { | |
return deathsPercentages[currentSchedule][d] + "%"; | |
}); | |
// Add nodes | |
var nodes = data.map(function (d) { | |
var category = d.schedules[currentSchedule]; | |
return { | |
series: d.series, | |
category: category, | |
schedules: d.schedules, | |
x: x(d.series) + Math.random(), | |
y: d.series === "visit" ? yVisits(category) + Math.random() : yDeaths(category) + Math.random(), | |
radius: radius, | |
color: d.series === "visit" ? colorVisits(category) : colorDeaths(category) | |
}; | |
}); | |
// Add circles | |
var circle = g.selectAll("circle") | |
.data(nodes) | |
.enter().append("circle") | |
.attr("class", "node") | |
.attr("cx", function (d) { | |
return d.x; | |
}) | |
.attr("cy", function (d) { | |
return d.y; | |
}) | |
.attr("r", function (d) { | |
return d.radius; | |
}) | |
.style("fill", function (d) { | |
return d.color; | |
}); | |
simulation.nodes(nodes); | |
for (var i = 0; i < 120; i++) { | |
simulation.tick(); // Initial ticks to poisition nodes | |
} | |
simulation.alphaTarget(alpha).restart(); | |
timeout = setTimeout(timer, duration); | |
function timer() { | |
currentSchedule = currentSchedule === ageGroups.length - 1 ? 0 : currentSchedule + 1; | |
// Update age group label | |
ageGroupLabel.text(ageGroups[currentSchedule]); | |
// Update percentage labels | |
visitsPercentagesLabels.text(function (d) { | |
return visitsPercentages[currentSchedule][d] + "%"; | |
}); | |
deathsPercentagesLabels.text(function (d) { | |
return deathsPercentages[currentSchedule][d] + "%"; | |
}); | |
// Update the nodes | |
nodes.forEach(function (d) { | |
d.category = d.schedules[currentSchedule]; | |
d.color = getColor(d); | |
}); | |
// Update the force | |
simulation | |
.force("x", d3.forceX().strength(strength).x(getClusterX)) | |
.force("y", d3.forceY().strength(strength).y(getClusterY)); | |
simulation.restart(); | |
timeout = setTimeout(timer, duration); | |
} | |
function ticked() { | |
circle | |
.attr("cx", function (d) { | |
return d.x; | |
}) | |
.attr("cy", function (d) { | |
return d.y; | |
}) | |
.style("fill", function (d) { | |
return d.color; | |
}); | |
} | |
// Largest reminder method to make sure all numbers add up to 100 | |
// https://stackoverflow.com/a/13483710/7612054 | |
function countsToPercentages(data, categories) { | |
return data.map(function (d) { | |
var total = categories.reduce(function (total, current) { | |
return total += d[current]; | |
}, 0); | |
var decimals = {}; | |
// 1. Rounding everything down | |
categories.forEach(function (e) { | |
var percentage = d[e] / total * 100; | |
var integer = Math.floor(percentage); | |
decimals[e] = percentage - integer; | |
d[e] = integer; | |
}) | |
// 2. Getting the difference in sum and 100 | |
var difference = 100 - categories.reduce(function (total, current) { | |
return total += d[current]; | |
}, 0); | |
// 3. Distributing the difference by adding 1 to items in decreasing order of their decimal parts | |
var categoriesCopy = categories.slice(); | |
categoriesCopy.sort(function (a, b) { | |
return decimals[b] - decimals[a]; | |
}) | |
for (var i = 0; i < difference; i++) { | |
d[categoriesCopy[i]] += 1; | |
} | |
return d; | |
}); | |
} | |
function percentagesToSequences(data, categories) { | |
var squences = []; | |
for (var i = 0; i < 100; i++) { | |
squences[i] = []; | |
} | |
data.forEach(function(d) { | |
var i = 0; | |
categories.forEach(function (e) { | |
for (var j = 0; j < d[e]; j++) { | |
squences[i + j].push(e); | |
} | |
i += j; | |
}) | |
}); | |
return squences; | |
} | |
function getClusterX(d) { | |
return x(d.series); | |
} | |
function getClusterY(d) { | |
return d.series === "visit" ? yVisits(d.category) : yDeaths(d.category); | |
} | |
function getColor(d) { | |
return d.series === "visit" ? colorVisits(d.category) : colorDeaths(d.category); | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment