Skip to content

Instantly share code, notes, and snippets.

@Zhenmao
Created March 15, 2018 13:20
Show Gist options
  • Save Zhenmao/8b2fa8d5228406dbec77801f7204da39 to your computer and use it in GitHub Desktop.
Save Zhenmao/8b2fa8d5228406dbec77801f7204da39 to your computer and use it in GitHub Desktop.
TBI-related Emergency Department Visits & Deaths
<!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