Skip to content

Instantly share code, notes, and snippets.

@kirkhunter
Last active May 15, 2016 01:43
Show Gist options
  • Save kirkhunter/0ead72f65bde18862a93ffae8789cace to your computer and use it in GitHub Desktop.
Save kirkhunter/0ead72f65bde18862a93ffae8789cace to your computer and use it in GitHub Desktop.
US Healthcare Data Dashboard

Final Project

The original data set can be found here: https://data.cms.gov/Medicare/Inpatient-Prospective-Payment-System-IPPS-Provider/97k6-zzx3

Thesis

My dashboard first communicates the average total covered healthcare charges across the US. This allows the viewer to see how the covered charges varies among the states.

The first image in the dashboard is the bar chart, which shows a selection of the US states and shows median covered charges per state. Hovering over an individual bar (which corresponds to one US state) alters the pie chart beside the bars. The pie chart then shows the minimum, median, and maximum average covered charges among all hospitals across the state.

The final image in the dashboard is a map that communicates with a color gradient the median covered charges across all US states.

Process

The data set used for the final project is from data.gov and it communicates healthcare financial information among hospitals across the US. To make the dashboard I aggregated the average covered charges by state, considering minimum, median, and maximum covered charges among hospitals.

In my EDA process I considered visualizing data for one state, perhaps creating a dashboard specific to California hospitals. Ultimately I chose to visualize healthcare data from across the US because I felt it was more compelling to allow the viewer to get a sense of which states have more or less average charges covered.

The visual encodings are through bars, color gradient, pie chart, annotation, and a legend. The bars are an important encoding of the data because it gives the viewer a sense of how median covered charges vary by state as well as an exact measure via annotation. The pie chart gives a visual depiction of the difference between minimum, maximum, and median covered charges among each state as well as in total across all US states. Finally, the map and color gradient give the viewer a chance to visually compare these covered charges among each state.

