My cities are being plotted with the wrong projection (quite small and to the left). Using d3.geo.albersUSA()

184 Views Asked by At

D3 newbie.

My cities are being plotted with the wrong projection (quite small and to the left) as compared to my map which is the correct size. It also looks like the longitude may be reversed. Using d3.geo.albersUSA().

The .scale of the map is set to 1000 and the map looks great. But my data is not projecting on the same scale.

//Width and height of map (adding relative margins to see if that effects placement of circles. makes no apparent difference.)
var margin = { top: 0, left: 0, right: 0, bottom: 0},
height = 500 - margin.top - margin.bottom,
width = 960 - margin.left - margin.right;


// D3 Projection
var projection = d3.geo.albersUsa()
                   .translate([width/2, height/2])    // translate to center of screen
                   .scale([1000]);          // scale things down so see entire US
        
// Define path generator
var path = d3.geo.path()               // path generator that will convert GeoJSON to SVG paths
             .projection(projection);  // tell path generator to use albersUsa projection

        
// Define linear scale for output
var color = d3.scale.linear()
              .range(["rgb(165,110,255)","rgb(0,45,150)","rgb(0,157,154)","rgb(250,77,86)"]);

var legendText = ["High Demand, High Supply", "Low Demand, Low Supply", "Low Demand, High Supply", "High Demand, Low Supply"];

//Create SVG element and append map to the SVG
var svg = d3.select("body")
            .append("svg")
            .attr("width", width)
            .attr("height", height);
        
// Append Div for tooltip to SVG
var div = d3.select("body")
            .append("div")   
            .attr("class", "tooltip")               
            .style("opacity", 0);

