Below is a bit of code that I've written and I'm hoping someone can help me out and explain why it's not responding the way I imagined it would.
I have (quite obviously) pieced this together from a number of examples, docs, etc. online and am using d3.v3.js. I want to better understand exactly what the center, scale, and translate 'attributes' of a projection do, so in addition to the large quantity of reading I've done, I thought I'd write a brief script that allows the user to click a new 'center' for the map - so in effect, you should be able to click this map (made with some data available in the gallery) and recenter the map on a new state/location/etc.
The issue is that every time I set a new center for the data as the inversely projected point that was clicked (that is, invert the point to get the new coordinates to set the center to), the map centers on alaska, then I can click that general area a few times and the world 'rotates' back into view.
What am I doing wrong? I thought this script would help me gain a better understanding of what's going on, and I'm getting there, but I would like a little help from you if at all possible.
<script>
var w = 1280;
var h = 800;
var proj = d3.geo.albers();
var path = d3.geo.path()
.projection(proj);
var svg = d3.select("body")
.insert("svg:svg")
.attr("height", h)
.attr("width", w);
var states = svg.append("svg:g")
.attr("id", "states");
d3.json("./us-states.json", function(d) {
states.selectAll("path")
.data(d.features).enter()
.append("svg:path")
.attr("d", path)
.attr("class", "state");
});
svg.on("click", function() {
var p = d3.mouse(this);
console.log(p+" "+proj.invert(p));
proj.center(proj.invert(p));
svg.selectAll("path").attr("d", path);
});
</script>
You should be able to use projection.rotate()
svg.on("click", function() {
p = proj.invert(d3.mouse(this));
console.log(p);
proj.rotate([-(p[0]), -(p[1])]);
svg.selectAll("path").attr("d", path);
});
Related
I am trying to visualize russians regions. I got data from here, validate here and all was well - picture.
But when I try to draw it, I receive only one big black rectangle.
var width = 700, height = 400;
var svg = d3.select(".graph").append("svg")
.attr("viewBox", "0 0 " + (width) + " " + (height))
.style("max-width", "700px")
.style("margin", "10px auto");
d3.json("83.json", function (error, mapData) {
var features = mapData.features;
var path = d3.geoPath().projection(d3.geoMercator());
svg.append("g")
.attr("class", "region")
.selectAll("path")
.data(features)
.enter()
.append("path")
.attr("d", path)
});
Example - http://ustnv.ru/d3/index.html
Geojson file - http://ustnv.ru/d3/83.json
The issue is the winding order of the coordinates (see this block). Most tools/utilities/libraries/validators don't really care about winding order because they treat geoJSON as containing Cartesian coordinates. Not so with D3 - D3 uses ellipsoidal math - benefits of this is include being able to cross the antimeridian easily and being able to select an inverted polygon.
The consequence of using ellipsoidal coordinates is the wrong winding order will create a feature of everything on the planet that is not your target (inverted polygon). Your polygons actually contain a combination of both winding orders. You can see this by inspecting the svg paths:
Here one path appears to be accurately drawn, while another path on top of it covers the entire planet - except for the portion it is supposed to (the space it is supposed to occupy covered by other paths that cover the whole world).
This can be simple to fix - you just need to reorder the coordinates - but as you have features that contain both windings in the same collection, it'll be easier to use a library such as turf.js to create a new array of properly wound features:
var fixed = features.map(function(feature) {
return turf.rewind(feature,{reverse:true});
})
Note the reverse winding order - through an odd quirk, D3, which is probably the most widespread platform where winding order matters actually doesn't follow the geoJSON spec (RFC 7946) on winding order, it uses the opposite winding order, see this comment by Mike Bostock:
I’m disappointed that RFC 7946 standardizes the opposite winding order
to D3, Shapefiles and PostGIS. And I don’t see an easy way for D3 to
change its behavior, since it would break all existing (spherical)
GeoJSON used by D3. (source)
By rewinding each polygon we get a slightly more useful map:
An improvement, but the features are a bit small with these projection settings.
By adding a fitSize method to scale and translate we get a much better looking map (see block here):
Here's a quick fix to your problem, projection needs a little tuning, also path has fill:#000 by default and stroke: #FFF could make it more legible.
var width = 700, height = 400;
var svg = d3.select(".graph").append("svg")
.attr("viewBox", "0 0 " + (width) + " " + (height))
.style("max-width", "700px")
.style("margin", "10px auto");
d3.json("mercator_files/83.json", function (error, mapData) {
var features = mapData.features;
var center = d3.geoCentroid(mapData);
//arbitrary
var scale = 7000;
var offset = [width/2, height/2];
var projection = d3.geoMercator().scale(scale).center(center)
.translate(offset);
var path = d3.geoPath().projection(projection);
svg.append("g")
.attr("class", "region")
.selectAll("path")
.data(features)
.enter()
.append("path")
.attr("d", path)
});
I am trying to visualize russians regions. I got data from here, validate here and all was well - picture.
But when I try to draw it, I receive only one big black rectangle.
var width = 700, height = 400;
var svg = d3.select(".graph").append("svg")
.attr("viewBox", "0 0 " + (width) + " " + (height))
.style("max-width", "700px")
.style("margin", "10px auto");
d3.json("83.json", function (error, mapData) {
var features = mapData.features;
var path = d3.geoPath().projection(d3.geoMercator());
svg.append("g")
.attr("class", "region")
.selectAll("path")
.data(features)
.enter()
.append("path")
.attr("d", path)
});
Example - http://ustnv.ru/d3/index.html
Geojson file - http://ustnv.ru/d3/83.json
The issue is the winding order of the coordinates (see this block). Most tools/utilities/libraries/validators don't really care about winding order because they treat geoJSON as containing Cartesian coordinates. Not so with D3 - D3 uses ellipsoidal math - benefits of this is include being able to cross the antimeridian easily and being able to select an inverted polygon.
The consequence of using ellipsoidal coordinates is the wrong winding order will create a feature of everything on the planet that is not your target (inverted polygon). Your polygons actually contain a combination of both winding orders. You can see this by inspecting the svg paths:
Here one path appears to be accurately drawn, while another path on top of it covers the entire planet - except for the portion it is supposed to (the space it is supposed to occupy covered by other paths that cover the whole world).
This can be simple to fix - you just need to reorder the coordinates - but as you have features that contain both windings in the same collection, it'll be easier to use a library such as turf.js to create a new array of properly wound features:
var fixed = features.map(function(feature) {
return turf.rewind(feature,{reverse:true});
})
Note the reverse winding order - through an odd quirk, D3, which is probably the most widespread platform where winding order matters actually doesn't follow the geoJSON spec (RFC 7946) on winding order, it uses the opposite winding order, see this comment by Mike Bostock:
I’m disappointed that RFC 7946 standardizes the opposite winding order
to D3, Shapefiles and PostGIS. And I don’t see an easy way for D3 to
change its behavior, since it would break all existing (spherical)
GeoJSON used by D3. (source)
By rewinding each polygon we get a slightly more useful map:
An improvement, but the features are a bit small with these projection settings.
By adding a fitSize method to scale and translate we get a much better looking map (see block here):
Here's a quick fix to your problem, projection needs a little tuning, also path has fill:#000 by default and stroke: #FFF could make it more legible.
var width = 700, height = 400;
var svg = d3.select(".graph").append("svg")
.attr("viewBox", "0 0 " + (width) + " " + (height))
.style("max-width", "700px")
.style("margin", "10px auto");
d3.json("mercator_files/83.json", function (error, mapData) {
var features = mapData.features;
var center = d3.geoCentroid(mapData);
//arbitrary
var scale = 7000;
var offset = [width/2, height/2];
var projection = d3.geoMercator().scale(scale).center(center)
.translate(offset);
var path = d3.geoPath().projection(projection);
svg.append("g")
.attr("class", "region")
.selectAll("path")
.data(features)
.enter()
.append("path")
.attr("d", path)
});
I`m making a globe, that can be rotated. And I need to drag countries and lands together (yes, I know, that I can use only countries, but I need both). But if I do it like that, lands vanishes, and I have no idea what's wrong. But if I comment "Countries!", this code works perfectly.
It's first time working with this, maybe I have a mistake with topojson?
var path = d3.geoPath().projection(projection);
svg.append("path").datum(graticule).attr("class", "graticule").attr("d", path);
//Land
d3.json("https://rawgit.com/jonataswalker/map-utils/master/data/json/world-110m.json", function(error, topo) {
if (error) throw error;
var land = topojson.feature(topo, topo.objects.land);
svg.selectAll("path.foreground").data([land]).enter().append("path").attr("d", path).attr("class", "foreground");
});
//Countries!
d3.json("https://rawgit.com/Bramsiss/Globe/master/world-counries.json", function(collection) {
var countries = svg.selectAll("path").data(collection.features).enter().append("path").attr("d", path).attr("class", "country");
});
//drag
var λ = d3.scaleLinear().domain([0, width]).range([-180, 180]);
var φ = d3.scaleLinear().domain([0, height]).range([90, -90]);
var drag = d3.drag().subject(function() {
var r = projection.rotate();
return {
x: λ.invert(r[0]),
y: φ.invert(r[1])
};
}).on("drag", function() {
projection.rotate([λ(d3.event.x), φ(d3.event.y)]);
svg.selectAll(".foreground").attr("d", path);
});
svg.call(drag);
The issue arises here:
svg.selectAll("path")
.data(collection.features)
.enter().append("path")
.attr("d", path)
.attr("class", "country");
});
svg.selectAll("path") selects one existing path - the world's outline. Your enter selection therefore does not include the first path - it has already been appended. It is part of the update selection. You do set the datum for the first path with this code, but you do not update its shape.
If you look very carefully, Angola has lighter boundaries than the neightbours, this is because instead of two lines (the edges of two countries), the border consists of one line.
Since you only use the enter to create and shape the new elements, the original outline of the earth's land is unchanged. However, when you use the update selection to rotate the earth:
svg.selectAll(".foreground, .country").attr("d", path);
you update the shape of all the paths based on the data appended when you added the countries. Since you replaced the data bound to the first path in the DOM (the outline of the earth's land) with the data for the first item in your countries data array, the path is redrawn according to the new data.
This is why if you drag the earth, the border of Angola changes, if you look at the DOM, you'll also note that this country has the class "foreground", rather than "country".
Solution:
Use a null selection to append countries:
svg.selectAll(null)
or
svg.selectAll()
or
svg.selectAll(".country") // since there are no elements with this class yet.
Here's an updated pen.
I'm new to d3.js. I'm trying to create this choropleth map from scratch:
My script is showing no errors and in elements tab I can see data but nothing is showing on screen. I have read multiple documentations but still don't know what I'm missing.
jsfiddle:
https://jsfiddle.net/jx3hjfgw
var width = 960,
height = 1160;
// SVG element as a JavaScript object that we can manipulate later
var svg = d3.select("#map").append("svg")
.attr("width", width)
.attr("height", height);
var center = [24.679341, 46.680381];
// Instantiate the projection object
var projection = d3.geo.conicConformal()
.center(center)
.clipAngle(180)
// Size of the map itself, you may want to play around with this in
// relation to your canvas size
.scale(10000)
// Center the map in the middle of the canvas
.translate([width / 2, height / 2])
.precision(.1);
// Assign the projection to a path
var path = d3.geo.path().projection(projection);
d3.json("https://code.highcharts.com/mapdata/countries/sa/sa-all.geo.json", function(err, data) {
$.each(data.features, function(i, feature) {
svg.append("path")
.datum(feature.geometry)
.attr("class", "border")
.attr("stroke", "black")
.attr("fill", "blue");
});
});
There are a couple issues with your map.
The primary issue is that your projection is not WGS84, that is to say it does not comprise of longitude/latitude pairs. The projection of the geojson is specified in your geojson itself:
"crs":{"type":"name","properties":{"name":"urn:ogc:def:crs:EPSG:32638"}}
CRS stands for spatial reference system
This projection (EPSG:32638) is a UTM system. D3 uses unprojected data (or data that is 'projected' to WGS84), points on a three dimensional ellipsoid, not already projected points on a planar grid (like UTM). If the geojson did not indicate what projection it used, you can still tell that it is not WGS84, or longitude latitude pairs, because the coordinates are not valid longitude/latitudes:
...[1348,4717],[1501,4754],[1572,4753]...
You have two options, one is to use unprojected data (or data in WGS84) and build your map around a d3 projection. The other is to use a geo.transform to show your data.
Solution 1
As noted in the other answer, you'll need to use the path function to actually display the data, you also shouldn't need an each loop in d3 to append features
For this solution to work, and it is probably the most straight-forward, you'll need to either re-project/un-project the geojson you have (with some tool other than d3) or, alternatively, you'll have to find an new data source (which is probably not too difficult) with the spatial data represented as long/lat pairs.
Also note that coordinates in geojson and d3 are [long,lat] therefore your centering coordinate of [24.679341, 46.680381] refers to 46.7 degrees North, 24.7 degrees East - this point is not in Saudi Arabia but is in Romania.
Take a look at an example projection suitable for Saudi Arabia here (using a basic geojson - just a country outline) (in d3v4 - slight differences from v3).
Together, that would give you something like:
var projection = d3.geo.mercator()
.center([46.680381,24.679341])
.scale(1000)
.translate([width / 2, height / 2])
.precision(.1);
var path = d3.geo.path().projection(projection)
d3.json("source.json", function(err, data) {
svg.selectAll("path")
.data(data.features)
.enter()
.append('path')
.attr("class", "border")
.attr("stroke", "black")
.attr("fill", "blue")
.attr("d", path);
});
Note that if you really want to keep the conical projection (d3.geo.conicConformal()), take a look on centering that type of projection here, it's for an albers projection, but the method is the same as they are both conical projections of the same sort.
Solution 2
The other option is to use a geo.transfrom which will translate and scale your data (since it is already planar in this case) to match your desired view. The downside of this approach is that you can't use a lat/long point to indicate anything - the map units will be the projection units and the map units are not degrees longitude or latitude.
The goal is to translate/scale/shift coordinates such as these:
...[1348,4717],[1501,4754],[1572,4753]...
to [x,y] pixel values that are within your SVG bounds.
This is more complex, you can try to figure out the translate manually (like I demonstrate with your data in this bl.ock, (uses d3v4, which has slightly different method names, but otherwise is the same for this type of operation), or use an automated function to determine the appropriate transform you need to use.
Change your last function to this
svg.append("path")
.data(feature.geometry)
.attr("d", path)
.attr("class", "border")
.attr("stroke", "black")
.attr("fill", "blue");
I think there is something wrong with you geojson file
I have uploaded it to github, which is automatically detecting geojson content and displays it as a map. You can see that only one line is displayed
As already pointed out, you don't have d property defined for path
I have updated your fiddle to match Github result:
var width = 960,
height = 1160;
// SVG element as a JavaScript object that we can manipulate later
var svg = d3.select("#map").append("svg")
.attr("width", width)
.attr("height", height);
var center = [24.679341, 46.680381];
// Instantiate the projection object
var projection = d3.geo.mercator()
.center(center)
.clipAngle(180)
// Size of the map itself, you may want to play around with this in
// relation to your canvas size
.scale(1000)
// Center the map in the middle of the canvas
.translate([width / 2, height / 2])
.precision(.1);
// Assign the projection to a path
var path = d3.geo.path().projection(projection);
d3.json("https://raw.githubusercontent.com/bumbeishvili/Assets/master/Other/junk/sa-all.geo.json", function(err, data) {
debugger;
svg.selectAll('path')
.data(data.features)
.enter()
.append("path")
.attr("d", path)
.attr("class", "border")
.attr("stroke", "black")
.attr("fill", "blue");
});
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<div id="map"></div>
although being pretty new to D3js I already get most of the things up and running. However, what is not yet working, is the display of a geoJSON-based map of inner-city boundaries (data: http://ddj.haim.it/data/muenchen.geojson).
Here's what I have been trying lately:
var nWidth = 800, nHeight = 600,
oMap = d3.select('body').append('svg').attr('width', nWidth).attr('height', nHeight),
oProjection = d3.geo.mercator().center([ 11.591, 48.139 ]).scale(50000).translate([nWidth / 2, nHeight / 2]),
oPath = d3.geo.path().projection(oProjection);
d3.json('data/muenchen.geojson', function(_mError, _oCollection) {
oMap.append('g')
.selectAll('path')
.data(_oCollection.features)
.enter()
.append('path')
.attr('class', 'entity')
.attr('d', oPath);
});
What do I get? A map where only the last featue is drawn correctly and another one is drawn as one huge rectangle (well, in the source, it's not a rect, it's a path).
Any help greatly appreciated.
Thanks a bunch,
Mario
Not sure whether I get your point, Lars. I can see the last polygon from the geoJSON, however, only the last one. Here's the current output: http://ddj.haim.it/d3.html
EDIT: Okay, I got your point. Sorry and thanks a lot for the answer.
The key thing is to "fill" the paths (in the CSS) with transparency.