Can I turn a D3 map projection clockwise around its centre point? - d3.js

How can I turn my map - if I can - clockwise by about 15-20 degrees, so that is looks like the map of the middle east I would see in an atlas?
Intuitively, .rotate looks like it should do it, but I've tried inserting different values and it just 'uncentres' the map.
The important bit of D3 code, I believe, is:
var projection = d3.geo.albers()
.center([49.7, 27.4])
.rotate([0, 0, 0])
.parallels([12.6, 40])
.scale(800)
.translate([width / 2, height / 2]);
(1)
Thanks.
(2)
I'm trying to replicate a map the looks like this (2) - just because it is what people are familiar with seeing in a regular atlas.

Not knowing what you have tried thus far using projection.rotate() I still think this method will give the desired result. For example, a clockwise rotation by 20 degrees around LAT=49.7N, LON=27.4E as specified in your example could be done by:
projection.rotate([-49.7,-27.4,-20])
I set up a Plunk demonstrating the outcome.
Update
If you are not bound to using the Albers projection, there might be other options giving results which better fit your needs of
what people are familiar with seeing in a regular atlas.
I looked it up in three atlases where the Arabian Peninsula was depicted using the equirectangular projection which looks like your desired output:
var projection = d3.geo.equirectangular()
.rotate([-49.7,-27.4])
You just center on LAT=49.7N, LON=27.4E by applying .rotate([-49.7,-27.4]) without the need to further roll the projection, i.e. you won't have the third element in the array supplied to rotate(). See my updated Plunk. To me this looks like what I would expect it look when seeing it in an atlas.

Related

h3.polyfill misses indices for Antarctica geojson

Could you please help me understand the issue with H3 geospatial indexing?
import h3
geo_antarctic = {"type":"Polygon","coordinates":[[[-170.63764683701507,-85.05113000000047],[-170.63764683701507,-77.89462449499929],[-63.82520589349025,-66.39564184408599],[-49.69216225292467,-77.30460454007437],[-35.16653406678777,-77.89462449499929],[-9.255954059083527,-70.29658532122083],[40.994867774038596,-68.50197979740217],[89.56411960844528,-64.94027568501143],[163.48124599227498,-67.77106116580279],[172.90327508598565,-72.42721956336818],[165.83675326570284,-77.7288586062699],[178.18462781512582,-77.47601087207454],[178.57721236069702,-85.0171471646522],[-178.63764683701507,-85.05113000000047]]]}
idx = h3.polyfill(geo_antarctic, 3)
I'm expecting to get indices like these ones, which are located inside of the geojson polygon above:
83ef9efffffffff
83eea4fffffffff
83f125fffffffff
83f2a4fffffffff
But instead, h3.polyfill returns indices that are “flipped” by 90 degrees like these:
836682fffffffff
830e59fffffffff
83b294fffffffff
836733fffffffff
838f0bfffffffff
830372fffffffff
All works fine for other geojsons that don't span Antarctica..
I’m using Python 3.10.7 and H3 3.7.4.
I would appreciate any hints.
Upd.
I used geo_json_conformant=True parameter and it flipped indices back. But it seems not all resolution 3 indices were generated and my expected indices are not in the list. On the image generated indices are in blue and expected are in red.
Upd 2
Following the suggestion from #nrabinowitz, I triangulated the original polygon from Pole and then polyfilled resulting "slices". Works perfectly fine, all missing indices are in place.
result
import h3
import geojson
geoj = {"type":"Polygon","coordinates":[[[-178.34111242523068,-85.0207089708011],[-178.69267492523034,-77.91567194747755],[-162.52079992523068,-78.4905544838336],[-140.02079992523,-73.8248242864237],[-126.66142492523065,-73.12494935304983],[-103.10673742523004,-74.59011176731619],[-103.45829992523063,-71.07406105104535],[-83.06767492523001,-73.52840349816283],[-61.97392492523001,-64.32087770911836],[-57.052049925230655,-62.43108077917767],[-59.86454992522999,-74.77584672076205],[-39.12236242523063,-77.8418507294947],[-12.052049925230301,-70.61261893331015],[35.05732507477002,-68.52824009786191],[53.33857507476973,-65.51296841598038],[76.54170007476968,-68.39918525054024],[93.06513757477003,-64.77413134111099],[143.69013757477003,-66.08937000336596],[173.22138757477006,-70.72898413027124],[167.94795007477003,-76.26869800825351],[177.79170007476975,-77.23507678015689],[178.60169170931843,-84.94715491814792],[-178.34111242523068,-85.0207089708011]]]}
polygon_coords = geoj["coordinates"][0]
pole_coord = (0.0, -89.999)
all_indexes = set()
for i in range(len(polygon_coords)-1):
polygon = geojson.Polygon([[pole_coord, tuple(polygon_coords[i]), tuple(polygon_coords[i+1])]])
idxes = h3.polyfill(dict(polygon), 5, geo_json_conformant=True)
all_indexes.update(idxes)
with open(f"./absent_polr.csv", "w") as out:
out.write("h3_idx\r\n")
out.write("\r\n".join(all_indexes))
H3 uses a Cartesian model for polygons used in polyfill, not a spherical model, so poles are slightly challenging. The other issue is that we assume the smaller polygon when some of the arcs are greater than 180 degrees, which is frequently the case very near the poles.
See the suggested workaround in this related issue:
I think the best workaround here is to slice up polygons that contain a pole. I think the simplest version of this, which ought to work, is to make triangles, one for each pair of vertexes, with the third vertex being the pole itself.
The other participant in that discussion glossed this as "slicing the polygon up like a pizza," which I thought was very descriptive.
There's a demo of this approach in this Observable notebook.

