I have a d3.js graph that is a forced layout design. I have allowed for users to zoom in and out of the graph with bounds set so they can't zoom in past 1 and can't zoom out past 0.1. Right now, when I plot values on the graph, I automatically send them to the center of the graph (based on the height and width of the SVG container). This works fine until I zoom out then zoom in to some where else and plot a new node. The new node will end up back at the original center and not my new relative center.
How I scale when zooming right now:
function onZoom() {
graph.attr("transform", "translate(" + zoom.translate() + ")" + " scale(" + zoom.scale() + ")");
}
I was unable to find any calls to get the current visible coordinates of the graph, but even with those, how would I use them to calculate the relative center of the graph if my SVG graph size always remains static?
I know this post is very old but I found it useful. Below is the update for d3 v5.
var el = d3.select('#canvas').node().getBoundingClientRect();
var z = d3.zoomTransform(svg.node());
var w = el.width;
var h = el.height;
var center = {
x: (z.x / z.k * -1) + (w / z.k * 0.5),
y: (z.y / z.k * -1) + (h / z.k * 0.5)
};
One thing of note, however... is that I found I also needed to divide the pan x/y by the scale factor z.k. Which, you did not do in your formula.
For simple geometric zoom, it's fairly straightforward to figure out the visible area from the visible area dimensions plus the translation and scale settings. Just remember that the translation setting is the position of the (0,0) origin relative to the top left corner of your display, so if translation is (-100,50), that means that top left corner is at (+100,-50) in your coordinate system. Likewise, if the scale is 2, that means that the visible area covers 1/2 as many units as the original width and height.
How to access the current transformation? graph.attr("transform") will give you the most recently set transform attribute string, but then you'll need to use regular expressions to access the numbers. Easier to query the zoom behaviour directly using zoom.translate() and zoom.scale().
With those together, you get
var viewCenter = [];
viewCenter[0] = (-1)*zoom.translate()[0] + (0.5) * ( width/zoom.scale() );
viewCenter[1] = (-1)*zoom.translate()[1] + (0.5) * ( height/zoom.scale() );
I.e., the position of the center of the visible area is the position of the top-left corner of the visible area, plus half the visible width and height.
Related
I have an svg that looks like this:
It's basically a network diagram comprised of nodes and links that has the ability to pan and zoom. Let's say for instance I accidentally drag the entire group off the screen. What is the best way for me to re-center my group so I can see it again?
I've tried to do a transform and changed the scale value, but things will still be outside the view.
this.zoomTrans.scale = this.zoomTrans.scale - .1;
this.container.attr('transform', 'translate(' + this.zoomTrans.x + ',' + this.zoomTrans.y + ') scale(' + this.zoomTrans.scale + ')');
In most situations d3 generates points starting from the origin (0, 0) which is also obviously the default translate. Moving back to (0, 0) should therefore get your back to a visible part of your chart.
To really center your chart, you need to calculate the center point, which is generally done by calculating the minX, maxX, minY and maxY from your points and then calculating (minX + maxX) / 2 and (minY + maxY) / 2 and center that point in your window.
A more advanced solution is to track the translation and limiting it in a way that the chart will never go out of the screen. Again, you need the min and max values for your coordinates. The min horizontal translation is then -maxX and the max horizontal translation is windowWidth + minX. With zoom not equal to 1, it's only about multiplying the limits correctly with the zoom.
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'm trying to determine how far away the camera needs to be from my object3D which is a collection of meshes in order for the entire model to be framed in the viewport.
I get the object3D size like this:
public getObjectSize ( target: THREE.Object3D ): Size {
let box: THREE.Box3 = new THREE.Box3().setFromObject(target);
let size: Size = {
depth: (-1 * box.min.z) + box.max.z,
height: (-1 * box.min.y) + box.max.y,
width: (-1 * box.min.x) + box.max.x
};
return size;
}
Next I use trig in an attempt to determine how far back the camera needs to be based on that box size in order for the entire box to be visible.
private determinCameraDistance(): number {
let cameraDistance: number;
let halfFOVInRadians: number = this.geometryService.getRadians(this.FOV / 2);
let height: number = this.productModelSizeService.getObjectSize(this.viewService.primaryView.scene).height;
let width: number = this.productModelSizeService.getObjectSize(this.viewService.primaryView.scene).width;
cameraDistance = ((width / 2) / Math.tan(halfHorizontalFOVInRadians));
return cameraDistance;
}
The math all works out on paper and the length of the adjacent side of the triangle (the camera distance) can be verified using a^2 + b^2 = c^2. However for some reason the distance returned is 10.4204 while the camera distance I need to show the entire object3D is actually 95 (determined by hard coding the value) which results in only being able to see a tiny portion of my model.
Any ideas on what I might be doing wrong, or better way to determine this. It seems to me like there is some kind of unit conversion that I'm missing when going from the box sizing units to camera distance units
Actual numbers used in the calculation:
FOV = 110 degrees,
Object3D size: {
Depth: 11.6224,
Height: 18.4,
Width: 29.7638
}
So we take half the field of view to create a right triangle with the adjacent side placed along our camera distance, that's 55 degrees. We then use the formula Degrees * PI / 180 to convert 55 degrees into the radian equivalent, which is .9599. Next we take half the object3D width, again to create a right triangle, which is 14.8819. We can now take our half width and divide it by the tangent of the FOV (in radians), this gives us the length for the adjacent side / camera distance of 10.4204.
We can further verify this is the correct length of this side I'll get the length of the hypotenuse using SOHCAHTOA again:
Sin(55) = 14.8819 / y
.8192 * y = 14.8819
y = 14.8819 / .8192
y = 18.1664
Now using this we can use the pythagorean theorem solve for b to check our math.
14.8819^2 + b^2 = 18.1664^2
221.4709 + b^2 = 330.0018
b^2 = 108.5835
b = 10.4203 (we're off by .0001 but that's due to rounding)
The issue ended up being that in THREE.js field of view represents the vertical viewing area. I had been assuming that THREE like Maya and other applications uses Field of View as the horizontal viewing area.
Multiplying the FOV that I was getting by the Aspect Ratio gives me the correct horizontal field of view, which results in a Camera distance of ~92.
I'm working on a map zoom algorithm which change the area (part of the map visible) coordinates on click.
For example, at the beginning, the area has this coordinates :
(0, 0) for the corner upper left
(100, 100) for the corner lower right
(100, 100) for the center of the area
And when the user clicks somewhere in the area, at a (x, y) coordinate, I say that the new coordinates for the area are :
(x-(100-0)/3, y-(100-0)/3) for the corner upper left
(x+(100-0)/3, y+(100-0)/3) for the corner upper right
(x, y) for the center of the area
The problem is that algorithm is not really powerful because when the user clicks somewhere, the point which is under the mouse moves to the middle of the area.
So I would like to have an idea of the algorithm used in Google Maps to change the area coordinates because this algorithm is pretty good : when the user clicks somewhere, the point which is under the mouse stays under the mouse, but the rest of area around is zoomed.
Somebody has an idea of how Google does ?
Lets say you have rectangle windowArea which holds drawing area coordinates(i.e web browser window area in pixels), for example if you are drawing map on the whole screen and the top left corner has coordinates (0, 0) then that rectangle will have values:
windowArea.top = 0;
windowArea.left = 0;
windowArea.right = maxWindowWidth;
windowArea.bottom = maxWindowHeight;
You also need to know visible map fragment, that will be longitude and latitude ranges, for example:
mapArea.top = 8.00; //lat
mapArea.left = 51.00; //lng
mapArea.right = 12.00; //lat
mapArea.bottom = 54.00; //lng
When zooming recalculate mapArea:
mapArea.left = mapClickPoint.x - (windowClickPoint.x- windowArea.left) * (newMapWidth / windowArea.width());
mapArea.top = mapClickPoint.y - (windowArea.bottom - windowClickPoint.y) * (newMapHeight / windowArea.height());
mapArea.right = mapArea.left + newWidth;
mapArea.bottom = mapArea.top + newHeight;
mapClickPoint holds map coordinates under mouse pointer(longitude, latitude).
windowClickPoint holds window coordinates under mouse pointer(pixels).
newMapHeight and newMapWidth hold new ranges of visible map fragment after zoom:
newMapWidth = zoomFactor * mapArea.width;//lets say that zoomFactor = <1.0, maxZoomFactor>
newMapHeight = zoomFactor * mapArea.height;
When you have new mapArea values you need to stretch it to cover whole windowArea, that means mapArea.top/left should be drawn at windowArea.top/left and mapArea.right/bottom should be drawn at windowArea.right/bottom.
I am not sure if google maps use the same algorithms, it gives similar results and it is pretty versatile but you need to know window coordinates and some kind of coordinates for visible part of object that will be zoomed.
Let us state the problem in 1 dimension, with the input (left, right, clickx, ratio)
So basically, you want to have the ratio to the click from the left and to the right to be the same:
Left'-clickx right'-clickx
------------- = --------------
left-clickx right-clickx
and furthermore, the window is reduced, so:
right'-left'
------------ = ratio
right-left
Therefore, the solution is:
left' = ratio*(left -clickx)+clickx
right' = ratio*(right-clickx)+clickx
And you can do the same for the other dimensions.
I have a neat script to draw for me using d3, but sometimes, when I have lots of data some of my nodes go off the div. I could code something to handle this at the co-ordinates level, I guess, but I can amend this easily using zoom and pan manually and was wondering whether there's a good, simple way to have it done automatically.
I can consider any other solution too.
To zoom/pan automatically, you would need to get the extent of your node positions and calculate the scale and offset accordingly. To get the min/max coordinates, you can simply iterate over your nodes. Once you have these, scale and offset can be calculated as follows.
scale = Math.min(width / (maxX - minX), height / (maxY - minY));
where width and height denote the dimensions of the container (i.e. the SVG). Assuming that you're zooming/panning by setting the SVGs transform attribute, this is what you would need to do.
svg.attr("transform",
"translate(" + minX*scale + "," + (-minY)*scale + ") scale(" + scale + ")");
What this does is compute the scale such that the larger of the x/y dimensions fits into the respective dimension of the container and repositions the container such that the top left corner of the extent of node positions corresponds to the top left corner of the container.