Setting projection of d3.geoPath() not working as documented - javascript

I've made a lot of maps in D3 and never had this problem, where the output is so small you can't see it. I've included a small geojson file, which is easily viewable using various other tools (e.g. here).
It appears that the projection is not being set properly with d3.geoPath(). The API specifies that d3.geoPath() takes a projection argument, however when I try to set the projection using this method (as in the example below), the resulting map is not projected properly. It's only after I set the projection using the d3.geoPath().projection() method that I obtain the proper result.
Is this an error in the documentation or am I missing something?
//setup and map parameters
var height = 300;
var width = 300;
var projection = d3.geoAlbers().scale(1000).translate([width/2, height/2]);
var path = d3.geoPath(projection);
//geoJSON data
var geojson =
{"type":"FeatureCollection","features":[
{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-86.910592,33.536105],[-86.907192,33.542204999999996],[-86.90539199999999,33.541305],[-86.901608,33.543456],[-86.895692,33.548805],[-86.89249199999999,33.550404],[-86.888392,33.549304],[-86.879772,33.553267999999996],[-86.868549,33.553418],[-86.8651,33.557134999999995],[-86.856129,33.562064],[-86.85159499999999,33.559404],[-86.848891,33.559805],[-86.852991,33.552104],[-86.85519099999999,33.543904999999995],[-86.855694,33.538151],[-86.860817,33.53405],[-86.850509,33.535693],[-86.83124699999999,33.544767],[-86.827029,33.534708],[-86.82383999999999,33.533389],[-86.826703,33.528081],[-86.826807,33.521107],[-86.822808,33.514319],[-86.81474899999999,33.502386],[-86.81411899999999,33.500078],[-86.816041,33.496234],[-86.823825,33.488839],[-86.825743,33.485408],[-86.82772299999999,33.477393],[-86.828558,33.472626999999996],[-86.82791,33.46967],[-86.833503,33.467552999999995],[-86.839366,33.463845],[-86.841534,33.460941],[-86.842309,33.465699],[-86.849758,33.466302],[-86.861663,33.456759],[-86.869367,33.4514],[-86.87749699999999,33.449382],[-86.884293,33.445453],[-86.88948599999999,33.441072999999996],[-86.890131,33.438437],[-86.899491,33.434508],[-86.908391,33.429308],[-86.922929,33.415512],[-86.925484,33.419897999999996],[-86.93114299999999,33.422801],[-86.93749199999999,33.421608],[-86.945352,33.425982999999995],[-86.946468,33.428407],[-86.937809,33.435314],[-86.925764,33.449968999999996],[-86.920859,33.454651999999996],[-86.92215,33.456643],[-86.927679,33.453849999999996],[-86.932965,33.449332],[-86.935435,33.446132],[-86.94133699999999,33.442150999999996],[-86.941301,33.445651999999995],[-86.942842,33.450103],[-86.946817,33.45017],[-86.95131599999999,33.45181],[-86.950279,33.458104],[-86.945494,33.463234],[-86.929693,33.478538],[-86.92935299999999,33.479858],[-86.920255,33.492874],[-86.92450699999999,33.493904],[-86.93110999999999,33.496843],[-86.933543,33.494935999999996],[-86.94144999999999,33.494743],[-86.944717,33.495934],[-86.946237,33.502279],[-86.946224,33.506071],[-86.941811,33.507621],[-86.937693,33.512206],[-86.92634,33.512381999999995],[-86.917255,33.517139],[-86.915435,33.519832],[-86.908779,33.518958999999995],[-86.903385,33.516537],[-86.898659,33.520165999999996],[-86.894459,33.526136],[-86.896992,33.530504],[-86.910592,33.536105]]]},"properties":{"a":1}}
]}
var svg = d3.select("#map");
svg.selectAll("path").data(geojson.features).enter().append("path").attr("fill","red").attr("d", path);
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg height="300px" width="300px" id="map"> </svg>