Pre-projected geometry v getting the browser to do it (aka efficiency v flexibility)

To improve the performance of my online maps, especially on smartphones, I'm following Mike Bostock's advice to prepare the geodata as much as possible before uploading it to the server (as per his command-line cartography). For example, I'm projecting the TopoJSON data, usually via d3.geoConicEqualArea(), at the command line rather than making the viewer's browser do this grunt work when loading the map.
However, I also want to use methods like .scale, .fitSize, .fitExtent and .translate dynamically, which means I can't "bake" the scale or translate values into the TopoJSON file beforehand.
Bostock recommends using d3.geoTransform() as a proxy for projections like d3.geoConicEqualArea() if you're working with already-projected data but still want to scale or translate it. For example, to flip a projection on the y-axis, he suggests:
var reflectY = d3.geoTransform({
point: function(x, y) {
this.stream.point(x, -y);
}
}),
path = d3.geoPath()
.projection(reflectY);
My question: If I use this D3 function, aren't I still forcing the viewer's browser to do a lot of data processing, which will worsen the performance? The point of pre-processing the data is to avoid this. Or am I overestimating the processing work involved in the d3.geoTransform() function above?
If I use this D3 function, aren't I still forcing the viewer's browser
to do a lot of data processing, which will worsen the performance? The
point of pre-processing the data is to avoid this. Or am I
overestimating the processing work involved in the d3.geoTransform()
function above?
Short Answer: You are overestimating the amount of work required to transform projected data.
Spherical Nature of D3 geoProjections
A d3 geoProjection is relatively unique. Many platforms, tools, or libraries take points consisting of latitude and longitude pairs and treat them as though they are on a Cartesian plane. This simplifies the math to a huge extent, but comes at a cost: paths follow Cartesian routing.
D3 treats longitude latitude points as what they are: points on a three dimensional ellipsoid. This costs more computationally but provides other benefits - such as routing path segments along great circle routes.
The extra computational costs d3 incurs in treating coordinates as points on a 3d globe are:
Spherical Math
Take a look at a simple geographic projection before scaling, centering, etc:
function mercator(x, y) {
return [x, Math.log(Math.tan(Math.PI / 4 + y / 2))];
}
This is likely to take longer than the transform you propose above.
Pathing
On a Cartesian plane, lines between two points are easy, on a sphere, this is difficult. Take a line stretching from 179 degrees East to 179 degrees West - treating these as though they were on a Cartesian plane that is easy - draw a line across the earth. On a spherical earth, the line crosses the anti-meridian.
Consequently, in flattening the paths, sampling is required along the route, great circle distance between points requires bends, and therefore additional points.I'm not certain on the process of this in d3, but it certainly occurs.
Points on a cartesian plane don't require additional sampling - they are already flat, lines between points are straight. There is no need to detect if lines wrap around the earth another way.
Operations post Projection
Once projected, something like .fitSize will force additional work that is essentially what you are proposing with the d3.geoTransform(): the features need to be transformed and scaled based on their projected location and size.
This is very visible in d3v3 (before there was fitSize()) when autocentering features: calculations involve the svg extent of the projected features.
Basic Quasi Scientific Performance Comparison
Using a US census bureau shapefile of the United States, I created three geojson files:
One using WGS84 (long/lat) (file size: 389 kb)
One using geoproject in node with a plain d3.geoAlbers transform (file size: 386 kb)
One using geoproject in node with d3.geoAlbers().fitSize([500,500],d) (file size 385 kb)
The gold standard of speed should be option 3, the data is scaled and centered based on an anticipated display extent, no transform is required here and I will use a null projection to test it
I proceeded to project these to a 500x500 svg using:
// For the unprojected data
var projection = d3.geoAlbers()
.fitSize([500,500],wgs84);
var geoPath = d3.geoPath().projection(projection)
// for the projected but unscaled and uncentered data
var transform = d3.geoIdentity()
.fitSize([500,500],albers);
var projectedPath = d3.geoPath()
.projection(transform);
// for the projected, centered, and scaled data
var nullProjection = d3.geoPath()
Running this a few hundred times, I got average rendering times (data was preloaded) of:
71 ms: WGS84
33 ms: Projected but unscaled and uncentered
21 ms: Projected, scaled, and centered
I feel safe in saying there is a significant performance bump in pre-projecting the data, regardless of if it is actually centered and scaled.
Note I used d3.geoIdentity() as opposed to d3.geoTransform() as it allows the use of fitSize(), and you can reflect if needed on the y: .reflectY(true);

