This is now driving me insane. I've read pretty much all the posts on this, and still cannot work out how to do it efficiently, i.e. not trial and error.
I have a map of Sweden, and I basically want to zoom into the map using a lat/lon position as the center point (14.4W, 57.2N). My projection variable settings are as follows:
var projection = d3.geo.albers()
.center([0, 57.2])
.rotate([14.4, 0])
.parallels([50, 60])
.scale(900 * 3)
.translate([width / 2, height / 2]);
At the default scale (150), the map centers properly. When I try to scale up however, I don't understand why the 14.4W, 57.2N position is not remaining in the center of the canvas?
Can anyone please shed some light? The full Gist code is here Sweden map Gist and the block is here Sweden map block
Related
Is there any way that I can focus into d3 world Map around a specific latitude and longitude on load of file.
Here is working plunker in which I can zoom around a d3 world Map.
plunker
Below code is used to zoom in for click.
function clicked() {
currScale2 = projection.scale();
if(beforeClickValue == 0)
beforeClickValue = 150;
beforeClickValue = beforeClickValue + 100;
projection.scale(beforeClickValue);
g.selectAll("path").attr("d", path);
}
I need to zoom in near or around Kenya, if I provide a particular location in Kenya, eg:
Latitude 0.55378653650984688
Longitude 35.661578039749543
If your centering point is determined by a feature
If your point is a feature centroid, then you can automatically center your map using that feature:
There are a few ways to achieve this, one would be to set your projection to be centered on your features:
projection.fitSize([width,height],geoJSONKenyaTurkana);
fitSize takes the width and height of a bounding box - your svg - and sets the scale and translate of the projection to maximize the size of the features within that bounding box. .fitExtent will allow a bit more flexibility regarding margins:
projection.fitExtent([[10,10],[width-10,height-10]],geoJSONKenyaTurkana);
This will provide margins of 10 pixels: the first coordinate is the top left of the bounding box, while the second coordinate is the bottom right.
After setting your projection to be centered with either method, then you can append the features - your zoom constraints, however, will be relative to this starting point - as you have zoomed in on the projection. Here's a plunkr with this approach (using fitSize):
https://plnkr.co/edit/E7vqcwwISmmxUarCsWvw?p=preview
I've used your featureCollection as the feature, but you could center it on an individual feature in the feature collection.
Alternatively, and possibly more in line with your title, you can use a zoom identity to set the intitial zoom factor with d3.zoom, this manipulates the svg rather than the projection and uses your zoom function:
var bounds = path.bounds(geoJSONKenyaTurkana),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
x = (bounds[0][0] + bounds[1][0]) / 2,
y = (bounds[0][1] + bounds[1][1]) / 2,
scale = .9 / Math.max(dx / width, dy / height),
translate = [width / 2 - scale * x, height / 2 - scale * y];
svg.call(_zoom.transform, d3.zoomIdentity
.scale(scale)
.translate(translate[0]/scale,translate[1]/scale)
);
This gives us something that looks like this:
https://plnkr.co/edit/CpL4EDUntz853WzrjtU0?p=preview
If you want to manually set a centering point
If however, you want to set your map to be centered according to a manually set point, you can accomplish this much the same way as above: modifying the projection, or modifying the zoom:
To modify the projection, you can use .center() which takes a coordinate and centers the map on this point:
projection.center([longitude,latitude])
Of course, points don't have area, so you will have to set the scale factor yourself, the value will depend on what you want to show:
projection.center([longitude,latitude]).scale(k);
Larger values are more zoomed in.
Alternatively, to manipulate the zoom function, we can use something like:
var x = projection([35.661578039749543,0.55])[0],
y = projection([35.661578039749543,0.55])[1],
scale = 20,
translate = [width / 2 - scale * x, height / 2 - scale * y];
svg.call(_zoom.transform, d3.zoomIdentity
.scale(scale)
.translate(translate[0]/scale,translate[1]/scale)
);
As with setting the projection to center on a specific point, you'll need to set a scale value manually. Here I've arbitrarily chosen 20.
I created a simple worldmap with d3 as you can see here: http://bl.ocks.org/wiesson/ef18dba71256d526eb42
Is there a simple way to shift the small part of russia (as illustrated in the picture) to the right, without creating a new topojson? If not, any other idea?
Okay, the answer was straightforward. As explained in the api docs, the method rotate can turn the map.
So, rotate([-11, 0]) "rotated" the map in the position I was looking for.
var projection = d3.geo.mercator().scale(width / 2 / Math.PI)
.rotate([-11, 0])
.translate([(width) / 2, height * 1.35 / 2])
.precision(.1);
My understanding of D3's projection functions are that they do the same thing as the scale functions. They map GPS coordinates to pixel coordinates. This is the projection I'm currently using. (I don't understand all the variables in detail but I fiddled around with them until the map showed up)
var projection = d3.geo.albers()
.center([-122.436269,37.798107])
.rotate([0, 0, 0])
.parallels([36, 38])
.scale(300000);
This draws the map fine:
.attr("d", d3.geo.path().projection(projection))
When I try to plot points though the numbers are crazy.
.attr("cx",function(d) {
return projection([d._lon,d._lat])[0];
})
.attr("cy",function(d) {
return projection([d._lon,d._lat])[1];
});
How do I properly do this?
Here are some examples of the points I'm getting through the projection function:
[5175.3799972560955, 1808.5108650794136]
[5158.315547249338, 1823.564395627589]
[5143.958532762888, 1831.9879789081751]
On a 1280x800 screen these are way off. Even if I scale them by dividing by 100, they'll still mostly just stack on top of each other. I have a 700*700 svg positioned using twitter bootstrap. Not sure how those are taken into account. I just assumed that if the map if fine, then the same projection should work for the points.
I'm trying to recreate a map of the Netherlands from Mike Bostock's tutorial. I get the first step done and actually see the smaller map. But when I go to the second step and try to change the projection everything is blank. Is it possible that this is caused by the data including the Dutch Antilles? They are so far it apart that I might be looking at the ocean?
My files
It seems to me that setting the center attribute on the projection was messing things up, it might be because your data was crossing date line? I notice the the bounding box stretch way off to the south and west.
My approach was to set the scale and translation based on one of the centrally located provinces. The scale and translation were automatically calculated based on Mike's code in this stack overflow question. The idea is to use a unit projection and then to calculate the bounds of a given feature. These bounds are then used to calculate appropriate scale and translation (avoiding using the center attribute). The projection is then updated using the recalculated scale and transform.
The relevant code is:
//select a province to center on
var l = topojson.feature(nld, nld.objects.subunits).features[8],
//calculate the bounds
b = path.bounds(l),
//calculate the scale based on the bounds (note that I've set the proportion to
//less than one so you can see all of the Netherlands)
s = .125 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height),
//calculate the translation based on the bounds
t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];
//update the projection
projection
.scale(s)
.translate(t);
And you can find an example here
I've created nycMap, a project that uses angularJS (MVC), yeoman (build), d3 (mapping) and geoJSON (geo data).
Everything works very nicely, but I did have to spend quite some time getting the right scale and translation. I was wondering how I can automatically figure out at what scale the map will show its best and what x and y values go into the translation?
'use strict';
japanAndCo2App.controller('MainCtrl', function($scope) {
function makeJapanAll(){
var path, vis, xy;
xy = d3.geo.mercator().scale(16000).translate([-5600,2200]);
path = d3.geo.path().projection(xy);
vis = d3.select("#japanAll").append("svg:svg").attr("width", 1024).attr("height", 700);
d3.json("data/JPN_geo4.json", function(json) {
return vis.append("svg:g")
.attr("class", "tracts")
.selectAll("path")
.data(json.features).enter()
.append("svg:path")
.attr("d", path)
.attr("fill",function(d,i){ return d.properties.color || "transparent"});
});
}
makeJapanAll();
});
(If you are interested in the code, it's all on github. The code for the map is in scripts/controllers/main.js which is the same as shown above.)
I've had the same problems. But it is very easy to do when you have a bounding box, which can be determined from the GeoJSON (like meetamit said), or while creating the GeoJson. And the width of the wanted SVG.
I'll start with the variables lattop, lonleft, lonright, width and height for the bounding box of the geojson and the dimensions of the image. I haven't yet occupied myself with calculating a good height from the difference in latutude. So the height is just estimated to be big enough to fit the image. The rest should be clear from the code:
var xym = d3.geo.mercator();
// Coordinates of Flanders
var lattop = 51.6;
var lonleft = 2.4;
var lonright = 7.7;
var width = 1500;
var height =1000;
// make the scale so that the difference of longitude is
// exactly the width of the image
var scale = 360*width/(lonright-lonleft);
xym.scale(scale);
// translate the origin of the map to [0,0] as a start,
// not to the now meaningless default of [480,250]
xym.translate([0,0]);
// check where your top left coordinate is projected
var trans = xym([lonleft,lattop]);
// translate your map in the negative direction of that result
xym.translate([-1*trans[0],-1*trans[1]]);
var path = d3.geo.path().projection(xym);
var svg = d3.select("body").append("svg").attr("width",width).attr("height",height);
Note, if you go over the date line (180 degrees), you will have to take the overflow into account.
Given this:
xy = d3.geo.mercator().scale(someScale).translate([0, 0]);
someScale is the pixel width of the entire world when projected using the mercator projection. So, if your json data had outlines for the whole world – spanning from lat/lng -180,90 to latLng 180,-90 – and if someScale was 1024, then the world would be drawn such that it exactly fits within a 1024x1024-pixel square. That's what you see on in this Google Maps view (well... sort of... not quite... read on...).
That's not enough though. When the world is drawn at 1024px, without any translation, lat/lng 0,0 (i.e. the "middle" of the world) will sit at the 0,0 pixel of the projected map (i.e. the top left). Under these conditions, the whole northern hemisphere and western hemisphere have negative x or y values, and therefore fall outside the drawn region. Also, under these conditions, the bottom right of the world (i.e. lat/lng -90, 180) would sit at the exact middle of the 1024x1024 square (i.e. at pixel 512,512).
So, in order to center the world in the square described here, you need to translate the map by half its width in the X and Y directions. I.e. you need
xy = d3.geo.mercator().scale(1024).translate([512, 512]);
That'll give you exactly the Google Map view I linked to.
If your json data only has part of the world (like, nyc or NY state) drawing it with this xy projection will render the outlines in the correct geographic position relative to the entire 1024x1024 world-spanning region. So it would appear rather small, with lots of whitespace.
The challenge is how to scale and translate the projection such that the area in question fills up the 1024x1024 square. And... so far I haven't answered this question, but I hope that this explanation points you in the right direction towards figuring out this math. I'll also try to continue the answer later, when I have more time. :/
There's an example here that gets the bounds of countries from geojson and then scales and translates the map to that country. The code is a bit ugly; there're however efforts to make this easier in the future (see this and this issue).