You will need to set your projection parameters correctly. A scale factor of 1 000 is much to small to show a small area as in your example. A scale factor of 50 000 might be more appropriate. You also need to properly center your map.
You will need to find a point near the center of your feature to properly center the map. You can do this manually by using google earth, or other methods to find a path centroid in geographic coordinates (not svg coordinates; however, you can find the svg centroid to refine the projection as well by translating features, I am not using that approach in my answer though).
With an Albers projection you should center on the y axis and rotate on the x axis:
var projection = d3.geoAlbers()
.rotate([-x,0])
.center([0,y])
.scale(k)
.translate([width/2,height/2])
You want to use the negative of your central meridian because you rotate the globe under the map. See this answer for a more graphical explanation of an Albers parameters
An Albers projection in d3 has default center and rotate coordinates. If you don't set the center coordinates with both rotate and center, the map will keep default parameters that are intended to show the entire US; this will modify the intended projection. Other projections are generally centered on [0,0] in d3 by default, which is off the coast of Africa.
I eyeballed a center coordinate using google earth to use in this projection. The centering point I am using here is:
86.884 degrees West (-86.884 degrees East)
33.507 degress North
I also zoomed in much further than your original zoom factor, using 100 000 rather than 1 000. Here is the projection showing your data:
//setup and map parameters
var height = 300;
var width = 300;
var projection = d3.geoAlbers()
.scale(100000)
.translate([width/2, height/2])
.rotate([86.884,0])
.center([0,33.507]);
var path = d3.geoPath(projection);
//geoJSON data
var geojson =
{"type":"FeatureCollection","features":[
{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-86.910592,33.536105],[-86.907192,33.542204999999996],[-86.90539199999999,33.541305],[-86.901608,33.543456],[-86.895692,33.548805],[-86.89249199999999,33.550404],[-86.888392,33.549304],[-86.879772,33.553267999999996],[-86.868549,33.553418],[-86.8651,33.557134999999995],[-86.856129,33.562064],[-86.85159499999999,33.559404],[-86.848891,33.559805],[-86.852991,33.552104],[-86.85519099999999,33.543904999999995],[-86.855694,33.538151],[-86.860817,33.53405],[-86.850509,33.535693],[-86.83124699999999,33.544767],[-86.827029,33.534708],[-86.82383999999999,33.533389],[-86.826703,33.528081],[-86.826807,33.521107],[-86.822808,33.514319],[-86.81474899999999,33.502386],[-86.81411899999999,33.500078],[-86.816041,33.496234],[-86.823825,33.488839],[-86.825743,33.485408],[-86.82772299999999,33.477393],[-86.828558,33.472626999999996],[-86.82791,33.46967],[-86.833503,33.467552999999995],[-86.839366,33.463845],[-86.841534,33.460941],[-86.842309,33.465699],[-86.849758,33.466302],[-86.861663,33.456759],[-86.869367,33.4514],[-86.87749699999999,33.449382],[-86.884293,33.445453],[-86.88948599999999,33.441072999999996],[-86.890131,33.438437],[-86.899491,33.434508],[-86.908391,33.429308],[-86.922929,33.415512],[-86.925484,33.419897999999996],[-86.93114299999999,33.422801],[-86.93749199999999,33.421608],[-86.945352,33.425982999999995],[-86.946468,33.428407],[-86.937809,33.435314],[-86.925764,33.449968999999996],[-86.920859,33.454651999999996],[-86.92215,33.456643],[-86.927679,33.453849999999996],[-86.932965,33.449332],[-86.935435,33.446132],[-86.94133699999999,33.442150999999996],[-86.941301,33.445651999999995],[-86.942842,33.450103],[-86.946817,33.45017],[-86.95131599999999,33.45181],[-86.950279,33.458104],[-86.945494,33.463234],[-86.929693,33.478538],[-86.92935299999999,33.479858],[-86.920255,33.492874],[-86.92450699999999,33.493904],[-86.93110999999999,33.496843],[-86.933543,33.494935999999996],[-86.94144999999999,33.494743],[-86.944717,33.495934],[-86.946237,33.502279],[-86.946224,33.506071],[-86.941811,33.507621],[-86.937693,33.512206],[-86.92634,33.512381999999995],[-86.917255,33.517139],[-86.915435,33.519832],[-86.908779,33.518958999999995],[-86.903385,33.516537],[-86.898659,33.520165999999996],[-86.894459,33.526136],[-86.896992,33.530504],[-86.910592,33.536105]]]},"properties":{"a":1}}
]}
var svg = d3.select("#map");
svg.selectAll("path").data(geojson.features).enter().append("path").attr("fill","red").attr("d", path);
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg height="300px" width="300px" id="map"> </svg>

