d3js scale, transform and translate - d3.js

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).

Related

three-globe SphereBufferGeometry/Mesh is offset on globe, but lines up if flat

I have a three-globe, and lat/long points perfectly go to the correct locations. The base (Earth) map is 1600x800.
However, I also have a RainViewer map (storm radar) which is square (4096x4096). If I scale that to 1600x1600 and overlay the Earth map, it fits perfectly lined up (top 800 and bottom 800 are outside the boundaries, but that is blank anyway, so perfect).
When I use the TextureLoader/SphereBufferGeometry/MeshPhongMaterial/Mesh, and add it to the scene, it locates itself completely in the wrong spot. No amount of rotateX/Y/Z, or phi/theta shifting seems to work to get it to position correctly.
How can one map this correctly on the globe?
Relevant code (url hardcoded to a timestamp for clarity):
this.myGlobe = new ThreeGlobe()
.globeImageUrl(myImageUrl)
.polygonsData(this.polyData)
.pointsData(gData)
.pointColor('color');
const renderer = new THREE.WebGLRenderer();
console.log('width=' + width);
renderer.setSize(width, width / 2);
document.getElementById('globeViz').appendChild(renderer.domElement);
const myScene = new THREE.Scene();
myScene.add(this.myGlobe);
myScene.add(new THREE.AmbientLight(0xbbbbbb));
myScene.add(new THREE.DirectionalLight(0xffffff, 0.6));
const camera = new THREE.PerspectiveCamera();
camera.aspect = 2; //window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
camera.translateZ(300);
const globeMaterial = new THREE.MeshPhongMaterial();
globeMaterial.bumpScale = 10;
new THREE.TextureLoader().load('//unpkg.com/three-globe/example/img/earth-water.png',
texture => {
globeMaterial.specularMap = texture;
globeMaterial.specular = new THREE.Color('grey');
globeMaterial.shininess = 15;
});
this.myGlobe.globeMaterial = globeMaterial;
new THREE.TextureLoader().load('https://tilecache.rainviewer.com/v2/radar/1652860800/4096/2/0_1.png',
cloudsTexture => {
const geo = new THREE.SphereBufferGeometry(this.myGlobe.getGlobeRadius() * (1 + 0.004), 80, 80);
const mesh = new THREE.MeshPhongMaterial({ map: cloudsTexture, transparent: true });
const weather = new THREE.Mesh(geo, mesh);
myScene.add(weather);
});
Correct placement:
In color (harder to see) to show apples-to-apples:
Incorrect placement when Globified:
I believe Marquizzo is correct in the comments, one of the projected images is rotated 90 degrees (plus or minus, but probably minus in your case) compared to the other. Since you said that your earth map is not rotated at all, this means the RainViewer map is.
This is consistent with how other NASA weather maps I recently projected on my own Earth globe had to be dealt with - in my case, the cloud cover simulation movie applied on the globe started with the prime meridian aka 0 degree of longitude to the left side of the image (instead of being positioned in the horizontal middle of the image as its customary in nearly all maps), and I'm guessing something similar is happening here, except for the direction of the angle needed to make it look right.
The assumption is supported by the fact that in your screenshots, the big orange spot that should be positioned close to the North American Great Lakes (i.e. 90 degrees West) is placed precisely on the prime meridian (i.e. 0 degrees of longitude). Yup, I know this thanks to my own globe... :)
To (partially, see below) fix this, you should construct your geometry so that the phiStart parameter of the constructor is set to the correct rotation angle, something like:
const geo = new THREE.SphereBufferGeometry(this.myGlobe.getGlobeRadius() * (1 + 0.004), 80, 80, - Math.PI / 2);
This will project the map starting from 90 degrees to the "left" as its left side, if this makes sense.
That being said, I don't think this is the entire extent of the issue, because that orange spot is also displaced at around 23 degrees of latitude North (i.e. at the Tropic of Cancer in your Globified screenshot) compared to the correct 46 degrees of latitude North (i.e. more or less where the left side of Lake Superior lies). This fits well with the fact that the projected image is a 1600 x 1600 px square, instead of an expected 1600 x 800 px rectangle, as the most probable cause of the latitudinal aka vertical displacement, so you might want to appropriately "crop" the RainViewer map to have the expected 2:1 horizontal to vertical size that's expected from a plane projection on a sphere. You could probably use the thetaStart and thetaLength parameters of the sphere geometry constructor to adjust things here as well, if that yields what you want.
Or, it might just be that both the longitudinal and latitudinal displacements are somehow caused by the usage of a 1600 x 1600 px square image source instead of a 1600 x 800 px one. The cause of the issue shouldn't affect the way it can be fixed though.

d3 floating grouped bar with ranged values in a timeline

