How can I nest GeoJSON / TopoJSON geometries OR nest the generated paths with D3?

819 views Asked by At

Problem:

I'm attempting to create an interactive map of the US in which state, county and national boundaries are displayed. Counties are shaded based on data, and hovering over a state should highlight all counties in the state, and the state should be clickable. I want to achieve this by having a SVG with the county shapes inside of state shapes, inside of a US shape.

I can generate a county map based on a CENSUS county shape file, and I can shade the states based on data in an external CSV by prepping the file with TopoJSON command line and using the following code in D3:

<!DOCTYPE html>
<meta charset="utf-8">
<style>

path {
  fill: none;
  stroke-linejoin: round;
  stroke-linecap: round;
}

</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script>

var width = 960,
    height = 600;

var path = d3.geo.path()
    .projection(d3.geo.albersUsa());

var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height);

d3.json("counties_pa.json", function(error, us) {
  if (error) return console.error(error);

var color = d3.scale.threshold()
    .domain([1, 10, 50, 100, 500, 1000, 2000, 5000])
    .range(["#fff7ec", "#fee8c8", "#fdd49e", "#fdbb84", "#fc8d59", "#ef6548", "#d7301f", "#b30000", "#7f0000"]);



svg.append('g').attr('class','counties').selectAll("path").data(topojson.feature(us, us.objects.cb_2014_us_county_20m).features).enter().append('path').attr('d',path).attr('style',function(d){return 'fill:'+color(d.properties.population / d.properties.area * 2.58999e6);});

});



</script>

This is mostly visually acceptable (except it doesn't have discrete state / national boundaries) - but is functionally inadequate. In order to apply CSS to the counties on a state hover, the counties need to be within a state shape, or grouped somehow.

What i've tried:

  • Using topojson-merge in the command line to merge the counties into state shapes, and then render the state shapes separately - this helps with having discrete state borders - but I haven't figured a way to nest the counties into the respective state shapes.

What i'm working out now:

  • Somehow combining a state TopoJSON file and a county TopoJSON file and nesting the counties in the states, then rendering with D3.

  • Somehow using d3 to take non-nested state and county data and just nest it on the client on the client level.

In the end I would like to learn about the most effective and quickest rendering process to achieve my desired functionality.

Thanks for your help in advance.

1

There are 1 answers

5
Ben Lyall On BEST ANSWER

I took a punt on your data sources, and here is what it looks like you're trying to achieve: http://bl.ocks.org/benlyall/55bc9474e6d531a1c1fe

Basically, I have generated a TopoJSON file using the following command line:

topojson -o counties_pa.json --id-property=+GEOID -p -e POP01.txt --id-property=+STCOU -p population=+POP010210D,area=ALAND,state=+STATEFP,county=+COUNTYFP cb_2014_us_county_20m.shp cb_2014_us_state_20m.shp

Some explanation on this:

  • -o counties_pa.json sets the name of the output file
  • --id-property=+GEOID will use that property in the input file as the id of each output geometry
  • -p means include all properties from the input file
  • -e POP01.txt will pull external data in from the file POP01.txt. This file is a csv file generated from the POP01.xls spreadsheet available from http://www.census.gov/support/USACdataDownloads.html#POP
  • --id-property=+STCOU means that the id property from the external file (POP01.txt) is in the STCOU column. This is used to match up with matching ids in the input file (which are in the GEOID property as explained above)
  • -p population=+POP010210D,area=ALAND,state=+STATEFP,county=+COUNTYFP explicitly lists the properties that I want in the output file, so anything extra won't be included. POP010210D is the column name for the population as at the 2010 census, so I just used that for demonstration purposes.
  • cb_2014_us_county_20m.shp cb_2014_us_state_20m.shp are the two input files. One for county shapes and one for state shapes. They will each be added to the output file in seperate properties named after their filenames.

I did it this way, as you seemed to be colouring your county areas based on population density, so both population and area needed to be in the output file. The population was pulled from the POP01 spreadsheet and linked to each county based on the GEOID (which is just the state number concatentated with the county number).

I was just looking for a quick and easy way to recreate your dataset, and then add the state boundaries to it so I could post the answer. Not sure how closely this matches your original data, but it seems to work for demonstration purposes.

From that, I took your code above and updated it to:

<!DOCTYPE html>
<meta charset="utf-8">
<style>

path {
  fill: none;
  stroke-linejoin: round;
  stroke-linecap: round;
}

path.state {
    fill: none;
    stroke: black;
    stroke-width: .5px;
}

</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script>

var width = 960,
    height = 600;

var path = d3.geo.path()
    .projection(d3.geo.albersUsa());

var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height);

d3.json("counties_pa.json", function(error, us) {
  if (error) return console.error(error);

var color = d3.scale.threshold()
    .domain([1, 10, 50, 100, 500, 1000, 2000, 5000])
    .range(["#fff7ec", "#fee8c8", "#fdd49e", "#fdbb84", "#fc8d59", "#ef6548", "#d7301f", "#b30000", "#7f0000"]);

    svg.append('g')
        .attr('class','counties')
        .selectAll("path")
      .data(topojson.feature(us, us.objects.cb_2014_us_county_20m).features).enter()
        .append('path')
        .attr('d', path)
        .attr("id", function(d) { return "county-" + d.id; })
        .attr("data-state", function(d) { return d.properties.state; })
        .attr('style',function(d) { 
            return 'fill:'+color(d.properties.population / d.properties.area * 2.58999e6);
        })
        .on("mouseover", hoverCounty)
        .on("mouseout", outCounty);

    svg.append('g')
        .attr('class', 'states')
        .selectAll("path")
      .data(topojson.feature(us, us.objects.cb_2014_us_state_20m).features).enter()
        .append("path")
        .attr("class", "state")
        .attr("id", function(d) { return "state-" + d.id; })
        .attr("d", path);
    });

function hoverCounty(county) {
    d3.selectAll("path[data-state='" + county.properties.state + "']").style("opacity", .5);
}

function outCounty(county) {
    d3.select(".counties").selectAll("path").style("opacity", null);
}

</script>

The new and interesting bits of code are:

  1. Add a data-state attribute to each county to determine which state it belongs to:

    .attr("data-state", function(d) { return d.properties.state; })
    
  2. Add the state boundaries (I combined states to the TopoJSON file in the topojson command line)

    svg.append('g')
        .attr('class', 'states')
        .selectAll("path")
      .data(topojson.feature(us, us.objects.cb_2014_us_state_20m).features).enter()
        .append("path")
        .attr("class", "state")
        .attr("id", function(d) { return "state-" + d.id; })
        .attr("d", path);
    });
    
  3. Added hover handlers so you can see how I'm determining the grouping of counties into states:

    function hoverCounty(county) {
        d3.selectAll("path[data-state='" + county.properties.state + "']").style("opacity", .5);
    }
    
    function outCounty(county) {
        d3.select(".counties").selectAll("path").style("opacity", null);
    }
    
  4. Tied these hover handlers to each county so they get executed at the appropriate times:

    .on("mouseover", hoverCounty)
    .on("mouseout", outCounty);