Rent Affordability in the USA (D3js - Tutorial Part 2)

Overview

So far, we have covered collecting a few datasets, "cleansing" them, and combining them to hold the pertinent information needed. The second part of this tutorial will cover the D3 code behind the creation of the map visualization.

Variables
// Define the width and height of our map
var width = 960,
    height = 500;

// This heat map is colored using a D3js threshold scale. 
// The two variables below are specifically for 
// the legend of the visualization
  var ext_color_domain = [0, 20, 40, 60, 80, 100, 120]
  var legend_labels = ["< 20%", "20%+", "40%+", "60%+", "80%+", "100%+", "120%+"]

/* Creating the threshold scale for the % 
// of Minimum Wage Income based in increments of 20. // Ex: < 20% will be colored #fee5d9 
// while 65% will be colored #fb6a4a */
    var color = d3.scale.threshold()
        .domain([20, 40, 60, 80, 100, 120, 200])
        .range(["#fee5d9", "#fcbba1", "#fc9272", "#fb6a4a", "#de2d26", "#a50f15", "#99000d"]);

Remember, a D3js scale takes a domain (input) and returns the range (output). In this case, we are setting up a scale to take the % of Minimum Wage Income and assigning it to an output color code.

Set-up the SVG and Tooltip
// Define a div for the tooltip    
var div = d3.select("body").append("div")
        .attr("class", "tooltip")
        .style("opacity", 0);
// Creates a new geographic path for GEO/TopoJSON
    var path = d3.geo.path();

// Define the svg for the map using the height and width defined earlier
    var svg = d3.select("body").append("svg")
        .attr("width", width)
        .attr("height", height);
Consume the data files

You should have 2 data files for this visualization. One of which being the TopoJSON and another being the census rent dataset with the derived minimum wage values and percent of income.

We will use d3.queue() which is a small library that allows you to execute asynchronous calls with configurable concurrency. Basically, you can load your data files before any of the other d3js code kicks off.


// begin a queue process
queue()
   // first load the topoJSON
   .defer(d3.json, "us_counties.json")
   // load the rent data
   .defer(d3.csv, "rent_data_production2.csv")
   // call the 'ready' function when complete
   .await(ready);


    function ready(error, topology, rent) {
        if (error) throw error;

     // topology is now us_counties.json object
    // rent is the rent_data_production2.csv 
   // object

    }

Now let's focus on everything within the 'ready' function. There are two data points we need to focus on. One being the % of MW Income value so that we can send it to the color threshold scale we created earlier and thus, color our map appropriately. The other data point we want to think about is what we want displayed when we hover over a county on our map. The TopoJSON object we are consuming does not have County Name as an attribute, and therefore, we will need to derive it from our Rent Data CSV. We do this in the below code:

 function ready(error, topology, rent) {
        if (error) throw error;

        // Create empty object for holding % of income figure
        var rateById = {}; 
        // Empty object for Name of County since new TopoJSON doesn't have it
        var nameById = {}; 

        // match on FIPS code to assign heatmap color
        // FIPS = county ID in our rent datasets
        rent.forEach(function(d) {

             // This "loop" says for every row in our rent CSV, assign the 
            // % of income value to the ID of the county
            rateById[+d.FIPS] = +d.TWO_BR_PERC_INCOME;
            // also assign the County Name to the id of the county in the 
            // namebyID object.
            nameById[+d.FIPS] = d.COUNTY;
        });
}

The + before d.FIPS ensures the value is converted to a numeric value which makes matching or joining on the value more clear and less error prone. I encourage you to console.log your rateByID and nameByID objects to see what this function is doing. You should see objects similar to the below with the pattern:
countyID : <value>

rateByID:

nameByID:

The rest of the code within our "ready" function will draw the paths to create the map, fill the counties with the appropriate color, and establish "mouseover" and "mouseout" events.

   svg.selectAll("path")
            // the line below is most important for d3 to draw the map correctly.
            // you can console.log(topology) to see the object hierarchy, in this case
            // "topology.objects.counties" but will vary based on other TopoJSON files
            .data(topojson.feature(topology, topology.objects.counties).features)
            .enter().append("path")
            .attr("d", path)
            // call our 'color' scale and send the % MW income value assigned to 
            // the particular county ID
            .style("fill", function(d) {
                return color(rateById[d.id])
            })
            // let's see the column name and exact MW % of income value
            .on("mouseover", function(d) {
                d3.select(this).transition().duration(300).style("opacity", 1);
                div.transition().duration(300)
                    .style("opacity", 1)
                    // call the name of the county based on the county ID and the MW income %
                div.text(nameById[d.id] + " - " + rateById[d.id] + "%")
                    .style("left", (d3.event.pageX) + "px")
                    .style("top", (d3.event.pageY - 30) + "px");
            })
            .on("mouseout", function() {
                d3.select(this)
                    .transition().duration(300)
                    .style("opacity", 0.8)
                div.transition().duration(300)
                    .style("opacity", 0);
            });

The last bit of code is outside of the "ready" function and is used to build a legend for the colors of the heat map.

    //Adding legend for our HeatMap

    var legend = svg.selectAll("g.legend")
        // use our colors just for the legend
        .data(ext_color_domain)
        .enter().append("g")
        .attr("class", "legend");

    var ls_w = 20,
        ls_h = 20;

    legend.append("rect")
        .attr("x", 20)
        .attr("y", function(d, i) {
            return height - (i * ls_h) - 4 * ls_h;
        })
        .attr("width", ls_w)
        .attr("height", ls_h)
        .style("fill", function(d, i) {
            return color(d);
        })
        .style("opacity", 0.8);

    legend.append("text")
        .attr("x", 50)
        .attr("y", function(d, i) {
            return height - (i * ls_h) - ls_h - 46;
        })
        .text(function(d, i) {
            return legend_labels[i];
        });

That pretty much sums up the D3js code behind the visualization. All of the files are available @ my GitHub (including the CSS and HTML code).

Rachel Rinaldi

Read more posts by this author.

San Antonio, TX