im trying to understand what tools i need to use as im new to d3 and didnt find any thing related...
i need a area chart that is like bars but can float and be on multiple values both on the x and y axis.
in this example the values are days but it might be hours/months etc...
need to know the direction i need to go.. / the right term to search...
There's no significant difference between drawing this chart and a normal bar chart.
And you need to define some scales that will map the values in your data to co-ordinates on your chart.
You need to draw some rect shapes.
So, in the above example you would define a time scale that, given an input date, will map that to a certain x co-ordinate on your chart. You can then use that to determine both the x co-ordinate for where the left-hand-side of a rectangle will be, and to work out how wide the rectangle needs to be.
const xScale = d3.scaleTime()
.domain([d3.min(dateValuesInMyDataset, d => d.date), d3.max(dateValuesInMyDataset, d => d.date)])
.range([0, widthOfMyChart]);
The above xScale if given the earliest date in your dataset would return the value 0, because this is the x co-ordinate representing that date.
Similarly, you would want to construct a linear scale which defines how to map the numerical range of values in your dataset, to the y co-ordinates in your chart. Then you can use the scale to determine the y value and height of all of the rectangles in your chart.
There are lots of good examples of this on ObservableHQ.com that you can browse and see the code for.

Adjusting SVG size for rotating globe

In http://bl.ocks.org/ee2dev/71316923a9cd9fb4314a,
you see a rotating globe covering a 250 x 250 area.
How do I need to change my code to set the surrounding SVG to let's say 300 x 250 (= the size of my globe + some horizontal space for the rotating city labels) ?
Any help would be greatly appreciated!
You're going to have to apply a translate to your projection. As per https://github.com/mbostock/d3/wiki/Geo-Projections#translate you'll see that the projections have a default translate suitable for an svg of 960 x 500. Since your existing example has that size, the default translate matches perfectly.
Change your projection code to:
var width = 300,
height = 250;
...
var projection = d3.geo.orthographic()
.scale(125)
.translate([width/2, height/2])
.clipAngle(90);
Note the inclusion of the translate call to the projection which changes from the default to the new one based on the size of the svg.
Working example at http://bl.ocks.org/benlyall/272235d004c7afc8dc68

How does D3's projection function work for paths and points?

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.

transformation issues when implementing fisheye distortion in a radial tree

Basically, I am attempting to apply the d3 fisheye distortion algorithm to a radial tree. I believe the issues I am encountering revolve around the fact that the coords being fed to the fisheye distortion are the coords computed by the d3.layout.tree. But the actual coords have been adjusted by the g transform. So, the coords resulting from the fisheye distortion need to be adjusted back to the g transform.
For example:
// re-setting the projection according to fisheye coords
diagonal.projection(function(d) { d.fisheye = fisheye(d); return [d.fisheye.y, d.fisheye.x / 180 * Math.PI]; })
I have been attempting this...here is the fiddle.
I am somewhat close...help is appreciated.
Following the direction I'd suggested in the comments, this is the result:
https://jsfiddle.net/xdk5ehcr/
Instead of using rotations and translations to position the nodes, I created two trigonometry-based functions to calculate horizontal and vertical position from the data (x,y) values, which are treated as polar coordinates.
Then I had to set the fisheye function to use my positioning functions as "accessor" functions instead of reading d.x and d.y directly. Unfortunately, the basic plug-in you were using for the fisheye didn't include a way to get and set x/y accessor functions, so I had to modify that too. I was surprised it wasn't already in the code; it's a standard functionality on most d3 layout objects.
(When I get github set up, I will have to make a pull request to add it in. I'll need to figure out how the fisheye scale/zoom function works, though -- I took that out of this example since you weren't using it.)
The positioning functions were as follows:
function getHPosition(d){
//calculate the transformed (Cartesian) position (H, V)
//(without fisheye effect)
//from the polar coordinates (x,y) where
//x is the angle
//y is the distance from (radius,radius)
//See http://www.engineeringtoolbox.com/converting-cartesian-polar-coordinates-d_1347.html
return (d.y)*Math.cos(d.x);
}
function getVPosition(d){
return (d.y)*Math.sin(d.x);
};
The functions are used to set the original position of the nodes and links, and then once the fisheye kicks in it uses these functions internally, returning the results (with distortion if appropriate) as d.fisheye.x and d.fisheye.y.
For example, for links that means the projection setting the d3.svg.diagonal function like this for initialization:
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [getHPosition(d), getVPosition(d)];
});
But like this for update:
diagonal.projection(function(d) {
d.fisheye = fisheye(d);
return [d.fisheye.x, d.fisheye.y];
});
There are a couple other little changes:
I simplified the dimensions of the plotting area a bit.
I added a background rectangle with pointer-events:all; so that the fisheye doesn't turn on and off as the mouse moves between nodes and empty background.
I didn't bother rotating the text (since the node groups are no longer rotating, it doesn't happen by default), but you could easily add in a rotate transformation on the individual text elements.
Finally, and this one stumped me for longer than I'd like to admit, the angles have to be in radians for the Javascript trig functions. Couldn't figure out why my layouts were so ugly, with overlapping lines. I thought it was something to do with switching between d3.svg.diagonal() and d3.svg.diagonal.radial() and spent a lot of time trying to do inverse-trig and all sorts of things...

Resources