Related

Looking for a world map with a specific projection in d3js

I have played around with the d3js (v5) maps,
i'm trying to generate this map (the screenshot was taken from a random website),
For my particular case there is no need to present Antarctica.
I have read the documentation here: https://github.com/d3/d3-geo#projections,
and followed the instructions and used geoMercator, got this flat map which gets cutoff in the top north for some reason.
What is the correct approach for getting the first map's layout?
any suggestions?
The projection you are looking at is a Mercator projection.
With d3.geoMercator(), the scale value is derived from the circumference of the cylinder that forms the projection surface. The scale value is the number of pixels per radian. The default value anticipates stretching the 360 degrees of the cylinder over 960 pixels: 960/Math.PI/2.
For vertical angular distances, there is no such scaling factor, as one moves to extreme longitudes, the angular distance between points is increasingly exaggerated, such that the poles will be at ± infinity on the y axis. Because of this Mercator's, especially web Mercator's are often truncated at ±~85 degrees. With an extent of [-180,85] and [180,-85], a Mercator is square.
This limit is incorporated into d3-geoMercator, which "Defines a default projection.clipExtent such that the world is projected to a square, clipped to approximately ±85° latitude. (docs)"
This means that if we want to show the full extent of a d3-geoMercator, across 960 x 960 pixels, we can use:
d3.geoMercator()
.scale(960/Math.PI/2) // 960 pixels over 2 π radians
.translate([480,480]) // the center of the SVG/canvas
Which gives us:
The default center of d3-geoMercator is [0°,0°], so if we want [0°,0°] to be in the middle of the SVG/canvas, we translate the center so that it is in the middle, with a translate of [width/2,height/2]
Now that we are showing the whole world, we can refine to show only the portion we want. The simplest method might just be lopping off pixels from the bottom of the svg/canvas. Using the above code with a canvas/svg height of 700 pixels (and keeping 960 pixels across, using the same scale and translate) I get:
I did not remove Antarctica from this image - it just happens that it is cut off without having to filter it out (this is not necessarily ideal practice: it is still drawn).
So, an SVG/Canvas with width 960, height 700, with a projection scale of 960/Math.PI/2 and a translate of [480,480] appears to be ok. These values will scale together for different view port sizes.
With maps, there is often a lot of eyeballing to get the visual effect desired, tweaking projection.translate() or projection.center() can help shift the map to the desired location. But we can do this computationally. I'll speak to one method here, using projection.fitSize() (though this won't solve the required aspect ratio without extra steps).
Project.fitSize([width,height],geojson) takes an array specifying the dimensions of the SVG/canvas and a geojson object and tweaks the projection scale and translate values so that the geojson feature is contained in the SVG/canvas. The geojson feature could be a bounding box of the part of the world you want to show, so you could use:
projection.fitSize([width,height], {
type: "Polygon",
coordinates: [[
[-179.999,84] ,
[-179.999,-57] ,
[179.999,-57] ,
[179.999,84],
[-179.999,84]
]]
})
Where ~84 degrees north is the north end of Greenland and ~56 degrees south is roughly the tip of South America. This will ensure that the entire portion of the world you want to see is visible. However, as noted above, this doesn't consider aspect, so if you constrain the above extent to square dimensions, you'll still be showing the full extent of the Mercator.

Use static image as a world map in java-script

