3

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) 
Lucy Green
  • 31
  • 2

1 Answers1

2

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>
Andrew Reid
  • 37,021
  • 7
  • 64
  • 83
  • Thank you Andrew! The projection code throws an error. (that's why I took it out). But maybe that is caused by my positive longitudes. Will fix and re-run. Thank you! :index.html:179 Uncaught TypeError: Cannot read properties of null (reading '0') – Lucy Green Sep 20 '21 at 17:33
  • I'm not certain what line is 179, if it one of the projection lines, logging the data may reveal a missing coordinate, if it's the first circle that causes the error, than there may be another issue. – Andrew Reid Sep 20 '21 at 17:34
  • Line 179 is returns coords[0]; – Lucy Green Sep 20 '21 at 17:36
  • There appears to be some points in your csv without latitude or longitude data (eg: louisville/jefferson county), this is a likely cause of that error, you may need to filter out those missing locations or find the missing data. – Andrew Reid Sep 20 '21 at 17:41
  • Am looking through to make a working example - though 42,000 rows is a lot for SVG. – Andrew Reid Sep 20 '21 at 17:48
  • When I log the data, both lat and long are being read. I've successfully change the long to negative (thank you for that!) Ah! It's stopping at a few cities that have a null value. I'll remove them. – Lucy Green Sep 20 '21 at 17:49
  • Negative longitudes, removing features with missing coordinates and using the projection should be all you need (I've updated the answer with a snippet, my apologies, I did not see in the data right away you had removed the negative sign from all coordinates, which would produce a projection error - as would missing coordinates). With 42 000 data points you might want to consider a canvas overlay on the map, or a pure canvas map, you may see performance issues on slower, older, or mobile devices. – Andrew Reid Sep 20 '21 at 17:59
  • Thank you, Andrew! I'll post when it's working. – Lucy Green Sep 20 '21 at 18:02
  • I'd appreciate a link to see a final product, looks like an interesting project. It'll be best to just leave another comment with it (answer posts with updates or other follow up are generally deleted by the community, unless accompanied with a solution to the original question). – Andrew Reid Sep 20 '21 at 18:19
  • It's absolutely working, I will shine the corners and add the colors and then share the link to the final here in the comments. Thanks so much, Andrew!!! – Lucy Green Sep 20 '21 at 19:06
  • Hi Andrew, still working on adding more charts to the dashboard, more interactivity, but the map is looking good. This link will get you to its current stage. Thanks again for your help! https://sjpozzuoli.github.io/Daves_Eagles/ – Lucy Green Sep 21 '21 at 13:57