The interactive elements of the dashboard include hover effects on the bar chart, pie chart, and map. The viewer can hover over a given bar which corresponds to a US state and this updates the pie chart with this state’s data. hovering over the sections of the pie chart updates the bars. By default the bars show the median covered charges for each state. By hovering over a section of the pie chart updates the bars to show that category (minimum, median, maximum) for each state. Finally, hovering over a given state on the map gives the exact value of the median covered charges.

Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
h2.title {
color: black;
text-align: center;
}
.categories {
fill: none;
stroke: #fff;
stroke-linejoin: round;
}
.categories-choropleth {
fill: #ccc;
}
#tooltip-container {
position: absolute;
background-color: #fff;
color: #000;
padding: 10px;
border: 1px solid;
display: none;
}
#canvas svg {
border: 0px;
}
.tooltip_key {
font-weight: bold;
}
.tooltip_value {
margin-left: 20px;
float: right;
}
.x-axis {
fill: #000;
}
.chart {
background: #fff;
margin: 5px;
}
.chart .category-bar {
stroke: white;
fill: steelblue;
}
.chart text.name {
fill: #000;
}
.chart line {
stroke: #c1c1c1;
}
.chart .rule {
fill: #000;
}
.main-category-text {
fill: #FF4A4A;
}
.main-category-bar {
stroke: #FF4A4A;
stroke-width: 2px;
}
path { stroke: #fff; }
path:hover { opacity:0.9; }
rect:hover { fill:steelblue; }
.axis { font: 10px sans-serif; }
.legend tr{ border-bottom:1px solid grey; }
.legend tr:first-child{ border-top:1px solid grey; }
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.x.axis path { display: none; }
.legend{
margin-bottom:76px;
display:inline-block;
border-collapse: collapse;
border-spacing: 0px;
}
.legend td{
padding:4px 5px;
vertical-align:bottom;
}
.legendFreq, .legendPerc{
align:right;
width:50px;
}
</style>
<body><h2 text="Healthcare Average Covered Charges (in thousands of dollars)" class="title"></h2></body>
<body id="dashboard"></body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.12/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.20/topojson.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script type="text/javascript">
d3.select("body")
.append("h2")
.text("Healthcare Average Covered Charges (in thousands of dollars)")
.attr('class', 'title');
function dashboard(id, fData){
var width = 1060, height = 500;
var barColor = 'lightblue';
function segColor(c){ return {low:"#84d561", mid:"#e9916c",high:"#bc7fd1"}[c]; }
// compute total for each state.
fData.forEach(function(d){d.total=d.freq.low+d.freq.mid+d.freq.high;});
// function to handle histogram.
function histoGram(fD){
var hG={}, hGDim = {t: 60, r: 0, b: 30, l: 0};
hGDim.w = 500 - hGDim.l - hGDim.r,
hGDim.h = 300 - hGDim.t - hGDim.b;
//create svg for histogram.
var hGsvg = d3.select(id).append("svg")
.attr("width", hGDim.w + hGDim.l + hGDim.r)
.attr("height", hGDim.h + hGDim.t + hGDim.b).append("g")
.attr("transform", "translate(" + hGDim.l + "," + hGDim.t + ")");
// create function for x-axis mapping.
var x = d3.scale.ordinal().rangeRoundBands([0, hGDim.w], 0.1)
.domain(fD.map(function(d) { return d[0]; }));
// Add x-axis to the histogram svg.
hGsvg.append("g").attr("class", "x axis")
.attr("transform", "translate(0," + hGDim.h + ")")
.call(d3.svg.axis().scale(x).orient("bottom"));
// Create function for y-axis map.
var y = d3.scale.linear().range([hGDim.h, 0])
.domain([0, d3.max(fD, function(d) { return d[1]; })]);
// Create bars for histogram to contain rectangles and freq labels.
var bars = hGsvg.selectAll(".bar").data(fD).enter()
.append("g").attr("class", "bar");
//create the rectangles.
bars.append("rect")
.attr("x", function(d) { return x(d[0]); })
.attr("y", function(d) { return y(d[1]); })
.attr("width", x.rangeBand())
.attr("height", function(d) { return hGDim.h - y(d[1]); })
.attr('fill',barColor)
.on("mouseover",mouseover)// mouseover is defined below.
.on("mouseout",mouseout);// mouseout is defined below.
//Create the frequency labels above the rectangles.
bars.append("text").text(function(d){ return d3.format(",")(d[1])})
.attr("x", function(d) { return x(d[0])+x.rangeBand()/2; })
.attr("y", function(d) { return y(d[1])-5; })
.attr("text-anchor", "middle");
function mouseover(d){ // utility function to be called on mouseover.
// filter for selected state.
var st = fData.filter(function(s){ return s.State == d[0];})[0],
nD = d3.keys(st.freq).map(function(s){ return {type:s, freq:st.freq[s]};});
// call update functions of pie-chart and legend.
pC.update(nD);
leg.update(nD);
}
function mouseout(d){ // utility function to be called on mouseout.
// reset the pie-chart and legend.
pC.update(tF);
leg.update(tF);
}
// create function to update the bars. This will be used by pie-chart.
hG.update = function(nD, color){
// update the domain of the y-axis map to reflect change in frequencies.
y.domain([0, d3.max(nD, function(d) { return d[1]; })]);
// Attach the new data to the bars.
var bars = hGsvg.selectAll(".bar").data(nD);
// transition the height and color of rectangles.
bars.select("rect").transition().duration(500)
.attr("y", function(d) {return y(d[1]); })
.attr("height", function(d) { return hGDim.h - y(d[1]); })
.attr("fill", color);
// transition the frequency labels location and change value.
bars.select("text").transition().duration(500)
.text(function(d){ return d3.format(",")(d[1])})
.attr("y", function(d) {return y(d[1])-5; });
}
return hG;
}
// function to handle pieChart.
function pieChart(pD){
var pC ={}, pieDim ={w:250, h: 250};
pieDim.r = Math.min(pieDim.w, pieDim.h) / 2;
// create svg for pie chart.
var piesvg = d3.select(id).append("svg")
.attr("width", pieDim.w).attr("height", pieDim.h).append("g")
.attr("transform", "translate("+pieDim.w/2+","+pieDim.h/2+")");
// create function to draw the arcs of the pie slices.
var arc = d3.svg.arc().outerRadius(pieDim.r - 10).innerRadius(0);
// create a function to compute the pie slice angles.
var pie = d3.layout.pie().sort(null).value(function(d) { return d.freq; });
// Draw the pie slices.
piesvg.selectAll("path").data(pie(pD)).enter().append("path").attr("d", arc)
.each(function(d) { this._current = d; })
.style("fill", function(d) { return segColor(d.data.type); })
.on("mouseover",mouseover).on("mouseout",mouseout);
// create function to update pie-chart. This will be used by histogram.
pC.update = function(nD){
piesvg.selectAll("path").data(pie(nD)).transition().duration(500)
.attrTween("d", arcTween);
}
// Utility function to be called on mouseover a pie slice.
function mouseover(d){
// call the update function of histogram with new data.
hG.update(fData.map(function(v){
return [v.State,v.freq[d.data.type]];}),segColor(d.data.type));
}
//Utility function to be called on mouseout a pie slice.
function mouseout(d){
// call the update function of histogram with all data.
hG.update(fData.map(function(v){
return [v.State,v.total];}), barColor);
}
// Animating the pie-slice requiring a custom function which specifies
// how the intermediate paths should be drawn.
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) { return arc(i(t)); };
}
return pC;
}
// function to handle legend.
function legend(lD){
var leg = {};
// create table for legend.
var legend = d3.select(id).append("table").attr('class','legend');
// create one row per segment.
var tr = legend.append("tbody").selectAll("tr").data(lD).enter().append("tr");
// create the first column for each segment.
tr.append("td").append("svg").attr("width", '16').attr("height", '16').append("rect")
.attr("width", '16').attr("height", '16')
.attr("fill",function(d){ return segColor(d.type); });
// create the second column for each segment.
tr.append("td").text(function(d){ return d.type;});
// create the third column for each segment.
tr.append("td").attr("class",'legendFreq')
.text(function(d) { return d3.format(",")(d.freq); });
// create the fourth column for each segment.
tr.append("td").attr("class",'legendPerc')
.text(function(d){ return getLegend(d,lD);});
// Utility function to be used to update the legend.
leg.update = function(nD){
// update the data attached to the row elements.
var l = legend.select("tbody").selectAll("tr").data(nD);
// update the frequencies.
l.select(".legendFreq").text(function(d){ return d3.format(",")(d.freq);});
// update the percentage column.
l.select(".legendPerc").text(function(d){ return getLegend(d,nD);});
}
function getLegend(d,aD){ // Utility function to compute percentage.
return d3.format("%")(d.freq/d3.sum(aD.map(function(v){ return v.freq; })));
}
return leg;
}
// calculate total frequency by segment for all state.
var tF = ['low','mid','high'].map(function(d) {
return {type:d, freq: d3.sum(fData.map(function(t) { return t.freq[d]; }))};
});
// calculate total frequency by state for all segment.
var sF = fData.map(function(d){return [d.State, d.total];});
var hG = histoGram(sF), // create the histogram.
pC = pieChart(tF), // create the pie-chart.
leg= legend(tF); // create the legend.
};
function draw(data) {
// "use strict";
// console.log(data)
var WIDTH = 800, HEIGHT = 400;
var COLOR_COUNTS = 9;
var SCALE = 0.7;
var config = {
"main_category": "Washington",
"avg_category": "Nation Average",
"color1": '#c3e2ff',
"color2": '#08306B',
"stateDataColumn": "provider_state",
"valueDataColumn": "med_average_covered_charges"
};
var MAIN_CATEGORY = config.mainCategory;
var AVG_CATEGORY = config.averageCategory;
function Interpolate(start, end, steps, count) {
var s = start,
e = end,
final = s + (((e - s) / steps) * count);
return Math.floor(final);
};
function Color(_r, _g, _b) {
var r, g, b;
var setColors = function(_r, _g, _b) {
r = _r;
g = _g;
b = _b;
};
setColors(_r, _g, _b);
this.getColors = function() {
var colors = {
r: r,
g: g,
b: b
};
return colors;
};
};
function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
};
var COLOR_FIRST = config.color1, COLOR_LAST = config.color2;
var rgb = hexToRgb(COLOR_FIRST);
var COLOR_START = new Color(rgb.r, rgb.g, rgb.b);
rgb = hexToRgb(COLOR_LAST);
var COLOR_END = new Color(rgb.r, rgb.g, rgb.b);
var MAP_CATEGORY = config.stateDataColumn;
var MAP_VALUE = config.valueDataColumn;
var width = WIDTH,
height = HEIGHT;
var valueById = d3.map();
var startColors = COLOR_START.getColors(),
endColors = COLOR_END.getColors();
var colors = [];
for (var i = 0; i < COLOR_COUNTS; i++) {
var r = Interpolate(startColors.r, endColors.r, COLOR_COUNTS, i);
var g = Interpolate(startColors.g, endColors.g, COLOR_COUNTS, i);
var b = Interpolate(startColors.b, endColors.b, COLOR_COUNTS, i);
colors.push(new Color(r, g, b));
};
var quantize = d3.scale.quantize()
.domain([0, 1.0])
.range(d3.range(COLOR_COUNTS).map(function(i) { return i }));
var path = d3.geo.path();
var svg = d3.select("#canvas-svg").append("svg")
.attr("width", width)
.attr("height", height);
d3.tsv("https://s3-us-west-2.amazonaws.com/vida-public/geo/us-state-names.tsv", function(error, names) {
// console.log(names);
name_id_map = {};
id_name_map = {};
for (var i = 0; i < names.length; i++) {
name_id_map[names[i].name] = names[i].id;
id_name_map[names[i].id] = names[i].name;
};
console.log(name_id_map);
console.log(id_name_map);
data.forEach(function(d) {
var id = name_id_map[d[MAP_CATEGORY]];
valueById.set(id, +d[MAP_VALUE]);
});
quantize.domain([d3.min(data, function(d){ return +d[MAP_VALUE] }),
d3.max(data, function(d){ return +d[MAP_VALUE] })]);
function makeMap(us) {
svg.append("g")
.attr("class", "categories-choropleth")
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("transform", "scale(" + SCALE + ")")
.style("fill", function(d) {
if (valueById.get(d.id)) {
var i = quantize(valueById.get(d.id));
var color = colors[i].getColors();
return "rgb(" + color.r + "," + color.g +
"," + color.b + ")";
} else {
return "";
}
})
.attr("d", path)
.on("mousemove", function(d) {
console.log(d);
var html = "";
html += "<div class=\"tooltip_kv\">";
html += "<span class=\"tooltip_key\">";
html += id_name_map[d.id];
html += "</span>";
html += "<span class=\"tooltip_value\">";
html += (valueById.get(d.id) ? (Math.round(+valueById.get(d.id) / 10.) / 100) : "");
html += "";
html += "</span>";
html += "</div>";
$("#tooltip-container").html(html);
$(this).attr("fill-opacity", "0.8");
$("#tooltip-container").show(); console.log(html);
var coordinates = d3.mouse(this);
var map_width = $('.categories-choropleth')[0].getBoundingClientRect().width;
if (d3.event.layerX < map_width / 2) {
d3.select("#tooltip-container")
.style("top", (d3.event.layerY + 15) + "px")
.style("left", (d3.event.layerX + 15) + "px");
} else {
var tooltip_width = $("#tooltip-container").width();
d3.select("#tooltip-container")
.style("top", (d3.event.layerY + 15) + "px")
.style("left", (d3.event.layerX - tooltip_width - 30) + "px");
}
})
.on("mouseout", function() {
$(this).attr("fill-opacity", "1.0");
$("#tooltip-container").hide();
});
svg.append("path")
.datum(topojson.mesh(us, us.objects.states, function(a, b) { return a !== b; }))
.attr("class", "categories")
.attr("transform", "scale(" + SCALE + ")")
.attr("d", path);
};
d3.json("https://s3-us-west-2.amazonaws.com/vida-public/geo/us.json", function(error, us) {
makeMap(us);
});
});
};
d3.csv("https://raw.githubusercontent.com/KirkHunter/KirkHunter.github.io/master/health.csv", draw);
var freqData=[
{'State': 'AK', 'freq': {'high': 191.0, 'low': 5.0, 'mid': 31.0}},
{'State': 'AL', 'freq': {'high': 460.0, 'low': 3.0, 'mid': 22.0}},
{'State': 'CT', 'freq': {'high': 254.0, 'low': 4.0, 'mid': 23.0}},
{'State': 'DE', 'freq': {'high': 140.0, 'low': 9.0, 'mid': 20.0}},
{'State': 'GA', 'freq': {'high': 342.0, 'low': 4.0, 'mid': 22.0}},
{'State': 'IN', 'freq': {'high': 226.0, 'low': 5.0, 'mid': 21.0}},
{'State': 'LA', 'freq': {'high': 342.0, 'low': 3.0, 'mid': 24.0}},
{'State': 'MD', 'freq': {'high': 101.0, 'low': 3.0, 'mid': 10.0}},
{'State': 'MO', 'freq': {'high': 234.0, 'low': 4.0, 'mid': 23.0}},
{'State': 'MS', 'freq': {'high': 350.0, 'low': 3.0, 'mid': 22.0}},
{'State': 'ND', 'freq': {'high': 104.0, 'low': 5.0, 'mid': 15.0}},
{'State': 'NE', 'freq': {'high': 354.0, 'low': 7.0, 'mid': 23.0}},
{'State': 'NV', 'freq': {'high': 401.0, 'low': 11.0, 'mid': 43.0}},
{'State': 'PA', 'freq': {'high': 614.0, 'low': 4.0, 'mid': 27.0}},
{'State': 'SD', 'freq': {'high': 196.0, 'low': 6.0, 'mid': 21.0}},
{'State': 'VT', 'freq': {'high': 94.0, 'low': 7.0, 'mid': 15.0}},
{'State': 'WI', 'freq': {'high': 243.0, 'low': 5.0, 'mid': 19.0}}
];
dashboard('#dashboard',freqData);
</script>
<body>
<div id="tooltip-container"></div>
<div id="canvas-svg"></div>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment