Search code examples
javascriptd3.jsdata-visualizationshapefiletopojson

Trying to Refactor Codebase with NY topojson file


I built an animated choropleth map using d3 which uses your typical usa topojson file (by county). The file can be found here:

https://d3js.org/us-10m.v1.json

My code works fine, however because my data is ny based, I would like to use just a ny map (by county), as opposed to the entire united states. Like the file here for example:

https://raw.githubusercontent.com/deldersveld/topojson/master/countries/us- 
states/NY-36-new-york-counties.json

However, when I replace the old file with the new one, I get the following error:

Uncaught ReferenceError: counties is not defined

I am assuming the error can be ultimately traced back to this code block:

   counties = svg.append("g")
    .attr("class", "counties")
    .selectAll("path")
    .data(topojson.feature(us, us.objects.counties).features)
    .enter()
    .append("path")
    .attr("d", path)
    .call(style,currentYear)

Specifically, this line:

.data(topojson.feature(us, us.objects.counties).features)

My assumption is because the shapefiles are slightly different, this line needs to be refactored somehow to be specific to this ny shapefile (or perhaps I'm wrong).

Anyways, here is my code. Any help would be immensely appreciated:

HTML

<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="https://d3js.org/queue.v1.min.js"></script>

<svg width="960" height="600"></svg>

CSS

div.tooltip {   
 position: absolute;           
 text-align: center; 
 vertical-align: middle;          
 width: auto;                 
 height: auto;                 
 padding: 2px;             
 font: 12px sans-serif;    
 color: white;    
 background: gray;   
 border: 0px;      
 border-radius: 8px;           
 pointer-events: none;         
}

.counties :hover {
 stroke: black;
 stroke-width: 2px;
}

.county-borders {
 fill: none;
 stroke: #fff;
 stroke-width: 0.5px;
 stroke-linejoin: round;
 stroke-linecap: round;
 pointer-events: none;
}

.year.label {
 font: 500 85px "Helvetica Neue";
 fill: gray;
}

.overlay {
 fill: none;
 pointer-events: all;
 cursor: ew-resize;
}

JS

choroplethMap();


function choroplethMap() {

 var svg = d3.select("svg");
 var path = d3.geoPath();
 var format = d3.format("");
 var height = 600;
 var width = 960;

 var colorScheme = d3.schemeReds[9];
 colorScheme.unshift("#eee");

 var color = d3.scaleQuantize()
  .domain([0, 20])
  .range(colorScheme);

 var x = d3.scaleLinear()
  .domain(d3.extent(color.domain()))
  .rangeRound([600,860]);

 var g = svg.append("g")
  .attr("transform", "translate(0,40)");

 g.selectAll("rect")
.data(color.range().map(function(d){ return color.invertExtent(d); }))
.enter()
.append("rect")
  .attr("height", 8)
  .attr("x", function(d){ return x(d[0]); })
  .attr("width", function(d){ return x(d[1]) - x(d[0]); })
  .attr("fill", function(d){ return color(d[0]); });

 g.append("text")
.attr("class", "caption")
.attr("x", x.range()[0])
.attr("y", -6)
.attr("fill", "#000")
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text("Unemployment Rate (%)");

g.call(d3.axisBottom(x)
.tickSize(13)
.tickFormat(format)
.tickValues(color.range().slice(1).map(function(d){ return color.invertExtent(d)[0]; 
})))
.select(".domain")
.remove();

var div = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);

// Add the year label; the value is set on transition.
var label = svg.append("text")
.attr("class", "year label")
.attr("text-anchor", "end")
.attr("y", 575)
.attr("x", 625)
.text(2013);

queue()
// .defer(d3.json, "https://d3js.org/us-10m.v1.json")
.defer(d3.json, 
"https://raw.githubusercontent.com/deldersveld/topojson/master/countries/us- 
states/NY-36-new-york-counties.json")
.defer(d3.csv, "../choropleth-ny.csv")
.await(ready);

function ready(error, us, unemployment) {
 if (error) throw error;

  // Initialize data to 1990
  var currentYear = 2013;

  // Add an overlay for the year label.
  var box = label.node().getBBox();

  var overlay = svg.append("rect")
    .attr("class", "overlay")
    .attr("x", box.x)
    .attr("y", box.y)
    .attr("width", box.width)
    .attr("height", box.height)
    .on("mouseover", enableInteraction);

  // Start a transition that interpolates the data based on year.
  svg.transition()
    .duration(25000)
    .ease(d3.easeLinear)
    .tween("year", tweenYear)
    //.each();

  counties = svg.append("g")
    .attr("class", "counties")
    .selectAll("path")
    .data(topojson.feature(us, us.objects.counties).features)
    .enter()
    .append("path")
    .attr("d", path)
    .call(style,currentYear)

  function style(counties, year){
    newunemployment = interpolateData(year);

  var rateById = {};
  var nameById = {};

  newunemployment.forEach(function(d) {
    var newcode = '';
    if (d.code.length < 5) {
      newcode = '0' + d.code;
      d.code = newcode;
    } 
    rateById[d.code] = +d.rate;
    nameById[d.code] = d.name;
  });
  
  counties.style("fill", function(d) { return color(rateById[d.id]); })
    .on("mouseover", function(d) {      
        div.transition()        
          .duration(200)      
          .style("opacity", .9);      
        div.html(nameById[d.id] + ' in ' + Math.round(currentYear) +': <br><strong>' 
        + rateById[d.id] + '%</strong>')
          .style("left", (d3.event.pageX) + "px")     
          .style("top", (d3.event.pageY - 28) + "px");})   
     // fade out tooltip on mouse out               
     .on("mouseout", function(d) {       
        div.transition()        
         .duration(500)      
         .style("opacity", 0);});
     }

  svg.append("path")
  .datum(topojson.mesh(us, us.objects.states, (a, b) => a !== b))
  .attr("fill", "none")
  .attr("stroke", "white")
  .attr("stroke-linejoin", "round")
  .attr("d", path);

// After the transition finishes, you can mouseover to change the year.
function enableInteraction() {
  var yearScale = d3.scaleLinear()
    .domain([2013, 2021])
    .range([box.x + 10, box.x + box.width - 10])
    .clamp(true);

  // Cancel the current transition, if any.
  svg.transition().duration(0);

  overlay
    .on("mouseover", mouseover)
    .on("mouseout", mouseout)
    .on("mousemove", mousemove)
    .on("touchmove", mousemove);

  function mouseover() { label.classed("active", true); }
  function mouseout() { label.classed("active", false); }
  function mousemove() { displayYear(yearScale.invert(d3.mouse(this)[0])); }
}

// Tweens the entire chart by first tweening the year, and then the data.
// For the interpolated data, the dots and label are redrawn.
function tweenYear() {
  var year = d3.interpolateNumber(2013, 2021);
  return function(t) { displayYear(year(t)); };
}

// Updates the display to show the specified year.
function displayYear(year) {
  currentYear = year;
  counties.call(style,year)
  label.text(Math.round(year));
}

// Interpolates the dataset for the given (fractional) year.
function interpolateData(year) {
  return unemployment.filter(function(row) {
  return row['year'] == Math.round(year);
});
  }
};

};

Here is a snapshot of my csv file:

name. |. year. |.  rate|.  code
Bronx.   2021.      1.      36005
Bronx.   2020.      2.      36005
Queens.  2021.      4.     36081
Queens.  2017.      8.     36081

Solution

  • cbertelegni is right in noting that you need to update the property you are accessing when using the new data. Once that is resolved you have a few new problems though:

    1. The data you have is not projected, before it was pre-projected and you didn't need a projection.

    2. The state outline is gone as we don't have a states property in the topojson.

    The first is pretty easy, we need to use a projection, perhaps something like:

       var geojson = topojson.feature(topo, topo.objects.cb_2015_new_york_county_20m);
          
       var projection = d3.geoAlbers()
         .fitSize([width,height],geojson);
         
       var path = d3.geoPath(projection);
    

    The second problem is also fairly straightforward. The states outlines were drawn where two polygons representing two different states shared an arc: topojson.mesh(us, us.objects.states, (a, b) => a !== b) (a and b represent states, where an arc separates two different states a !== b). If we use the counties data here, we'll just get a mesh that separates the counties.

    Instead we can change the equation a bit when using the counties geometry: if an arc is shared only by one feature, a and b will both represent that feature, so we can use:

     var outline = topojson.mesh(topo, topo.objects.cb_2015_new_york_county_20m, (a, b) => a === b);
    

    to find which arcs are not shared between counties (ie: the outer edges or the boundary of the state).

    I've created a simplistic chorlopleth below that demonstrates the two changes in this answer in combination with cbertelegni's change.

    var svg = d3.select("svg");
    var path = d3.geoPath();
    var format = d3.format("");
    var height = 360;
    var width = 500;
    
    var names = ["Steuben","Sullivan","Tioga","Fulton","Lewis","Rockland","Schuyler","Dutchess","Westchester","Clinton","Seneca","Jefferson","Wyoming","Monroe","Chemung","Erie","Richmond","Rensselaer","Tompkins","Montgomery","Schoharie","Bronx","Franklin","Otsego","Allegany","Yates","Cortland","Ontario","Wayne","Niagara","Albany","Onondaga","Herkimer","Cattaraugus","Ulster","Nassau","Livingston","Cayuga","Chenango","Columbia","Oswego","Putnam","Greene","New York","Orange","Madison","Warren","Suffolk","Oneida","Chautauqua","Orleans","Saratoga","Schenectady","St. Lawrence","Kings","Genesee","Essex","Queens","Broome",,"Washington","Hamilton","Delaware"]
    
    var max = 20;
    var lookup = new Map();
    names.forEach(function(name,i) {
      lookup.set(name, max - i * max / names.length);
    })
    
    var colorScheme = d3.schemeReds[9];
    colorScheme.unshift("#eee");
    
    var color = d3.scaleQuantize()
     .domain([0, 20])
     .range(colorScheme);
    
    d3.json("https://raw.githubusercontent.com/deldersveld/topojson/master/countries/us-states/NY-36-new-york-counties.json", function(topo) {
       
       var geojson = topojson.feature(topo, topo.objects.cb_2015_new_york_county_20m);
       
       var outline = topojson.mesh(topo, topo.objects.cb_2015_new_york_county_20m, (a, b) => a === b);
       
       var projection = d3.geoAlbers()
         .fitSize([width,height],geojson);
         
       var path = d3.geoPath(projection);
       
       var counties = svg.selectAll(null)
         .data(geojson.features)
         .enter()
         .append("path")
         .attr("d",path)
         .attr("fill", d=> color(lookup.get(d.properties.NAME))) 
         
       var state = svg.append("path")
         .attr("d", path(outline))
         .attr("class","state");
     
    })
    .county-borders {
     fill: none;
     stroke: #fff;
     stroke-width: 0.5px;
     stroke-linejoin: round;
     stroke-linecap: round;
     pointer-events: none;
    }
    
    .state {
      fill: none;
      stroke: black;
      stroke-dashArray: 4 6;
      stroke-width: 1px;
    }
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <script src="https://d3js.org/topojson.v1.min.js"></script>
    <script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
    <script src="https://d3js.org/queue.v1.min.js"></script>
    
    <svg width="960" height="600"></svg>