I am trying to create a simple bubblemap that plots longitude-latitude pairs by a number of admissions for a county using US DHS data. My CSV file has the name of county, longitude, latitude, type of Admission, class of admission, number of admissions, and country of origin. I've created some checkboxes that will allow the user to see a bubblemap of the different classes of admissions that were admitted to the United States.
I've learned that the d3geoAlbersUsa projection projects the map of the USA at [0,0] which is off the coast of Africa. From the photo below (see Imgur link), you can see that my points seem to plot at the correct coordinates. However, the background map is not visible. When I use d3.geoMercator() and center the projection on [0,0] I see the map. In both cases, I don't know how to make the bubbles appear on the map.
I'm new to d3 so I'm not sure how to go about this. How do I create a bubblemap using long lat coordinates with the d3geoAlbersUsa projection? Thanks for the help.
Here is my index.html:
var width = 750
var height = 750
// The svg
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("width", width)
.attr("height", height)
// Map and projection
// Map and projection
// var projection = d3.geoMercator()
// //.center([-100, 30]) // GPS of location to zoom on
// .center([0, 0])
// .scale(200) // This is like the zoom
// .translate([ width/2, height/2 ])
var projection = d3.geoAlbersUsa()
//.center([0,0])
.scale([1000]) // This is like the zoom
.translate([width / 2, height / 2])
var data = d3.csv("sheet.csv", function(data) {
var markers = data.filter(function(d) {
if (
(d["MajorClassAdmission"] == "EmploymentPreference1st" ||
d["MajorClassAdmission"] == "EmploymentPreference2nd" ||
d["MajorClassAdmission"] == "EmploymentPreference3rd") &&
d["CountryofBirth"] == "Bangladesh" && d["Admissions"] != "D" && d["lon"] != "NA" && d["lat"] != "NA") {
return d;
}
})
//console.log(markers)
// Load external data and boot
//d3.json("projectedgeography.json", function(data){
d3.json("projectedgeography.geojson", function(data) {
//d3.json("https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson", function(data){
// Filter data
//data.features = data.features.filter( function(d){return d.properties.name=="USA"} )
//data.features = data.features.filter( function(d){return d.properties.name=="USA"} )
// Create a color scale
var color = d3.scaleOrdinal()
.domain(["EmploymentPreference1st", "EmploymentPreference2nd", "EmploymentPreference3rd"])
.range(["#402D54", "#D18975", "#8FD175"])
// Add a scale for bubble size
var size = d3.scaleLinear()
.domain([1, 100]) // What's in the data
.range([4, 50]) // Size in pixel
//var path = d3.geo.path().projection(projection)
//Draw the map
svg.append("g")
.selectAll("path")
.data(data.features)
.enter()
.append("path")
.style("stroke", "#black")
.style("opacity", .3)
//create a tooltip (hover information)
var Tooltip = d3.select("#my_dataviz")
.append("div")
.attr("class", "tooltip")
.style("opacity", 1)
.style("background-color", "white")
.style("border", "solid")
.style("border-width", "2px")
.style("border-radius", "5px")
.style("padding", "5px")
// Three function that change the tooltip when user hover / move / leave a cell
var mouseover = function(d) {
Tooltip.style("opacity", 1)
}
var mousemove = function(d) {
Tooltip
.html(d.CountyState + "<br>" + "long: " + d.lon + "<br>" + "lat: " + d.lat + "<br>" + "Admissions: " + d.Admissions)
.style("left", (d3.mouse(this)[0] + 10) + "px")
.style("top", (d3.mouse(this)[1]) + "px")
}
var mouseleave = function(d) {
Tooltip.style("opacity", 0)
}
// Add circles:
svg
.selectAll("myCircles")
.data(markers)
.enter()
.append("circle")
.attr("class", function(d) {
return (d.MajorClassAdmission)
})
.attr("cx", function(d) {
return projection([d.lon, d.lat])[0]
})
.attr("cy", function(d) {
return projection([d.lon, d.lat])[1]
})
.attr("r", function(d) {
return d.Admissions
})
.style("fill", function(d) {
return color(d.MajorClassAdmission)
})
.attr("stroke", function(d) {
return color(d.MajorClassAdmission)
})
.attr("stroke-width", 3)
.attr("fill-opacity", .4)
.on("mouseover", mouseover)
.on("mousemove", mousemove)
.on("mouseleave", mouseleave)
// This function is gonna change the opacity and size of selected and unselected circles
function update() {
// For each check box:
d3.selectAll(".checkbox").each(function(d) {
cb = d3.select(this);
group = cb.property("value")
//console.log(group)
// If the box is check, I show the group
if (cb.property("checked")) {
//console.log("checked")
svg.selectAll("." + group).transition().duration(1000).style("opacity", 1).attr("r", function(d) {
return d.Admissions
})
// Otherwise I hide it
} else {
//console.log("unchecked")
svg.selectAll("." + group).transition().duration(1000).style("opacity", 0).attr("r", 0)
}
})
}
// When a button change, I run the update function
d3.selectAll(".checkbox").on("change", update)
// And I initialize it at the beginning
update()
})
})
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<style>
.circle:hover {
stroke: black;
stroke-width: 4px;
}
.legend circle {
fill: none;
stroke: #ccc;
}
.legend text {
fill: #777;
font: 10px sans-serif;
text-anchor: middle;
}
</style>
<!-- Load d3.js and the geo projection plugin -->
<script src="https://d3js.org/d3.v4.js"></script>
<script src="https://d3js.org/d3-geo-projection.v2.min.js"></script>
<script src="https://d3js.org/queue.v1.min.js"></script>
<!-- <script src="http://d3js.org/d3.geo.projection.v0.min.js"></script> optional, depending on projection -->
<h1>LPR Top 200 Bangladesh</h1>
<!-- Button -->
<div>
<input type="checkbox" class="checkbox" value="EmploymentPreference1st" checked><label>Employment Preference 1</label>
<input type="checkbox" class="checkbox" value="EmploymentPreference2nd" checked><label>Employment Preference 2</label>
<input type="checkbox" class="checkbox" value="EmploymentPreference3rd" checked><label>Employment Preference 3</label>
</div>
<!-- Create an element where the map will take place -->
<!-- <svg id="my_dataviz" width="500" height="500"></svg> -->
<div id="my_dataviz"></div>
Attached are screenshots of my data and how my bubblemap looks like using d3.geoMercator() and d3.geoAlbersUsa()
Here are the screenshots: https://i.sstatic.net/fhNUw.jpg
Edit: It looks like I was using a bad geojson. I had initially downloaded the shapefile from US Census and used Mapshaper to convert to a geojson file. Using that geojson, I used a projection app to create a geojson with the geoalbersUsa projection. In effect, I was using d3.geoAlbersUsa() on a geojson already converted to that projection. I misunderstood how geoAlbersUsa() works. Using the original geojson from Mapshaper, I get the map I'm looking for: https://i.sstatic.net/y9ltA.jpg
D3 projections are all pretty similar. If one projection projects a given point correctly, chances are so does every other one. They take coordinates in decimal degrees and spit out coordinates in pixels and not much else. Consequently, your statement: "d3geoAlbersUsa
projection projects the map of the USA at [0,0]
which is off the coast of Africa." is incorrect. [0,0]
in degrees is off the coast of Africa, [0,0]
in pixels can be anywhere. D3 is giving you pixels, not degrees.
If you have features/tiles/raster that show Africa where the US is or vice versa, you have conflicting projections or coordinate systems, not a failure of d3.geoAlbersUsa.
Also, if you are mixing pre-projected geometry and unprojected geometry, you're making it too difficult: you'll need to ensure that the projections used to pre-project one geometry matches the projection used to project the second, which unless you use d3 to preproject the geometry offline somewhere will cause you some headaches.
If you have a csv with coordinates in decimal degrees and a geojson with the same, you can assume that the projection (and consequently, any paths) will be rendered consistently, and correctly (where correctly is not necessarily equivalent to as desired...).
Now d3.geoAlbersUsa is a bit of a special projection in that it is a composite projection combining several distinct projections (all Albers, while also shrinking Alaska down several times). It is calibrated to be centered on the US assuming a screen size of 960x600 pixels. You shouldn't need to alter the centering point, it has been set for you. The translate must be modified in your case, however, as this translates the projected coordinates. The default translate expects a container of 960x600 pixels. You want the translate to be equal to width/2,height/2, as you have. Trickier is the scale, the default scale is 1070, which extends the US across 960 pixels. This scale factor is linear, so we can use: 1070/960*width to create a new scale factor (assuming width is the limiting factor).
d3.geoMercator is more plain, but we need to center that projection properly because it isn't by default centered on the US. We can use:
d3.geoMercator()
.center([-100,30]) // as you had
.scale(1000) // or whatever
.translate([width/2,height/2])
We still need to apply the translate, because the default values don't expect a square 750x750 svg.
Note there is also projection.fitSize()
which will automatically adjust scale and translate to center a geojson feature like so:
d3.geoMercator()
.fitSize([width,height],geojson) // geojson must be a valid geojson object, not an array of geojson features.
We pass the projection to the path generator and then draw points and geojson relatively straightforwardly:
The following use the commented out line accessing a geojson, if you share projectedgeography.geojson
, I may be able to provide you with some information on why your code doesn't work as expected. But given its name, it sure seems like it isn't unprojected data
AlbersUsa
var width = 750
var height = 400
// The svg
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("width", width)
.attr("height", height);
// Create a color scale
var color = d3.scaleOrdinal()
.domain(["A", "B", "C" ])
.range([ "#402D54", "#D18975", "#8FD175"])
var projection = d3.geoAlbersUsa()
.translate([width/2,height/2])
.scale(1070/960*width); // scale the scale factor, otherwise map will overflow SVG bounds.
var path = d3.geoPath(projection);
d3.json("https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson", function(geojson){
geojson.features = geojson.features.filter( function(d){return d.properties.name=="USA"} )
// d3.csv("file.csv", function(csv) {
// As I cannot access your csv, I'm reproducing a few lines here:
var csv = [
{lon: -116.2,lat: 43.5, Admissions: 24, MajorClassAdmission: "A"},
{lon: -81.7,lat: 41.4, Admissions: 13, MajorClassAdmission: "B"},
{lon: -74.1,lat: 40.9, Admissions: 35, MajorClassAdmission: "C"},
{lon: -121.6,lat: 37.3, Admissions: 14, MajorClassAdmission: "B"},
{lon: -73.9,lat: 40.68, Admissions: 13, MajorClassAdmission: "A"},
]
svg.selectAll("path")
.data(geojson.features)
.enter()
.append("path")
.attr("d", path);
svg.selectAll("circle")
.data(csv)
.enter()
.append("circle")
.attr("cx", function(d) {
return projection([d.lon,d.lat])[0];
})
.attr("cy", function(d) {
return projection([d.lon,d.lat])[1];
})
.attr("r", function(d) {
return d.Admissions
})
.attr("fill", function(d) {
return color(d.MajorClassAdmission);
})
// ...
// })
})
.circle:hover{
stroke: black;
stroke-width: 1px;
}
path {
fill: none;
stroke: #ccc;
stroke-width: 1px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://d3js.org/d3.v4.js"></script>
<div id="my_dataviz"></div>
Mercator
var width = 750
var height = 400
// The svg
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("width", width)
.attr("height", height);
// Create a color scale
var color = d3.scaleOrdinal()
.domain(["A", "B", "C" ])
.range([ "#402D54", "#D18975", "#8FD175"])
var projection = d3.geoMercator()
var path = d3.geoPath(projection);
d3.json("https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson", function(geojson){
geojson.features = geojson.features.filter( function(d){return d.properties.name=="USA"} )
projection.fitSize([width,height],geojson)
// d3.csv("file.csv", function(csv) {
// As I cannot access your csv, I'm reproducing a few lines here:
var csv = [
{lon: -116.2,lat: 43.5, Admissions: 24, MajorClassAdmission: "A"},
{lon: -81.7,lat: 41.4, Admissions: 13, MajorClassAdmission: "B"},
{lon: -74.1,lat: 40.9, Admissions: 35, MajorClassAdmission: "C"},
{lon: -121.6,lat: 37.3, Admissions: 14, MajorClassAdmission: "B"},
{lon: -73.9,lat: 40.68, Admissions: 13, MajorClassAdmission: "A"},
]
svg.selectAll("path")
.data(geojson.features)
.enter()
.append("path")
.attr("d", path);
svg.selectAll("circle")
.data(csv)
.enter()
.append("circle")
.attr("cx", function(d) {
return projection([d.lon,d.lat])[0];
})
.attr("cy", function(d) {
return projection([d.lon,d.lat])[1];
})
.attr("r", function(d) {
return d.Admissions
})
.attr("fill", function(d) {
return color(d.MajorClassAdmission);
})
// ...
// })
})
.circle:hover{
stroke: black;
stroke-width: 1px;
}
path {
fill: none;
stroke: #ccc;
stroke-width: 1px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://d3js.org/d3.v4.js"></script>
<div id="my_dataviz"></div>
While the comments in your code obfuscate the code you are using to generate problematic maps, there are a few potential simplifications in your code: you appear to be using both d3v4 and d3v3 given the use of d3.geo.path (v3) instead of d3.geoPath (v4+). The base d3 bundle includes all the geographic functions you need, so you don't need to import https://d3js.org/d3-geo-projection.v2.min.js or any other modules.