Indicators Map
/* the map shading that we borrowed from color-brewer */
.shade0-6 {
fill: rgb(179, 88, 6);
}
.shade1-6 {
fill: rgb(241, 163, 64);
}
.shade2-6 {
fill: rgb(254, 224, 182);
}
.shade3-6 {
fill: rgb(216, 218, 235);
}
.shade4-6 {
fill: rgb(153, 142, 195);
}
.shade5-6 {
fill: rgb(84, 39, 136);
}
.highlighted {
fill: yellow;
}
.selected {
fill: pink
}
/* the title at the top */
#overalltitle {
font-family: sans-serif
}
/* the scale */
.scalelabel {
font-family: sans-serif;
text-anchor: middle;
font-size: 15px
}
/* for the infopanel labels
ipheader2b is a left-anchored version of ipheader2
*/
.ipheader1,
.ipheader2b,
.ipheader3 {
font-family: sans-serif
}
.ipheader1,
.ipheader2 {
text-anchor: middle
}
.ipheader1 {
font-size: 25px
}
.ipheader2,
.ipheader2b {
font-size: 17px
}
.ipheader3 {
font-size: 15px
}
/* now for the table below */
th.mainHeader {
width: 125px;
background-color: white;
}
td.value {
text-align: center;
}
Displaying OverallPovertyRacial DisparityImmigrant Exclusion index with scale at right.
Click a state for more detail. More information about the methodology.
Below Average Above State Overall Poverty Racial Disparity Immigrant Exclusion State Poverty (Lowest Income Quartile) Average Annual Income Per Household: Share of Persons without Health Insurance: Percent of Renter Households with High Housing Cost Burden: Racial Disparity Percent of Segregated Schools: White-Minority Wage Gap: White-Minority Unemployment Gap: Immigrant Exclusion Disconnected Immigrant Youth Rate: Immigrants with Difficulty Speaking English: Health Insurance Gap between Native-born and Immigrants:
var svg = d3.select("svg");
var mapGroup = d3.select("#mapGroup");
var ipGroup = d3.select("#ipGroup");
var ipGroup2 = d3.select("#ipGroup2");
// a scale built for our data set
// that uses a color scheme that we got from colorbrewer2
var overallThresholds = [0.540421054, 0.604559104, 0.625938453, 0.647317803, 0.711455852];
var povertyThresholds = [0.524509758, 0.595976441, 0.619798669, 0.643620897, 0.71508758];
var migrationThresholds = [0.4173349, 0.531180528, 0.569129071, 0.607077613, 0.720923242];
var raceThresholds = [0.601859716, 0.69230335, 0.722451227, 0.752599105, 0.843042739];
var shades = ["shade0-6", "shade1-6", "shade2-6", "shade3-6", "shade4-6", "shade5-6"];
// if we put the RGB values directly in here then the transition looks nice
var shadesRGB = ["rgb(177,88,6)", "rgb(241,163,64)", "rgb(254,224,182)",
"rgb(216,218,235)", "rgb(153,142,195)", "rgb(84,39,136)"
];
var selectedColor = "pink"; // pink
var highlightedColor = "yellow"; // yellow
var overallScale = d3.scale.threshold().domain(overallThresholds).range(shadesRGB);
var povertyScale = d3.scale.threshold().domain(povertyThresholds).range(shadesRGB);
var migrationScale = d3.scale.threshold().domain(migrationThresholds).range(shadesRGB);
var raceScale = d3.scale.threshold().domain(raceThresholds).range(shadesRGB);
// helpful to figure out if two things are the same color
// takes as input two color strings
// we use this as a cheap way below to tell if something is highlighted
// by comparing whatever garbage RGB string the browser puts out
// to our nice named colors below
// this protects us if, for example, one browsers puts spaces after the commas and another doesn't
function colorEquals(color1, color2) {
c1 = d3.rgb(color1);
c2 = d3.rgb(color2);
return (c1.r == c2.r && c1.g == c2.g && c1.b == c2.b);
}
// which we are coloring
// 0 - overall index
// 1 - poverty
// 2 - race
// 3 - migration
// 4 - name (used only for graph)
var whichColorMap = 0;
var selectedStatePath = null;
// a smart coloring function that uses the state above
// mapping a float value to a CSS class, into which the color information is encoded
function bgColor(d) {
if (whichColorMap == 0) {
return overallScale(d.overallIndex);
} else if (whichColorMap == 1) {
return povertyScale(d.povertyIndex);
} else if (whichColorMap == 2) {
return raceScale(d.raceIndex);
} else if (whichColorMap == 3) {
return migrationScale(d.migrationIndex);
} else {
console.log("whichColorMap in undefined state");
}
}
// for the two darkest shades, white looks best for the text
// otherwise return black
function fgColor(d) {
bg = bgColor(d);
if (bg == shadesRGB[0] || bg == shadesRGB[5]) {
return "white";
} else {
return "black";
}
}
// a wrapper to apply this function to the properties of a JSON feature
// where the data lives
function bgColorWrapper(d) {
return bgColor(d.properties);
}
// updating the info panel background
// also changes the text color depending,
// so it merits a separate function
function updateInfoPanel(d) {
console.log("updateInfoPanel: " + bgColor(d) + " " + fgColor(d));
d3.select("#ipRect").style("fill", bgColor(d));
d3.select("#ipRect2").style("fill", bgColor(d));
ipGroup.selectAll("text").style("fill", fgColor(d));
ipGroup2.selectAll("text").style("fill", fgColor(d));
}
// to power the combo box
d3.select("#indexSelect").on("change", function() {
// update the global state [which thing we are displaying]
whichColorMap = d3.select(this).property("selectedIndex");
// and recolor the entire map based on the argument
mapGroup.selectAll("path")
.transition().duration(1000)
.style("fill", function(d) {
return bgColorWrapper(d)
});
// including the box, if we are so interested
if (ipGroup.style("visibility") == "visible") {
updateInfoPanel(selectedStatePath.datum().properties);
}
// TODO: used to recolor the table; now this should update the bar graph
});
// load the US states JSON
// edit this later to make it dynamic -- for now we have x,y coordinates and a scale
var projection = d3.geo.albersUsa().translate([225, 150]).scale([550]);
var path = d3.geo.path().projection(projection);
d3.json("https://jsri.loyno.edu/indicatorsmap/us-states.json", function(json) {
// the fun part, where we bind the input data to the map
d3.csv("https://jsri.loyno.edu/indicatorsmap/stateindexsummary.csv", function(data) {
// merge the overall index and the GeoJSON so we can
// reference this data value as we colorcode our
for (var i = 0; i < data.length; i++) {
// Grab state name
var dataState = data[i].State;
// a linear search (ugh!) to find the corresponding state
//console.log("Initiating linear search for " + dataState + " over " + json.features.length + " features");
for (var j = 0; j < json.features.length; j++) {
var jsonState = json.features[j].properties.name;
//console.log("Looking for " + jsonState);
if (dataState == jsonState) {
// once we find the right state,
// embed the index indicators into the GeoJSON
// data structure to be used as properties
var thisProp = json.features[j].properties;
thisProp.overallIndex = parseFloat(data[i].OverallIndex);
thisProp.povertyIndex = parseFloat(data[i].PovertyIndex);
thisProp.migrationIndex = parseFloat(data[i].MigrationIndex);
thisProp.raceIndex = parseFloat(data[i].RaceIndex);
thisProp.overallRank = parseInt(data[i].OverallRank);
thisProp.povertyRank = parseInt(data[i].PovertyRank);
thisProp.migrationRank = parseInt(data[i].MigrationRank);
thisProp.raceRank = parseInt(data[i].RaceRank);
thisProp.povertyIncome = data[i].PovertyIncome;
thisProp.povertyHealthIns = data[i].PovertyHealthIns;
thisProp.povertyHousing = data[i].PovertyHousing;
thisProp.raceSchools = data[i].RaceSchools;
thisProp.raceEarnings = data[i].RaceEarnings;
thisProp.raceEmployment = data[i].RaceEmployment;
thisProp.immigrantsYouth = data[i].ImmigrantsYouth;
thisProp.immigrantsEnglish = data[i].ImmigrantsEnglish;
thisProp.immigrantsHealth = data[i].ImmigrantsHealth;
// cancel the search
break;
}
}
}
// nested inside two-layers of callback, the magic happens here
// as we bind the data and create one path for each GeoJSON feature
// with the data, first build the SVG output
mapGroup.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path)
.style("stroke", "black")
.style("fill", function(d) {
return bgColorWrapper(d)
})
.on("mouseover", function() {
thisPath = d3.select(this);
// weird things happen when you highlight something
// that has already been selected
if (!colorEquals(thisPath.style("fill"), selectedColor)) {
thisPath.style("fill", highlightedColor)
}
})
.on("mouseout", function(d) {
thisPath = d3.select(this);
if (colorEquals(thisPath.style("fill"), highlightedColor)) {
thisPath.style("fill", bgColorWrapper(d))
}
})
.on("click", function(d) {
// first unhighlight the previous state
if (selectedStatePath != null) {
// important -- we have to color based on the data attached to the path
// the datum has properties (our data) and the geometry data from the
// JSON input file
selectedStatePath.style("fill", bgColorWrapper(selectedStatePath.datum()))
}
// now color the new state
selectedStatePath = d3.select(this);
selectedStatePath.style("fill", selectedColor);
// prepare the info panel
var s = selectedStatePath.datum().properties;
d3.select("#ipStateName").text(s.name);
d3.select("#ipOverallRank").text("Rank: " + s.overallRank);
d3.select("#ipOverallScore").text("Score: " + s.overallIndex.toPrecision(3));
d3.select("#ipPovertyRank").text("Rank: " + s.povertyRank);
d3.select("#ipPovertyScore").text("Score: " + s.povertyIndex.toPrecision(3));
d3.select("#ipMigrationRank").text("Rank: " + s.migrationRank);
d3.select("#ipMigrationScore").text("Score: " + s.migrationIndex.toPrecision(3));
d3.select("#ipRaceRank").text("Rank: " + s.raceRank);
d3.select("#ipRaceScore").text("Score: " + s.raceIndex.toPrecision(3));
d3.select("#ip2StateName").text(s.name);
d3.select("#ipPoverty1").text(s.povertyIncome);
d3.select("#ipPoverty2").text(s.povertyHealthIns);
d3.select("#ipPoverty3").text(s.povertyHousing);
d3.select("#ipRace1").text(s.raceSchools);
d3.select("#ipRace2").text(s.raceEarnings);
d3.select("#ipRace3").text(s.raceEmployment);
d3.select("#ipMigration1").text(s.immigrantsYouth);
d3.select("#ipMigration2").text(s.immigrantsEnglish);
d3.select("#ipMigration3").text(s.immigrantsHealth);
// same background color as the state -- a nice touch
updateInfoPanel(s);
// display the info panel
ipGroup.style("visibility", "visible");
ipGroup2.style("visibility", "visible");
});
});
});