// Load in my states data!
d3.csv("https://raw.githubusercontent.com/sjpozzuoli/Daves_Eagles/main/Data_Main/data_clusters_latlong_ready.csv", function(data) {
color.domain([0,1,2,3]); // setting the range of the input data

// Load GeoJSON data and merge with states data
d3.json("https://gist.githubusercontent.com/michellechandra/0b2ce4923dc9b5809922/raw/a476b9098ba0244718b496697c5b350460d32f99/us-states.json", function(json) {

// Loop through each state data value in the .csv file
for (var i = 0; i < data.length; i++) {

    // Grab State Name
    var dataState = data[i].state;

    // Grab data value 
    var dataValue = data[i].visited;

    // Find the corresponding state inside the GeoJSON
    for (var j = 0; j < json.features.length; j++)  {
        var jsonState = json.features[j].properties.name;

        if (dataState == jsonState) {

        // Copy the data value into the JSON
        json.features[j].properties.visited = dataValue; 

        // Stop looking through the JSON
        break;
        }
    }
}
        
// Bind the data to the SVG and create one path per GeoJSON feature
svg.selectAll("path")
    .data(json.features)
    .enter()
    .append("path")
    .attr("d", path)
    .style("stroke", "#fff")
    .style("stroke-width", "1")
    .style("fill", function(d) {

    // Get data value
    var value = d.properties.visited;

    if (value) {
    //If value exists…
    return color(value);
    } else {
    //If value is undefined…
    return "rgb(213,222,217)";
    }
});

//this piece of code brings in the data and reshapes it to numberic from string. Runs great. 
// Map the cities I have lived in!
d3.csv("https://raw.githubusercontent.com/sjpozzuoli/Daves_Eagles/main/Data_Main/data_clusters_latlong_ready.csv", function(data) {
  var data;
  data.forEach(function(d){
    //create number values from strings
    d.demand_score = +d.demand_score;
    d.hotness_rank = +d.hotness_rank;
    d.hotness_rank_yy = +d.hotness_rank_yy;
    d.unique_viewers_per_property_yy =+ d.unique_viewers_per_property_yy;
    d.median_days_on_market_yy =+ d.median_days_on_market_yy ;
    d.median_listing_price_yy =+ d.median_listing_price_yy;
    d.mortgage_rate =+ d.mortgage_rate;
    d.supply_score =+ d.supply_score;
    d.date = new Date(d.date);
    d.latitude =+ d.latitude;
    d.longitude =+ d.longitude;
    d.class =+ d.class;

  //console.log(d);
  //console.log(d.city);
  var city_state = d.city + ", " + d.state;
      //console.log(city_state);
    });
console.log(data, "data");
svg.selectAll("circle")
    .data(data)
    .enter()
    .append("circle")
  //dots are being drawn just with reverse longitude (neg?)and off the map
    .attr("cx", function(d) {
    var coords = ([d.longitude, d.latitude])
    console.log("coords", coords)
    //console.log(d.longitude, "d.longitude");
        return coords[0];
    })
    .attr("cy", function(d) {
        var coords = ([d.longitude, d.latitude])
    return coords[1];
    })
    
  //size of circle working
  .attr("r", function(d) {
        return Math.sqrt(d.hotness_rank) * .05;
    })
    //todo: add if statement to correspond color with class 
    .style("fill", "rgb(217,91,67)")    
        .style("opacity", 0.85) 
1

There are 1 best solutions below

11
Andrew Reid On

The United States in the Western hemisphere, so its longitudes are negative, the behavior you are seeing is expected and the data is correct in this regard.

The problem is that you are treating longitude and latitude as pixel coordinates rather than points on a three dimensional globe. This is why they appear to the left of your SVG. To convert from points on a globe to Cartesian pixels we need a projection.

While you use a projection to project outline of the United States (when you provide the projection to the path generator), you don't use one to project your points. Whenever working with multiple sources of geographic data you need to ensure consistency in projection, otherwise features from different data sources will not align.

The solution is quite simple - project your points with the same projection you use for the outline: projection([longitude,latitude]) which will return a two element array containing the projected x and y (in pixels) coordinates of that point:

.attr("cx", function(d) {
    var coords = projection([d.longitude, d.latitude])
    return coords[0];
})
.attr("cy", function(d) {
    var coords = projection([d.longitude, d.latitude])
    return coords[1];
})

And here's a snippet (limiting the total circle count to 3000 for demonstration and performance):

//Width and height of map (adding relative margins to see if that effects placement of circles. makes no apparent difference.)
var margin = { top: 0, left: 0, right: 0, bottom: 0},
height = 500 - margin.top - margin.bottom,
width = 960 - margin.left - margin.right;


// D3 Projection
var projection = d3.geo.albersUsa()
                   .translate([width/2, height/2])    // translate to center of screen
                   .scale([1000]);          // scale things down so see entire US
        
// Define path generator
var path = d3.geo.path()               // path generator that will convert GeoJSON to SVG paths
             .projection(projection);  // tell path generator to use albersUsa projection

        
// Define linear scale for output
var color = d3.scale.linear()
              .range(["rgb(165,110,255)","rgb(0,45,150)","rgb(0,157,154)","rgb(250,77,86)"]);

var legendText = ["High Demand, High Supply", "Low Demand, Low Supply", "Low Demand, High Supply", "High Demand, Low Supply"];

//Create SVG element and append map to the SVG
var svg = d3.select("body")
            .append("svg")
            .attr("width", width)
            .attr("height", height);
        
// Append Div for tooltip to SVG
var div = d3.select("body")
            .append("div")   
            .attr("class", "tooltip")               
            .style("opacity", 0);

// Load in my states data!
d3.csv("https://raw.githubusercontent.com/sjpozzuoli/Daves_Eagles/main/Data_Main/data_clusters_latlong_ready.csv", function(data) {
color.domain([0,1,2,3]); // setting the range of the input data

// Load GeoJSON data and merge with states data
d3.json("https://gist.githubusercontent.com/michellechandra/0b2ce4923dc9b5809922/raw/a476b9098ba0244718b496697c5b350460d32f99/us-states.json", function(json) {

// Loop through each state data value in the .csv file
for (var i = 0; i < data.length; i++) {

    // Grab State Name
    var dataState = data[i].state;

    // Grab data value 
    var dataValue = data[i].visited;

    // Find the corresponding state inside the GeoJSON
    for (var j = 0; j < json.features.length; j++)  {
        var jsonState = json.features[j].properties.name;

        if (dataState == jsonState) {

        // Copy the data value into the JSON
        json.features[j].properties.visited = dataValue; 

        // Stop looking through the JSON
        break;
        }
    }
}
        
// Bind the data to the SVG and create one path per GeoJSON feature
svg.selectAll("path")
    .data(json.features)
    .enter()
    .append("path")
    .attr("d", path)
    .style("stroke", "#fff")
    .style("stroke-width", "1")
    .style("fill", function(d) {

    // Get data value
    var value = d.properties.visited;

    if (value) {
    //If value exists…
    return color(value);
    } else {
    //If value is undefined…
    return "rgb(213,222,217)";
    }
});

//this piece of code brings in the data and reshapes it to numberic from string. Runs great. 
// Map the cities I have lived in!
d3.csv("https://raw.githubusercontent.com/sjpozzuoli/Daves_Eagles/main/Data_Main/data_clusters_latlong_ready.csv", function(data) {
  var data;
  data.forEach(function(d){
    //create number values from strings
    d.demand_score = +d.demand_score;
    d.hotness_rank = +d.hotness_rank;
    d.hotness_rank_yy = +d.hotness_rank_yy;
    d.unique_viewers_per_property_yy =+ d.unique_viewers_per_property_yy;
    d.median_days_on_market_yy =+ d.median_days_on_market_yy ;
    d.median_listing_price_yy =+ d.median_listing_price_yy;
    d.mortgage_rate =+ d.mortgage_rate;
    d.supply_score =+ d.supply_score;
    d.date = new Date(d.date);
    d.latitude =+ d.latitude;
    // ensure longitue for US is negative:
    d.longitude = d.longitude > 0 ? -d.longitude : + d.longitude;
    d.class =+ d.class;
    });
    
    data = data.filter(function(d,i) {
      return d.longitude && d.latitude && i++ < 3000;
    })
    

svg.selectAll("circle")
    .data(data)
    .enter()
    .append("circle")
    .attr("cx", function(d) {
        var coords = projection([d.longitude, d.latitude])
        
        return coords[0]
    })
    .attr("cy", function(d) {
        
        var coords = projection([d.longitude, d.latitude])
        return coords[1];
    })
    
  //size of circle working
  .attr("r", function(d) {
        return Math.sqrt(d.hotness_rank) * .05;
    })
    //todo: add if statement to correspond color with class 
    .style("fill", "rgb(217,91,67)")    
        .style("opacity", 0.85) 
        
})
})

})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>