D3js v5 Zooming to Bounding box on geoMercator().fitSize() - javascript

I use this as reference: https://bl.ocks.org/iamkevinv/0a24e9126cd2fa6b283c6f2d774b69a2
Adjusted some syntax to fit for version 5
Scale works, Translate looks like it works too because if I change the value, it zooms on different place..
But the problem is it doesn't zoom on the correct place I clicked.
I think this doesn't get to the place correctly because I use d3.geoMercator().fitSize([width, height], geoJSONFeatures) instead:
var bounds = path.bounds(d),
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 = Math.max(1, Math.min(8, 0.9 / Math.max(dx / width, dy / height))),
translate = [width / 2 - scale * x, height / 2 - scale * y];
Already tried to change the values to fit mine but failed, I can't get it.
Here is my projection:
var width = 500;
var height = 600;
d3.json("/regions50mtopo.json")
.then((geoJSON) => {
var geoJSONFeatures = topojson.feature(geoJSON, geoJSON.objects["Regions.50m"]);
// My Projection
var projection = d3.geoMercator().fitSize([width, height], geoJSONFeatures);
...
Any help, guide or reference?
Note: I'm mapping different country and fitSize(...) solves the
problem easily to fit on my svg that's why I can't use the same as in
the reference link I provided.

Found an answer: https://bl.ocks.org/veltman/77679636739ea2fc6f0be1b4473cf03a
centered = centered !== d && d;
var paths = svg.selectAll("path")
.classed("active", d => d === centered);
// Starting translate/scale
var t0 = projection.translate(),
s0 = projection.scale();
// Re-fit to destination
projection.fitSize([960, 500], centered || states);
// Create interpolators
var interpolateTranslate = d3.interpolate(t0, projection.translate()),
interpolateScale = d3.interpolate(s0, projection.scale());
var interpolator = function(t) {
projection.scale(interpolateScale(t))
.translate(interpolateTranslate(t));
paths.attr("d", path);
};
d3.transition()
.duration(750)
.tween("projection", function() {
return interpolator;
});
Exactly what I'm looking for. It works now as expected.
But maybe somebody also have suggestions on how to optimise it, because as the author said too, it feels slow and "laggy" when zooming in/out.

Related

Initiate d3 map over certain area given latitude and longitude

I am building a map in d3 and basing it off of this codepen by Andy Barefoot: https://codepen.io/nb123456/pen/zLdqvM?editors=0010. I want to modify the initiateZoom() function so that if I set the lat/lon coordinates for a box surrounding say Ohio, the map will initialize its panning to be over Ohio.
function initiateZoom() {
minZoom = Math.max($("#map-holder").width() / w, $("#map-holder").height() / h);
maxZoom = 20 * minZoom;
zoom
.scaleExtent([minZoom, maxZoom])
.translateExtent([[0, 0], [w, h]])
;
midX = ($("#map-holder").width() - minZoom * w) / 2;
midY = ($("#map-holder").height() - minZoom * h) / 2;//These are the original values
var swlat = 32;
var swlon = -82;
var nelat = 42;
var nelon = -72;
var projectCoordinates = projection([(swlat+nelat)/2, (swlon+nelon)/2]);
/*This did not work
var midX = minZoom*(w-(swlat+nelat)/2) - ($("#map-holder").width()-(swlat+nelat)/2);
var midY = minZoom*(h-(swlon+nelon)/2) - ($("#map-holder").height()-(swlon+nelon)/2);*/
/*Neither did this
var midX = minZoom*(w-projectCoordinates[0])-($("#map-holder").width()-projectCoordinates[0]);
var midY = minZoom*(h-projectCoordinates[1])-($("#map-holder").height()-projectCoordinates[1]);*/
svg.call(zoom.transform, d3.zoomIdentity.translate(midX, midY).scale(minZoom));
}
The idea behind the original approach was to:
1: Get the current pixel display of the map
2: Get the new pixel distance from the map corner to the map point after zoom has been applied
3: The pixel distance of the center of the container to the top of the container
4: subtract the values from 2 and 3
The original post was trying to translate the map so that it would initialize the zoom and pan over the center of the map. I tried to modify this approach first by directly substituting the lat/lon values into the above equations. I also tried first transforming the lat/lon values using the projection and then substituting those values in, with little success. What do I need to do in order to get my desired result?
Setting a translateExtent could be a bad idea because it depends on the zoom scale.
The following replacement works.
function initiateZoom() {
// Define a "minzoom" whereby the "Countries" is as small possible without leaving white space at top/bottom or sides
minZoom = Math.max($("#map-holder").width() / w, $("#map-holder").height() / h);
// set max zoom to a suitable factor of this value
maxZoom = 20 * minZoom;
// set extent of zoom to chosen values
// set translate extent so that panning can't cause map to move out of viewport
zoom
.scaleExtent([minZoom, maxZoom])
.translateExtent([[0, 0], [w, h]])
;
var swlat = 32;
var swlon = -82;
var nelat = 42;
var nelon = -72;
var nwXY = projection([swlon, nelat]);
var seXY = projection([nelon, swlat]);
var zoomScale = Math.min($("#map-holder").width()/(seXY[0]-nwXY[0]), $("#map-holder").height()/(seXY[1]-nwXY[1]))
var projectCoordinates = projection([(swlon+nelon)/2, (swlat+nelat)/2]);
svg.call(zoom.transform, d3.zoomIdentity.translate($("#map-holder").width()*0.5-zoomScale*projectCoordinates[0], $("#map-holder").height()*0.5-zoomScale*projectCoordinates[1]).scale(zoomScale));
}

Problems fitting a map in a container

In a previous question, a user informed me of a great function to center a map and adapt its size to the container.
"There is this nice gist from nrabinowitz, which provides a function which scales and translate a projection to fit a given box.
It goes through each of the geodata points (data parameter), projects it (projection parameter), and incrementally update the necessary scale and translation to fit all points in the container (box parameter) while maximizing the scale:
function fitProjection(projection, data, box, center) {
...
return projection.scale(scale).translate([transX, transY])
}
I love this function but for now I would not mind using something that solves my problem. This works for any map, but specifically for the one in Colombia it does not work for me.
I'm trying to center the map to the container so that it fits the center and the size is the right one to the container. but I can not get it to adapt. I have also tried with a .translate and it does not work for me. Is something wrong?
Here is my code:
function fitProjection(projection, data, box, center) {
// get the bounding box for the data - might be more efficient approaches
var left = Infinity,
bottom = -Infinity,
right = -Infinity,
top = Infinity;
// reset projection
projection
.scale(1)
.translate([0, 0]);
data.features.forEach(function(feature) {
d3.geo.bounds(feature).forEach(function(coords) {
coords = projection(coords);
var x = coords[0],
y = coords[1];
if (x < left) left = x;
if (x > right) right = x;
if (y > bottom) bottom = y;
if (y < top) top = y;
});
});
// project the bounding box, find aspect ratio
function width(bb) {
return (bb[1][0] - bb[0][0])
}
function height(bb) {
return (bb[1][1] - bb[0][1]);
}
function aspect(bb) {
return width(bb) / height(bb);
}
var startbox = [[left, top], [right, bottom]],
a1 = aspect(startbox),
a2 = aspect(box),
widthDetermined = a1 > a2,
scale = widthDetermined ?
// scale determined by width
width(box) / width(startbox) :
// scale determined by height
height(box) / height(startbox),
// set x translation
transX = box[0][0] - startbox[0][0] * scale,
// set y translation
transY = box[0][1] - startbox[0][1] * scale;
// center if requested
if (center) {
if (widthDetermined) {
transY = transY - (transY + startbox[1][1] * scale - box[1][1])/2;
} else {
transX = transX - (transX + startbox[1][0] * scale - box[1][0])/2;
}
}
return projection.scale(scale).translate([transX, transY])
}
var width = document.getElementById('statesvg').offsetWidth;
var height =document.getElementById('statesvg').offsetHeight;
/*// Define path generator
var path = d3.geo.path() // path generator that will convert GeoJSON to SVG paths
.projection(projection); // tell path generator to use albersUsa projection
*/
//remove svg
d3.select("#statesvg svg").remove();
var svg = d3.select("#statesvg")
.append("svg")
.attr("width", width+"px")
.attr("height", height+"px");
d3.json("https://rawgit.com/john-guerra/43c7656821069d00dcbc/raw/be6a6e239cd5b5b803c6e7c2ec405b793a9064dd/Colombia.geo.json", function(data) {
var features = data.features;
var projection=fitProjection(d3.geo.mercator(), data, [[0, 0], [width, height]], true)
var path = d3.geo.path()
.projection(projection);
svg.selectAll('path')
.data(features)
.enter().append('path')
.classed('map-layer', true)
.attr('d', path)
.attr('vector-effect', 'non-scaling-stroke')
});
http://plnkr.co/edit/JWL6L7NnhOpwkJeTfO6h?p=preview
You said that the function...
works for any map, but specifically for the one in Colombia it does not work for me.
This makes no sense: what makes you think that the function has personal issues with Colombia?
The problem is just those islands at the top left corner, the Archipelago of San Andrés, Providencia and Santa Catalina. Let's remove them:
data.features = data.features.filter(function(d){
return d.properties.DPTO !== "88"
});
Here is the result in my browser:
Here is your updated Plunker: http://plnkr.co/edit/1G0xY7CCCoJv070pdcx4?p=preview

dropdown menu for Zooming to bbox (d3.js)

I am trying to zoom in bbox depending on the option of drop down menu, I tryed the code D3.js - Zooming to bbox with a dropdown menu but it is not working,and here is a js fiiddle of my work
<div id="LayerCover"style="display: inline-block;">
</div> //this is the div where drop down menu must place
function mapZoomgDorow(file){
d3.queue()
.defer(d3.json, "Data/Updated_map.json")
.await(menuChanged);
}
function menuChanged(error, jordan) {
if (error) throw error;
var select = d3.select('#LayerCover')
.append('select')
select.selectAll("option")
.data(jordan.features)
.enter().append("option")
.filter(function(d) { return d.properties.Level == '1' })
.text(function(d) { return d.properties.Name_1; console.log(d.properties.Name_1); })
.on("click",clicked)
this give me the drop down menu but when I click nothing happened ,note that my function clicked is just like https://bl.ocks.org/mbostock/4699541
For the answer I've removed some of your code to make it more specific to the problem you are having (and thus hopefully easier to use).
When appending your features, you could append a select menu and its options:
// append a menu:
var select = d3.select('form')
.append('select')
.on('change',function() { zoom(this.value); });
var options = select.selectAll('option')
.data(jordan.features)
.enter()
.append('option')
.html(function(d) { return d.properties.name_2; })
.attr('value',function(d,i) { return i; });
I'm using an old version of your jordan.json (I think you've updated it, but your fiddle wanted me to create a profile for drop box so it was easier to use the old, and I don't have your csv). You'll want to make sure that this is working before implementing the zoom functionality. Also, you'll need to place an on change event for the select menu.
Also, it might be easiest if your click (on the map) to zoom functionality and your select an option zoom functionality used the same function - if we do this they'll both need to take the same value. The increment works fine for this (unless you are modifying the number of elements in the geojson). You'll need to apply the same filter to each though - the data for the paths and the options must be the same if using the increment.
Your zoom funcion appears to work great, I've modified it slightly with an if statement: If you click or select the same feature twice, the map zooms out:
var last = -1; // the last feature zoomed to
function zoom(i) {
// if clicking on the same feature that was zoomed to last zoom out:
if (i == last) {
var bounds = path.bounds(jordan),
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 = .8 / Math.max(dx / width, dy / height),
translate = [width / 2 - scale * x, height / 2 - scale * y];
last = -1;
}
// otherwise, zoom in:
else {
var bounds = path.bounds(jordan.features[i]),
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 = .8 / Math.max(dx / width, dy / height),
translate = [width / 2 - scale * x, height / 2 - scale * y];
last = i;
}
g.transition()
.duration(750)
.style("stroke-width", 1.5 / scale + "px")
.attr("transform", "translate(" + translate + ")scale(" + scale + ")");
}
I've put together a block here.

Zoom softly into the center of a D3 map

I have a create an interactive map. This map can be zoomed via buttons. These buttons already work but I don't like the current style. It does not feel UX-friendly when by clicking the button the map is shaking a bit. I would like to zoom into the center of the map softly instead.
Here zoom-in code on the button:
d3.select('#zoomplusbutton').on('click', function () {
ardamap.zoomByFactor(1.3);
});
And here the function code with the strange zooming:
zoomByFactor: function (factor) {
d3.event.preventDefault();
var scale = zoom.scale();
var extent = zoom.scaleExtent();
var newScale = scale * factor;
if (extent[0] <= newScale && newScale <= extent[1]) {
var t = zoom.translate();
var c = [width / 2, height / 2];
zoom
.scale(newScale)
.translate([c[0] + (t[0] - c[0]) / scale * newScale, c[1] + (t[1] - c[1]) / scale * newScale])
.event(g.transition());
}
}
I really don't know anymore where I got this zooming code from.
So how can I really zoom to the center of the map softly? Feel free to change the JS-code on the website.
After several hours now figured out a 'OK' solution. It's not perfectly the center but at the end without shaking and any other bad behaviour:
zoomButton: function (zoom_in) {
var scale = zoom.scale(),
extent = zoom.scaleExtent(),
translate = zoom.translate(),
x = translate[0], y = translate[1],
factor = zoom_in ? 1.3 : 1/1.3,
target_scale = scale * factor;
if (target_scale === extent[0] || target_scale === extent[1] || target_scale > extent[1] || target_scale < extent[0]) { return false; }
var clamped_target_scale = Math.max(extent[0], Math.min(extent[1], target_scale));
if (clamped_target_scale != target_scale){
target_scale = clamped_target_scale;
factor = target_scale / scale;
}
x = (x - center[0]) * factor + center[0];
y = (y - center[1]) * factor + center[1];
if (zoom_in){
x = x - 500;
}else{
x = x + 500;
}
d3.transition().duration(450).tween("zoom", function () {
var interpolate_scale = d3.interpolate(scale, target_scale),
interpolate_trans = d3.interpolate(translate, [x,y]);
return function (t) {
zoom.scale(interpolate_scale(t))
.translate(interpolate_trans(t));
ardamap.zoomedButton();
};
});
}
These calculations work pretty well. And here the two button raising events:
d3.select('#zoomplusbutton').on('click', function(){
ardamap.zoomButton(true);
});
d3.select('#zoomminusbutton').on('click', function () {
ardamap.zoomButton(false);
});
You can find the code update on arda-maps.org as well. If you have I fix for really going to the center of the current map, feel free to edit my answer. I'm still interested in improving this!

What is the original radius function for circle pack?

I'm interested in tweaking the radius of the circles on the circle pack layout. For that I need to know how the original radius is calculated.
By reading the d3.js source code for pack layout it seems the default radius function is simply Math.sqrt of value for each node. But that is not really the case because I modified the D3.js original circle pack example adding a .radius(function(d){return Math.sqrt(d);}) and as you can see at bl.ocks.org/ecerulm/f0a36710e3 the radius of the circles are not the same.
The d3.layout.pack() uses Math.sqrt as radius function. But pack.nodes will apply a scale transform d3_layout_packTransform(node, x, y, k) to make the whole circle pack chart to fit if radius wasn't explicitly set. That is why if you apply you own function (even if its radius(Math.sqrt)) you will need to apply your own scaling after if you want to get the same result as with implicit radius.
In the example below I explicitly set Math.sqrt as the radius function and then scale afterward to fit [diameter,diameter] with my own function pack_transform since d3_layout_packTranform is not accesible:
var pack = d3.layout.pack()
.value(function(d) { return d.size; })
.radius(Math.sqrt)
.size([diameter - 4, diameter - 4]);
var packnodes = pack.nodes(root);
var packroot = packnodes[0];
var w = diameter, h = diameter;
function pack_transform(node, k) {
function inner_transform(node,cx,cy,k) {
var children = node.children;
node.x = cx + k * (node.x-cx);
node.y = cy + k * ( node.y-cy);
node.r *= k;
if (children) {
var i = -1, n = children.length;
while (++i < n) inner_transform(children[i],cx,cy, k);
}
}
return inner_transform(node,node.x,node.y,k);
}
pack_transform(packroot, 1 / Math.max(2 * packroot.r / w, 2 * packroot.r / h));

Categories

Resources