I am trying to render an image like below and want it to act like the world map.
I want to create a javascript function which shows location dynamically into the image when I pass (latitude, longitude) pair.
I have tried google map API documented in "developers.google.com/maps/documentation/javascript/examples/…". But the problem is that image is not in that projection type. So results are so different than expected.
Your image appears to use a plate carreé projection, which is commonly used for raster data sets of the world. This projection easily allows reprojection - and as rioV8 suggests, simply maps longitude and latitude as though they were x,y coordinates on a Cartesian plane rather than a spheroid.
The image you have shared is twice as wide as high, which also helps confirm a plate carreé: if the projection maps longitude and latitude as Cartesian coordinates the image should be twice as wide as high (360 degrees around, 180 degrees pole to pole).
Now we need to make a projection for this (not particularly difficult) or we could use a d3 projection: d3.geoEquirectangular() (plate carreé goes by a few names).
I'll just go over the d3 method here, if using the d3 projection, we need to understand the scale of the projection. The default value takes 2π radians and maps it to 960 pixels, which gives us a scale of: 960/2π. If we want to wrap 2π radians to an arbitrary image width we use a scale of width/2π.
So we could project points relative to this image with:
d3.geoEquirectangular()
.scale(width/Math.PI/2)
.translate([width/2,height/2]) // image width/height so [0,0] is centered
var width =400;
var height = 200;
var projection = d3.geoEquirectangular()
.scale(width/Math.PI/2)
.translate([width/2,height/2]);
var svg = d3.select("svg");
svg.append("circle")
.attr("r", 4)
.attr("fill","yellow")
.attr("transform", "translate("+projection([79.842778,6.934444])+")")
img, svg {
position: absolute;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<div>
<img src="https://i.stack.imgur.com/4p3MK.jpg" width="400" height="200"/>
<svg width="1958" height="929"></svg>
</div>
Of course once we know the projection, we could reproject the original raster image (eg.), and project points onto that.

Constraining zooming and panning in a D3 map viewport

How would I go about fixating the viewport on a D3 (v4) map, such that users cannot zoom and pan outside of this viewport? Say I wanted to fix the viewport on South East Asia, and that my starting point is this fiddle. So pseudo code would be something like:
...constrainViewport([[x0,y0],[x1,y1]])
where x0,y0,x1 and y1 are the coordinates of the bounding viewport rectangle.
I have seen the following block from Sean Connelley however the solution presented is very complicated and out of date (v3). Happy to say I don't understand what is going on there either. Is there really no better (or 'native') way to achieve this result?
You can do this with zoom.translateExtent([a,b]) where a is a coordinate representing the top right and b is a coordinate representing the bottom left of the panning extent (it will constrain zoom when zooming out too).
Since you are working with geographic data, you can set your limits fairly easily in degrees of latitude and longitude:
// extent bounds (arbitrarily chosen)
var topLeft = projection([-180,80]);
var bottomRight = projection([0,-20]);
zoom.translateExtent([topLeft,bottomRight])
Here's an updated fiddle.

Mapbox.js: set map bounds on an area on both side of the date-line

In Mapbox, the fitBounds method doesn't seem to play nicely with points that are on both sides of the Atlantic (where the coords jump from 359 to 0).
How is it possible fit the bounds of Alaska or a flight from HK to SF. It might be possible to code a workaround making use of a center point and a custom zoom-level but it feel like a overengineered workaround around a poor handling of the bounds.
If firBounds is no help, is there another method like that would achieve similar results?
Example of fitBounds around Alaska, since Alaska crosses the antimeridian:
map.fitBounds([[172.461667,71.365162],[-129.979511,51.214183]])
Those cases are always a bit of a pain. What may help is wrapping your bounding box coordinates using:
https://www.mapbox.com/mapbox-gl-js/api/#lnglat#wrap
Another solution might be to convert the bounds to a center & zoom value using:
https://github.com/mapbox/geo-viewport
const {width, height} = map.getCanvas().getBoundingClientRect();
const viewport = geoViewport(
[minX, minY, maxX, maxY], // the bounding box (west, south, east north)
[width, height], // map dimensions in pixel
0, // minzoom
22, // maxzoom
512 // tilesize for mapbox-gl-js
);
map.setCenter(viewport.center);
map.setZoom(viewport.zoom)

How to reduce visible part (or mask) of mapbox style layer?

I have hexagonal tileset added as a layer to mapbox style though mapbox studio.
Trying to find possibility to reduce layer visible area. For example to show only 100m radius (or square with side equal to 100m) area around map center (current point marker).
Is this possible?
You can create a bounding box and use fitBounds method of the map, for example:
const boundingBox = [
[minX, minY],
[maxX, maxY]
];
map.fitBounds(boundingBox);
More about fitBounds and other examples you can find here.
For creating bounding box you can use Turf.js library.
This code uses buffer and bbox methods to create bounding box with 100m side and given point in the center:
const pointBuffer = turf.buffer(point /* - your point GeoJSON */, 0.1, 'kilometers');
const boundingBox = turf.bbox(pointBuffer);

Categories

Resources