Using d3 to map geojson distribution data with projections - javascript

I'm trying to map bird distributions using d3. So far, I've successfully used d3 to plot and project a path of a map of Illinois. Now, I'm requesting data from the eBird api, then manipulating it to be read as geojson, then create a series of d3 circles to plot them against the map. The circles are getting created and attached to the DOM, but the projection I've defined seems to plot them too far up and to the right.
Still learning the basics of d3, so please bear with me. My code can be found in the gist below.
https://gist.github.com/cqlanus/599a6b02e5168a051fef948ba541e296
Alternatively, if I try to plot the data as a path, the path elements are created, but they don't appear on the DOM:
const sightings = svg.append('g').attr('class', 'sightings')
sightings.selectAll('path')
.data(sightingGeoArr)
.enter().append('path')
.attr('fill', '#000')
.attr('d', geoPath)

Geojson uses an x,y format for coordinates. Your geojson however is y,x:
"coordinates": [result.lat, result.lng]
Try:
"coordinates": [result.lng, result.lat]
I don't have your state geojson for bounding, but I've estimated a suitable projection for the below snippet which shows your script working (I love Ebird data but I hadn't though about converting it to geojson for use in d3 before, I'll have to give it a go).
const height = 500, width = 500
/* Create a color function */
/* Define a projection function */
const albersProj = d3.geoAlbers()
.center([0,40.08])
.rotate([88.0,0])
.translate([width/2,height/2])
.scale(4000)
/* Define a path function */
const geoPath = d3.geoPath()
.projection(albersProj)
.pointRadius(5)
/* Attach svg to page */
var svg = d3.select( "body" )
.append( "svg" )
.attr( "width", width )
.attr( "height", height );
/* Attach group to svg */
var g = svg.append( "g" );
/* Get iNaturalist data */
d3.json('http://ebird.org/ws1.1/data/obs/region_spp/recent?rtype=subnational1&r=US-IL&sci=Setophaga%20coronata&back=15&maxResults=500&locale=en_US&fmt=json&includeProvisional=true')
.get((err, data) => {
console.log(data.length)
/* Manipulate it to be read as geojson */
const sightingGeoArr = data.map(result => ({
"geometry": {
"type": "Point",
"coordinates": [result.lng, result.lat]
}
}))
const sightings = svg.append('g').attr('class', 'sightings')
sightings.selectAll('circle')
.data(sightingGeoArr)
.enter().append('circle')
.attr('fill', '#000')
.attr('cx', d => albersProj(d.geometry.coordinates)[0])
.attr('cy', d => albersProj(d.geometry.coordinates)[1])
.attr('r', 2)
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>

Related

Group values by rows in javascript

I am trying to create a choropleth map with d3.js(v6). I am able to show the map however no data is encoded. I am having trouble grouping the data by row. My csv file looks like this:
code, location, date, cases
AFG, Afghanistan,2010, 10000
AFG, Afghanistan,2011, 30000
AFG, Afghanistan,2012, 60000
ALB, Albania,2010, 1000
ALB, Albania,2011, 3000
ALB, Albania,2012, 6000
ALG, ALgeria,2010, 4000
ALG, ALgeria,2011, 7000
ALG, ALgeria,2012, 16000...
I am trying to group the rows so I would get one value for each location using the following code but it doesn't seem to work.
Promise.all([
d3.json("https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson"),
d3.csv("data.csv", function(d) {
let datagroup = d3.group(data, d => d.location);
datagroup.set(d.location, +d.cases)
})]).then(function(loadData){
let topo = loadData[0]
// Draw the map
svg1.append("g")
.selectAll("path")
.data(topo.features)
.enter()
.append("path")
// draw each country
.attr("d", d3.geoPath()
.projection(projection)
)
// set the color of each country
.attr("fill", function (d) {
d.total = data.get(d.id) || 0;
return colorScale(d.total);
})
I would appreciate it if someone could take a look and point out my mistake. Thanks a lot!

Drawing circles via d3js and converting coordinates

I have following d3js code that draws country map based on geojson via d3.json and renderMap function. I want to add cities to the map that will be represented as a circles. Cities will be in the same svg as a country map.
Cities are in ua_cities_admin.csv and rendered via renderCity function.
What I don't understand is how to place data array from renderCity in the svg component and how to map city coordinates to country map.
Any help with this will be appreciated.
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<title>geoPath measures</title>
</head>
<body>
<div align="center">
<svg id="my_dataviz"></svg>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.9.1/d3.min.js"></script>
<script>
var height = window.innerHeight - 100;
var width = window.innerWidth - 100;
var svg = d3.select('#my_dataviz')
.attr("width", width)
.attr("height", height);
function renderMap(data) {
var projection = d3.geoIdentity().reflectY(true).fitSize([width, height], data)
var geoGenerator = d3.geoPath().projection(projection);
svg.append("g")
.selectAll("path")
.data(data.features)
.enter()
.append('path')
.attr('d', geoGenerator)
.attr('fill', 'steelblue');
};
function renderCity(data) {
var projection = d3.geoIdentity().reflectY(true).fitSize([width, height], data)
console.log(data)
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", 50)
.attr("cy", 50)
.attr("r", 3);
};
d3.json('https://raw.githubusercontent.com/EugeneBorshch/ukraine_geojson/master/Ukraine.json', renderMap);
d3.csv('ua_cities_admin.csv', renderCity);
</script>
</body>
</html>
ua_cities_admin.csv file has following fields:
City
lat
lng
Here's your premise:
You have two input data sets consisting of geographic data, a geojson file and a csv file. Both use latitude longitude as their coordinate system.
You want to project them, but you want them to share the same projected coordinate system, so that they align with one another.
However, if you use two different projections you won't get alignment: any common coordinate in the two input datasets will be projected differently by each different projection.
The simplest way to fix this is to reuse the same projection for both datasets, and to use a geographic projection, something like:
var projection = d3.geoMercator()
var geoGenerator = d3.geoPath().projection(projection);
function renderMap(data) {
projection.fitSize([width, height], data)
svg.append("g")
.selectAll("path")
.data(data.features)
.enter()
.append('path')
.attr('d', geoGenerator)
.attr('fill', 'steelblue');
};
function renderCity(data) {
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", d=>projection([d.lon,d.lat])[0])
.attr("cy", d=>projection([d.lon,d.lat])[1])
.attr("r", 3);
};
d3.json("file", function(geojson) {
d3.csv("file", function(csv) {
renderMap(geojson);
renderCity(csv);
})
})
I nested your requests because otherwise, whichever file loads first will be drawn first. We also need the geojson to be loaded first to set the projection data that will be used for the csv's circles.
Additional Detail
Projection.fitSize()
For reference, projection.fitSize() requires a valid geojson object. The data generated by d3.csv is an array of objects. We need to convert this to geojson if we want fitSize to work. Geojson point features look like:
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [longitude, latitude]
},
"properties": { /* ... */ }
}
So to create geojson we need to process your data a bit:
var features = data.map(function(d) {
return {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [d.lng, d.lat]
},
"properties": { "name":d.city }
}
})
But we do need to pass an object, not an array, so we need to create a geojson feature collection:
var featureCollection = { type:"FeatureCollection", features:features }
Projection vs Identity
Unlike d3.geoIdentity, a d3.geoProjection can be passed a coordinate to project. d3.geoIdenity can work, but with some tweaking to the above code; however, it may result in an abnormal shape or unfamiliar representation of the area of interest as it basically implements a Plate Carree projection: lat/long treated as planar with appropriate scaling/translating to center and size the map appropriately. A d3.geoProjection could be suitable here, projections also allow for fitSize() to be used.
If you wish for the same shape as d3.geoIdentity, you can use d3.geoEquirectangular() in place of d3.geoMercator in the code above.
Manually Setting Projection Parameters
fitSize() sets a identity's/projection's scale and translate - that's it.
You could define the projection once, rather than waiting to redefine its parameters once the data is loaded. If your data is static, you could extract the scale and translate values used by fitSize() by logging projection.scale() and projection.translate() after you run fitSize(), and then use those values to set the projection scale and translate yourself. This could mean you wouldn't need to wait for the geojson to load before drawing any circles (provided you ensure that you are not drawing the geojson over top of the circles).

D3 adds path for geojson but not topojson

I'm trying to display a map using D3. I have the same map as a geoJSON file and a topoJSON file. When I load in the geoJSON file, it renders on the page and the <path> tag gets filled with d=.... But when I change the url to get the topoJSON file, the <path> tag remains empty, even though I'm leaving the rest of the code untouched. The svg still renders with the topoJSON request, but nothing appears in it. Any thoughts on what might be going on?
My code:
var width = 550;
var height = 570;
var arizonaProjection = d3.geoMercator()
.center([-111.6602, 34.2744])
.scale(4500)
.translate([width/2, height/2]);
var path = d3.geoPath()
.projection(arizonaProjection);
var svg = d3.select("#map").append("svg")
.attr("height", height)
.attr("width", width);
d3.json("geojson/Arizona.geojson", function(error, Arizona) {
svg.append("path")
.attr("d", path(Arizona));
console.log(path);
});
The file geojson/Arizona.geojson is stored in a different directory of my local server, as is the topojson file at topojson/Arizona.json.
Sample of the topoJSON:
"transform":{
"scale":[0.00003998538143372804,0.000031941344085415994],
"translate":[-114.81659,31.33218]
},
"objects":{
"Arizona_88_to_89":{
"type":"GeometryCollection",
"geometries":[
{
"arcs":[[0,1,2,3,4,5]],
"type":"Polygon",
"properties":{
"startcong":"88",
"district":"1",
"statename":"Arizona",
"member":{
"88":{"7845":{"party":"Republican","name":"Rhodes, John Jacob","district":"1"}},
"89":{"7845":{"party":"Republican","name":"Rhodes, John Jacob","district":"1"}}
},
"endcong":"89",
"id":"004088089001"
}
},{
"arcs":[[6,-5,7,-3,8,-1,9,10,11,12]],
"type":"Polygon",
"properties":{
"startcong":"88",
"district":"2",
"statename":"Arizona",
"member":{
"88":{"10566":{"party":"Democrat","name":"Udall, Morris K.","district":"2"}},
"89":{"10566":{"party":"Democrat","name":"Udall, Morris K.","district":"2"}}
},
"endcong":"89",
"id":"004088089002"
}
},{
"arcs":[[-12,13,-10,-6,-7,14]],
"type":"Polygon",
"properties":{
"startcong":"88",
"district":"3",
"statename":"Arizona",
"member":{
"88":{"10623":{"party":"Democrat","name":"Senner, Georg F., Jr.","district":"3"}},
"89":{"10623":{"party":"Democrat","name":"Senner, Georg F., Jr.","district":"3"}}
},
"endcong":"89",
"id":"004088089003"
}
}
]
}
Sample of the geoJSON:
{"type": "FeatureCollection", "features": [{"geometry": {"type": "MultiPolygon", "coordinates": [[[[-112.75515, 33.99991], [-112.75073, 33.99984], [-112.75034, 33.99992], [-112.74655, 33.99991], [-112.74509, 33.9999], [-112.7442, 33.9999], [-112.74395, 33.9999], [-112.74346, 33.99977], [-112.74331, 33.99973], [-112.74262, 33.99955],
A d3 geopath takes a geojson object, not a topology/topojson object. To use topojson with a d3 geoPath, you must first convert it back to geojson. You can do this quite easily with topojson.js:
var featureCollection = topojson.feature(Arizona, Arizona.objects.counties)
Of course you can get the features as an array with:
var features = topojson.feature(Arizona, Arizona.objects.counties).features
The above assumes that topology.objects contains a property for counties, you'll have to take a look at your topojson to find out if counties is correct or not (I'm guessing you might be showing counties). If you used a tool such as mapshaper.org, the property name may be the same as the original file name.

d3.js build connections by using loop function

I have build a connection by using d3. The codes show the data and method of connection:
var places = {
TYO: [139.76, 35.68],
BKK: [100.48, 13.75],
BER: [13.40, 52.52],
NYC: [-74.00, 40.71],
};
var connections = {
CONN1: [places.TYO, places.BKK],
CONN2: [places.BER, places.NYC],
};
...
svg.append("path")
.datum({type: "LineString", coordinates: connections.CONN1})
.attr("class", "route")
.attr("d", path);
svg.append("path")
.datum({type: "LineString", coordinates: connections.CONN2})
.attr("class", "route")
.attr("d", path);
You can see my codes, that I use the two identical methods to build two connections. That is not good to build more connections.
I am wondering, if there is a loop function to interpret the connections by using data "connections" directly? I mean, I could get information for data "connections" and use them directly to build connections.
I have thought some ways, such as .datum({type: "LineString", function(d,i) {
return coordinates: connections[i];});. But it does not work.
Could you please tell me some way to solve it? Thanks.
Generally when you want to append many features in d3, you want to use an array not an object. With an array you can use a d3 enter selection which will then allow you to build as many features as you need (if sticking to an object, note that connections[0] is not what you are looking for, connections["conn1"] is).
Instead, use a data structure like:
var connections = [
[places.TYO, places.NYC],
[places.BKK, places.BER],
...
]
If you must have identifying or other properties for each datapoint use something like:
var connections = [
{points:[places.TYO, places.NYC],id: 1,...},
{points:[places.BKK, places.BER],id: 2,...},
...
]
For these set ups you can build your lines as follows:
paths = svg.selectAll(".connection")
.data(connections)
.enter()
.append("path")
.attr("class","connection")
.attr('d', function(d) {
return path ({
type:"LineString",
coordinates: d
});
})
See here. Or:
paths = svg.selectAll(".connection")
.data(connections)
.enter()
.append("path")
.attr("class","connection")
.attr('d', function(d) {
return path ({
type:"LineString",
coordinates: d.points
});
})
Alternatively, you can use a data set up like:
var connections = [
{target:"TYO", source:"NYC"},
{target:"BKK", source: "BER"},
...
]
paths = svg.selectAll(".connection")
.data(connections)
.enter()
.append("path")
.attr("class","connection")
.attr('d', function(d) {
return path ({
type:"LineString",
coordinates: [ places[d.source],places[d.target] ]
});
})
See here.
If selecting elements that don't yet exist, using these lines
d3.select("...")
.data(data)
.enter()
.append("path")
will append a path for each item in the data array - this means that d3 generally avoids the use of for loops as the desired behavior is baked right into d3 itself.

d3.path plots only first feature of feature collection

I am trying to plot a map of the UK and plotting a few selected points on it.
I am following the first part of this tutorial https://bost.ocks.org/mike/map/
Here's what I have done.
var svg = d3.select('body').append('svg')
.attr('height', height)
.attr('width', width)
.call(zoom);
d3.json("/data/uk.json", function(error, uk) {
if (error) return console.error(error);
var subunits = topojson.feature(uk, uk.objects.subunits);
var projection = d3.geo.albers()
.center([0, 55.4])
.rotate([4.4, 0])
.parallels([50, 60])
.scale(6000)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
svg.append('path')
.datum(subunits)
.attr('d', path);
svg.selectAll('.subunit')
.data(topojson.feature(uk, uk.objects.subunits).features)
.enter().append('path')
.attr('d', path);
Here is the part where I try to plot the points
d3.json('/data/places.json', function (error , result) {
if(error)
console.error(error);
svg.append('path')
.data(result.features)
.style('stroke', 'green')
.attr('d' , d3.geo.path().projection(projection))
});
The above code plots only one point on the map, i.e the first one in the JSON file
You are not correctly binding your features' data when using
svg.append('path')
.data(result.features)
.style('stroke', 'green')
.attr('d' , d3.geo.path().projection(projection))
This will append one path, bind the first element of result.features to this path and set the style and attribute accordingly.
To work the way you want it, you need to make use of the data joining mechanism of D3.
svg.selectAll('path.features')
.data(result.features)
.enter().append('path')
.attr('class', 'feature')
.style('stroke', 'green')
.attr('d' , d3.geo.path().projection(projection))
This will compute a data join for the features in result.features putting the new elements in the enter selection which is accessible by calling enter() on the seletion. Using this enter selection you can now append paths for all your features.
A further side note not directly related to your issue:
Depending on the number of features you want to append to your map, the .attr("d") setter might be called quite often. You could improve performance by reusing one instance of the path generator:
var geoPath = d3.geo.path().projection(projection); // You need just one instance
svg.selectAll('path.features')
.data(result.features)
.enter().append('path')
.attr('class', 'feature')
.style('stroke', 'green')
.attr('d' , geoPath) // Re-use the path generator
This is considered best practice, which should be generally applied.

Categories

Resources