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.
Related
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).
I am building a web application to display different trends and stats between countries in the world. With d3, I am able to load the topojson file and project the world map.
var countryStatistics = [];
var pathList = [];
function visualize(){
var margin = {top: 100, left: 100, right: 100, bottom:100},
height = 800 - margin.top - margin.bottom,
width = 1200 - margin.left - margin.right;
//create svg
var svg = d3.select("#map")
.append("svg")
.attr("height", height + margin.top + margin.bottom)
.attr("width", width + margin.left + margin.right)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//load topojson file
d3.queue()
.defer(d3.json, "world110m.json")
.await(ready)
var projection = d3.geoMercator()
.translate([ width / 2, height / 2 ])
.scale(180)
//pass path lines to projection
var path = d3.geoPath()
.projection(projection);
function ready (error, data){
console.log(data);
//we pull the countries data out of the loaded json object
countries = topojson.feature(data, data.objects.countries).features
//select the country data, draw the lines, call mouseover event to change fill color
svg.selectAll(".country")
.data(countries)
.enter().append("path")
.attr("class", "country")
.attr("d", path)
.on('mouseover', function(d) {
d3.select(this).classed("hovered", true)
//this function matches the id property in topojson country, to an id in (see below)
let country = matchPath(this.__data__.id);
console.log(country)
})
.on('mouseout', function(d) {
d3.select(this).classed("hovered", false)
})
//here I push the country data into a global array just to have access and experimentalism.
for (var i = 0; i < countries.length; i++) {
pathList.push(countries[i]);
}
}
};
The matchPath() function is used to allow me to match the path data to countryStatistics for display when a certain country is mouseovered.
function matchPath(pathId){
//to see id property of country currently being hovered over
console.log("pathID:" + pathId)
//loop through all countryStatistics and return the country with matching id number
for(var i = 0; i < countryStatistics.length; i++){
if(pathId == countryStatistics[i].idTopo){
return countryStatistics[i];
}
}
}
The Problem: This works, but only in one direction. I can reach my statistical data from each topojson path... but I can't reach and manipulate individual paths based on the data.
What I would like to happen, is to have a button that can select a certain property from countryStatistics, and build a domain/range scale and set a color gradient based on the data values. The step I am stuck on is getting the stat data and path data interfacing.
Two potential solutions I see,
1:There is a way to connect the topo path data to the statistical data during the render, I could call a function to redraw sgv...
2:I build a new object that contains all of the path data and statistical data. In this case can I just pull out the topojson.objects.countries data and ignore the rest?
How should I achieve this? Any pointers, next step will be appreciated.
(where I am at with this project... http://conspiracytime.com/globeApp )
TopoJSON is a really powerfull tool. It has its own CLI (command line interface) to generate your own TopoJSON files.
That CLI allows you to create a unique file with the topology and the data you want to merge with.
Even though it is in its v3.0.2 the first versión looks the clear one to me. This is an example of how you can merge a csv file with a json through a common id attribute.
# Simplified versión from https://bl.ocks.org/luissevillano/c7690adccf39bafe583f72b044e407e8
# note is using TopoJSON CLI v1
topojson \
-e data.csv \
--id-property CUSEC,cod \
-p population=+t1_1,area=+Shape_area \
-o cv.json \
-- cv/shapes/census.json
There is a data.csv file with a cod column and census.json file with a property named CUSEC.
- Using the --id-property you can specify which attributes will be used in the merge.
- With the property -p you can create new properties on the fly.
This is the solid solution where you use one unique file (with one unique request) with the whole data. This best scenario is not always possible so another solution could be the next one.
Getting back to JavaScript, you can create a new variable accessible through the attribute in common the following way. Having your data the format:
// countryStatistics
{
"idTopo": "004",
"country": "Afghanistan",
"countryCode": "afg",
// ..
},
And your TopoJSON file the structure:
{"type":"Polygon","arcs":[[0,1,2,3,4,5]],"id":"004"},
{"type":"MultiPolygon","arcs":[[[6,7,8,9]],[[10,11,12]]],"id":"024"} // ...
A common solution to this type of situation is to create an Array variable accessible by that idTopo:
var dataById = [];
countryStatistics.forEach(function(d) {
dataById[d.idTopo] = d;
});
Then, that variable will have the next structure:
[
004:{
"idTopo": "004",
"country": "Afghanistan",
"countryCode": "afg",
//...
},
008: {
//...
}
]
From here, you can access the properties through its idTopo attribute, like this:
dataById['004'] // {"idTopo":"004","country":"Afghanistan","countryCode":"afg" ...}
You can decide to iterate through the Topo data and add these properties to each feature:
var countries = topojson
.feature(data, data.objects.countries)
.features.map(function(d) {
d.properties = dataById[d.id];
return d
});
Or use this array whenever you need it
// ...
.on("mouseover", function(d) {
d3.select(this).classed("hovered", true);
console.log(dataById[d.id]);
});
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>
I have a json file which contain multiple paths
Json file
{
"paths": {
"path1": {
"name": "PathName1",
"path": "m 411.42,469.19 -0.98 ... z",
},
"path2": {
"name": "PathName2",
"path": "m 396.03,243.46 1.48,0.11 0,0 -0.01,0.47 -0.95,1.45 ... z",
}
}
}
And I would like to create an svg from this Json file using d3, but I don't know how to use my paths
var svg = d3.select('#map').append("svg")
.attr("id", "svg")
.attr("width", 500)
.attr("height", 500);
d3.json("urlToJsonFile", function(req, data) {
// how can I show my paths ?
}
If you have several paths, and want to draw them all, you might want to parse your JSON data into a collection to use data()/enter()/exit() joins (or process them in a loop or use some other optimisation technique).
To use the data in the JSON structure you provided you can path objects, bind each "path" object as "datum", and pass the path data to its "d" attribute. This will draw one of your paths:
svg.append("path")
.attr("class", "path1")
.datum(data.paths.path1)
.attr("d", function(d) { return d.path});
Try it out: JSFiddle
Salutations all and happy holidays.
I Noticed an interesting behavioral quirk while trying to draw polygon layers with L.geoJson(). consider the following code:
var polygonCoords = [
{"type": "Feature",
"properties": {"group": "Violations"},
"geometry": {
"type" : "Polygon",
"coordinates": [[
[-107.69348, 43.22519],
[-105.48523, 42.99259],
[-107.7594, 42.26105]
]]
}
}];
and
var polygons = L.polygon([
[43.22519, -107.69348],
[42.99259, -105.48523],
[42.26105, -107.7594]
]);
Now, both work in their respective contexts. I was just wondering why the coordinate matrix within L.polygon() has to be reflected in order to show up where one expects it to be when passed into L.goeJson() like so:
var jsonPoly = L.geoJson(polygonCoords, {
style: function(feature) {
if (feature.properties.group == "Violations") {
return {color: "#ff0000"};
}
}
});
Or is this an oversight within leaflet? Also, is there a way to automate this reflection with say toGeoJson(polygons)?
Thanks so much all.
When creating a geoJson layer the coordinates are expected to match the GeoJSON standard (x,y,z or lng, lat, altitude) (GeoJSON position specs)
If you have string of GeoJSON where your coordinates are not in this format, you can create your GeoJSON layer with a custom coordsToLatLng function that will handle this conversion to the standard's format (Leaflet Doc)
If you have a polygon layer and want to add it to an existing GeoJSON feature group you can do something like:
var polygons = L.polygon([
[43.22519, -107.69348],
[42.99259, -105.48523],
[42.26105, -107.7594]
]);
var gg = polygons.toGeoJSON();
var jsonFeatureGroup = L.geoJson().addTo(map);
jsonFeatureGroup.addData(gg);
map.fitBounds(jsonFeatureGroup.getBounds());