Back to Top

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"); }); }); });