Adjusting SVG size for rotating globe - d3.js

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

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.

Zoom world map in D3 upto particular level Around a given latitude and longitude

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.

openseadragon svg overlay - clickable area is too large

I am working with an OpenSeaDragon image with an overlay (array of overlays?) that has about 500 clickable svg rect elements, implemented using the Overlay.onclick() function
The bounding rectangle for the clickable area varies but is always much larger than the visible rectangle, and often covers neighboring rectangles as well. I have tried messing with margin, border, and padding to no avail. This image show an example, showing the difference. The actual displayed rectangle is the same dimension as the visible text box, while the clickable area is the entire highlighted rectangle.
There does not seem to be a lot of predictability - the clickable area varies in an apparently random way but is always larger than the correct size, up to about twice in both directions. As shown, it's not always centered - I'm not sure it ever is. The proportions remain the same when the image is zoomed in and out.
I'm fairly new at JS, and this involves so many components including D3 and the OSD suite, that I'm not sure where to start. Any suggestions would be appreciated!
For reference, here is the code where the boxes are generated from an array. This was adapted from a single rectangle example, and I have no idea if this was a good way to do this. (I would have liked to have the inside of the box fully transparent except when mousing over it, but that's a whole other problem...)
var overlay = this.viewer.svgOverlay();
len = nodes.length;
var d3Rect = [];
var url = [];
for (var i = 0; i < len; i++) {
var mynode = nodes[i];
d3Rect[i] = d3.select(overlay.node()).append("rect")
.style('fill', '#ffffff')
.style('fill-opacity', '0.05')
.style('stroke', '#000066')
.style('stroke-width', '0.0005')
.style('stroke-opacity', '0.5')
.attr("x", mynode.x1)
.attr("width", mynode.width)
.attr("y", mynode.y1)
.attr("height", mynode.height)
.attr("title", mynode.title)
.attr("href", mynode.link);
overlay.onClick(d3Rect[i].node(), function() {
window.open(this.element.getAttribute("href"), '_blank');
});
}
I am not 100% sure, but it looks like you are creating 1 overlay element of unknown size, then adding all the rects inside it, and then binding the click event to the top overlay instead of the individual rects.
In any case, if the shapes you need are simple rects, you should try using
viewer.addOverlay( element, location, placement, onDraw )
link to docs
One possibility is that you're running into precision problems. What are the dimensions of your image in viewport coordinates? By default the width would be 1, but this might cause rounding issues when zoomed in on these SVG elements. Try making your image 1000 wide and multiply all of your overlay coordinates by 1000 and see if that helps.

d3js scale, transform and translate

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

How to position an axes in a figure relative to another axes?

When laying out a figure in MATLAB, typing axis equal ensures that no matter what the figure dimensions, the axes will always be square:
My current problem is that I want to add a second axes to this plot. Usually, that's no problem; I would just type axes([x1 y1 x2 y2]), and a new square figure would be added with corners at (x1, y1), (x2, y2), which is a fixed location relative to the figure. The problem is, I want this new axes to be located at a fixed location relative to the first axes.
So, my questions are:
Does anyone know how I can position an axes in a figure by specifying the location relative to another axes?
Assuming I can do 1, how can I have this new axes remain in the same place even if I resize the figure?
An axis position property is relative to its parent container. Therefore, one possibility is to create a transparent panel with the same size as the first axis, then inside it create the second axis, and set its location and size as needed. The position specified would be as if it were relative to the first axis.
Now we need to always maintain the panel to be the same size/location as the first axis. Usually this can be done using LINKPROP which links a property of multiple graphic objects (panel and axis) to be the same, namely the 'Position' property.
However, this would fail in your case: when calling axis image, it fixes the data units to be the same in every direction by setting aspect ratio properties like 'PlotBoxAspectRatio' and 'DataAspectRatio'. The sad news is that the 'Position' property will not reflect the change in size, thus breaking the above solution. Here is an example to illustrate the problem: if you query the position property before/after issuing the axis image call, it will be the same:
figure, plot(1:10,1:10)
get(gca,'Position')
pause(1)
axis image
get(gca,'Position')
Fortunately for us, there is a submission on FEX (plotboxpos) that solves this exact issue, and returns the actual position of the plotting region of the axis. Once we have that, it's a matter of syncing the panel position to the axis position. One trick is to create a event listener for when the axis changes size (it appears that the 'TightInset' property changes unlike the 'Position' property, so that could be the trigger in our case).
I wrapped the above in a function AXESRELATIVE for convenience: you call it as you would the builtin AXES function. The only difference is you give it as first argument the handle to the axis you want to relatively-position the newly created axis against. It returns handles to both the new axis and its containing panel.
Here is an example usage:
%# automatic resize only works for normalized units
figure
hParentAx = axes('Units','normalized');
axis(hParentAx, 'image')
%# create a new axis positioned at normalized units with w.r.t the previous axis
%# the axis should maintain its relative position on resizing the figure
[hAx hPan] = axesRelative(hParentAx, ...
'Units','normalized', 'Position',[0.7 0.1 0.1 0.1]);
set(hAx, 'Color','r')
And the function implementation:
function [hAx hPan] = axesRelative(hParentAx, varargin)
%# create panel exactly on top of parent axis
s = warning('off', 'MATLAB:hg:ColorSpec_None');
hPan = uipanel('Parent',get(hParentAx, 'Parent'), ...
'BorderType','none', 'BackgroundColor','none', ...
'Units',get(hParentAx,'Units'), 'Position',plotboxpos(hParentAx));
warning(s)
%# sync panel to always match parent axis position
addlistener(handle(hParentAx), ...
{'TightInset' 'Position' 'PlotBoxAspectRatio' 'DataAspectRatio'}, ...
'PostSet',#(src,ev) set(hPan, 'Position',plotboxpos(hParentAx)) );
%# create new axis under the newly created panel
hAx = axes('Parent',hPan, varargin{:});
end
On a completely different note: before you recent edit, I got the impression that you were trying to produce a scatter plot of images (i.e like a usual scatter plot, but with full images instead of points).
What you suggested (from what I understand) is creating one axis for each image, and setting its position corresponding to the x/y coordinates of the point.
My solution is to use the IMAGE/IMAGESC functions and draw the small images by explicitly setting the 'XData' and 'YData' properties to shift and scale the images appropriately. The beauty of this is it require a single axis, and doesn't suffer from having to deal with resizing issues..
Here is a sample implementation for that:
%# create fan-shaped coordinates
[R,PHI] = meshgrid(linspace(1,2,5), linspace(0,pi/2,10));
X = R.*cos(PHI); Y = R.*sin(PHI);
X = X(:); Y = Y(:);
num = numel(X);
%# images at each point (they don't have to be the same)
img = imread('coins.png');
img = repmat({img}, [num 1]);
%# plot scatter of images
SCALE = 0.2; %# image size along the biggest dimension
figure
for i=1:num
%# compute XData/YData vectors of each image
[h w] = size(img{i});
if h>w
scaleY = SCALE;
scaleX = SCALE * w/h;
else
scaleX = SCALE;
scaleY = SCALE * h/w;
end
xx = linspace(-scaleX/2, scaleX/2, h) + X(i);
yy = linspace(-scaleY/2, scaleY/2, w) + Y(i);
%# note: we are using the low-level syntax of the function
image('XData',xx, 'YData',yy, 'CData',img{i}, 'CDataMapping','scaled')
end
axis image, axis ij
colormap gray, colorbar
set(gca, 'CLimMode','auto')
This is usually the sort of thing you can take care of with a custom 'ResizeFcn' for your figure which will adjust the position and size of the smaller axes with respect the the larger. Here's an example of a resize function that maintains the size of a subaxes so that it is always 15% the size of the larger square axes and located in the bottom right corner:
function resizeFcn(src,event,hAxes,hSubAxes)
figurePosition = get(get(hAxes,'Parent'),'Position');
axesPosition = get(hAxes,'Position').*figurePosition([3 4 3 4]);
width = axesPosition(3);
height = axesPosition(4);
minExtent = min(width,height);
newPosition = [axesPosition(1)+(width-minExtent)/2+0.8*minExtent ...
axesPosition(2)+(height-minExtent)/2+0.05*minExtent ...
0.15*minExtent ...
0.15*minExtent];
set(hSubAxes,'Units','pixels','Position',newPosition);
end
And here's an example of its use:
hFigure = figure('Units','pixels'); %# Use pixel units for figure
hAxes = axes('Units','normalized'); %# Normalized axes units so it auto-resizes
axis(hAxes,'image'); %# Make the axes square
hSubAxes = axes('Units','pixels'); %# Use pixel units for subaxes
set(hFigure,'ResizeFcn',{#resizeFcn,hAxes,hSubAxes}); %# Set resize function

Resources