arsm800
arsm800

Reputation: 61

D3 - Rendering Multiple Years' Worth of State Data on US Map

I am new to programming and I have been working the examples in the O'Reilly Interactive Data Visualization book.

Right now, I am working on visualizing 5 years' worth of data on electric vehicles by state. Each state's color changes depending on the number of registered electric vehicles during that particular year.

Question:

Each time I click on a year in the dropdown menu, it creates another map svg underneath it. How can I get it re-render the data on the same (existing) map svg?

Background:

I have global variables that define the dimensions of the svg. I also have two functions:

What I have tried so far:

I console.log(year) after the d3 color.domain method and the correct year is stored as the year variable at that point.

Also, if I manually change the argument when drawMap() is called, I can successfully change the data rendered on the existing map.

My code:

var w = 1200;
var h = 800;
var barpadding = 1;

var drawMap = function(year) {

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

  var projection = d3.geo.albersUsa()
                         .translate([w/2, h/2]) 
                         .scale([1600]);   

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

  var color = d3.scale.quantize()   
                    .range(["rgb(237, 248, 233)", "rgb(186, 228, 179)", "rgb(116,196,118)", "rgb(49, 163, 84)", "rgb(0, 109, 44)"]);


  color.domain([
    d3.min(data, function(d) {return d["electric_vehicles_" + year];}),
    d3.max(data, function(d) { return d["electric_vehicles_" + year];})

  ]);

  console.log(year);

  d3.json("us-states.json", function(json) {  
    for (var i = 0; i < data.length; i ++) {  
      var dataState = data[i].state;         
      var dataValue = parseFloat(data[i]["electric_vehicles_" + year]);  
      for (var j = 0; j < json.features.length; j++) {   
        var jsonState = json.features[j].properties.name;
        if (dataState === jsonState) {                    
          json.features[j].properties.value = dataValue;
          break;
        }
      }
    }

    var stateView = function() {
    console.log("State clicked.");
    location.href="/states/1";  

    svg.selectAll("path")
      .data(json.features)
      .enter()
      .append("path")
      .attr("d", path)
      .style("fill", function(d) {
        var value = d.properties.value;
        if (value) {
          return color(value);
        }
        else {
          return "#ccc";
        }
      })
      .on("mouseover", function(d, i) {
        d3.select(this)
        .style("fill-opacity", 0.5);
      })
      .on("mouseout", function(d, i) {
        d3.selectAll("path")
        .style("fill-opacity", 1);
      })
      .on("click", function() {
        stateView();
      });
  });
});
};

var clickEventMap = function() {
  $("#map2013").on("click", function() {
    // var year = 2013;
    drawMap(2013);
  });

  $("#map2012").on("click", function() {
    // var year = 2012;
    drawMap(2012);
  });

  $("#map2011").on("click", function() {
    // var year = 2011;
    drawMap(2011);
  });

  $("#map2010").on("click", function() {
    // var year = 2010;
    drawMap(2010);
  });

  $("#map2009").on("click", function() {
    // var year = 2009;
    drawMap(2009);
  });
};

drawMap(2013);
clickEventMap();

Thanks for your input.

Upvotes: 4

Views: 318

Answers (1)

James Stewart
James Stewart

Reputation: 378

Your main problem is that you are doing too much inside the drawMap function. A lot of what is in there only needs to be done once, not every time you switch years. This is why you are getting multiple maps. This code creates a new svg every time you call drawMap

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

If you just move that up out of the drawMap function, you will only get one svg.

You are also getting the 'us-states.json' file every time you draw the map. This will probably technically work, but causes a lot of slow network requests that you really don't need. Just make the request once when the page loads. It's a pretty big restructuring change to your code, because basically everything after that needs to be called from within the callback.

Lastly, after you make those changes you will need to change your code that draws the states to get them to update correctly. Right now you do everything on the "enter" selection, which will only operate on new paths. Since you will be editing existing paths, your code needs to look more like this:

var paths = svg.selectAll("path")
    .data(json.features)
    .enter()
    .append("path")
    .attr("d", path)
    .on("mouseover", function(d, i) {
        d3.select(this)
            .style("fill-opacity", 0.5);
    })
    .on("mouseout", function(d, i) {
        d3.selectAll("path")
            .style("fill-opacity", 1);
    })
    .on("click", function() {
        stateView();
    });

paths.style("fill", function(d) {
    var value = d.properties.value;
    if (value) {
        return color(value);
    } else {
        return "#ccc";
    }
});

Mike Bostocks articles Thinking with Joins and Three Little Circles are both great resources for understanding updates in d3.

Basically, just think about what things you just need to do once, and what needs to change every time you click on a new year (updating your color scale and setting fill styles for each state). Try pulling everything that happens once out into an init function, and keep you drawMap function small.

Upvotes: 1

Related Questions