What does cluster.size do in D3JS?

I am trying to create a graph based on Mike Bostock's Heirarchical Edge Bundling(here is the gist). I need to make my JSON look as readme-flare-imports.json looks, but I can't figure out what "size" is. I read the API and it didn't seem to help me. Also, it will be a dynamic JSON file based on a mySQL database, so I won't be able to set the size myself. Is anybody able to clear things up for me as to what it is or how I may be able to determine what the size should be? Thank you in advance!
cluster.size determines how large of an area the cluster will take up. You pass values to it like so
// The angle
var x = 360;
// The radius
var y = window.height / 2;
cluster.size([x, y])
x will determine how much of a circle the cluster will use to branch out children. A value of 360 will use the entire circle to display all values. A value of 180 will only use half the circle to branch out values.
y will determine how wide the circle will become in any single direction, i.e., the radius of the circle.
In the Heirarchical Edge Bundling example, I believe the size attribute in the json file is ignored as I could not find anything in the code that cared about it.

Drawing a radius in D3

Apologies if this is painfully simple, I have tried searching for a solution.
In D3, I can perform an arc by defining this:
var ringBannerCcwArc = d3.svg.arc()
.innerRadius(420)
.outerRadius(470)
.startAngle(0)
.endAngle(function(t) {return t * -1 * 2 * Math.PI / 6; });
and then defining it in the DOM here:
labels.append("path")
.attr("id", "ring-banner");
And then at the appropriate time I can do:
labels.transition.select("#ring-banner").style("fill", "red")
.attrTween("d", function() { return ringBannerCcwArc });
And that will produce a red coloured arcing "label" starting at 0 and stopping at whatever
t * -1 * 2 * Math.PI / 6
produces as an angle (yes, 60 degress, but I intend for it to be a variable result).
What I would like to do is simply create a line that extends outward from this banner on the radius of the "endAngle" (so that I can build a dynamically driven display).
Something like this image:
My goal is to dynamically attach data to this and employ the amazingness of D3 to it. So once I understand how to draw the above solution, I would then like to know the resulting coordinates of the end line (x2,y2). The line itself can be a set length (say, 50) but how would I determine it's position so that I could attach a final area to it?
Something like this image:
Again, I apologize if this seems obvious. Thanks for any help.
EDIT:
The original question was regarding my use of D3 - creating a ringBannerArc - and then wanting to tap into the coordinate system. So, as the two respondents state, it is a basic trig problem but that's not really the question.
In the end, the answer is that d3 does not do what I was asking. But it can easily perform the solution. If, like me, you are struggling with implementing d3 and understanding it's very unique (and extremely powerful) approach to data visualization then you might find these links helpful. Thanks to the guys over at the d3 Google Group.
Helpful contributors:
Ian Johnson:
First you want to know how to draw a line from one point to another. That's the end result you want and if you can't do it for arbitrary lines you can't do it for the line you want. so lets start there:
http://tributary.io/inlet/4229462/ The second part is calculating the point on the circle you want to draw. But before you do that, you should set things up so you can verify easily where that point is. so lets draw the overall circle, and then draw a point we can use:
http://tributary.io/inlet/4229477/ Now lets try to place that point at some point on the circle based on an input radius using trig:
http://tributary.io/inlet/4229496/ once we can control that point, we come full circle ;) and move the line http://tributary.io/inlet/4229500/
Chris Viau: Wrapped it up in a nice helper function: http://jsfiddle.net/christopheviau/YPAYz/ Suggested a great book for beginners written by Scott Murray: http://ofps.oreilly.com/titles/9781449339739/index.html
Scott Murray: Makes a wonderful reference to a white paper written by the d3 authors - for those of us who like to understand the nuts and bolts: http://vis.stanford.edu/files/2011-D3-InfoVis.pdf
This is essentially a basic trigonometry question.
For a circle, if the angles start from vertical and go clockwise, and your coordinates are normal screen coordinates,
x = cx + sin(angle) * r
y = cy + cos(angle) * r
From these, you can then compute either line simply.
First you want to know how to draw a line from one point to another. That's the end result you want and if you can't do it for arbitrary lines you can't do it for the line you want.
The second part is calculating the point on the circle you want to draw. But before you do that, you should set things up so you can verify easily where that point is. so lets draw the overall circle, and then draw a point we can use:
http://tributary.io/inlet/4229477/
Now lets try to place that point at some point on the circle based on an input radius using trig, once we can control that point, we come full circle ;) and move the line
http://tributary.io/inlet/4229500/

d3js - how to set albers projection properly?

I have some geojson data for Japan, which I managed to position properly on a mercator projection, but I'm a bit lost as to how to position it properly using an albers projection, other than trial and error.
Is there a good tool to use?
blocks example: http://bl.ocks.org/4043986
long, lat for japan (wikipedia):
latitudes 24° - 46°N,
longitudes 122° - 146°E.
geojson link: https://gist.github.com/raw/4043986/f53b85ab0af1585cd0461b4865ca4acd1fb79e9f/japan.json
As of now, it's the version 3 of D3.js.
It might be worth looking at the original source albers.js at github, which contains :
d3.geo.albers = function() {
return d3.geo.conicEqualArea()
.parallels([29.5, 45.5])
.rotate([98, 0])
.center([0, 38])
.scale(1000);
};
Now, d3.js use combination of projection.rotate and projection.center to place center of the projection to long 98°W, lat 38°N (around Hutchinson, Kansas).
From Geo Projections API,d3.geo.conicEqualArea()
.parallels([29.5, 45.5]) sets the Albers projection’s two standard parallels latitudes 29.5°N and
45.5°N, respectively. But what is two standard parallels?
To understand what parallels setting is, one need to know that Albers projection is a kind of conic projection.
A conic projection projects information from the spherical Earth to a cone that is either tangent to the Earth at a single parallel, or that is secant at two standard parallels.
Choosing the best standard parallels setting seems to be a subtle task, of which the goal is to minimize the projection distortion when mapping between surfaces. Anyway, choosing the two values to be closed to a country top/bottom edges is intuitively good, as it helps minimize the distance between the [conic/sphere] surfaces enclosing a country.
I found the answer looking through the repository - the tool is right there!
clone d3.js from the github repository.
edit /d3/examples/albers.html line 53 to point at your GEOJSON file:
Put the origin long / lat sliders to the center of your country / region (for me, it was 134° / 25°)
Change the paralells to be as close to the edges of your country / region.
adjust scale & offset to a nice size & position.
There are similar tools for the other projections.
edit: The repository has changed (and is constantly changing), so I've created a gist to preserve the example: https://gist.github.com/4552802
The examples are no longer part of the github repository.

Resources