Search code examples
javascriptjqueryd3.jsgeo

D3js Geo unzoom element when zoom map


I have a D3js map built with topojson.js.

var projection = d3.geo.mercator();

Everything works fine, but I am looking for an effect that I cannot manage to achieve. When, zooming the map I would like the pins over it to scale down, But if I scale it down, I need to recalculate their coordinates and I can't find the formula to do so.

Zoom handler

scale = d3.event.scale;
if (scale >= 1) { 
   main.attr("transform", "translate(" + d3.event.translate + ")
   scale(" + d3.event.scale + ")");
}    
else {
   main.attr("transform", "translate(" + d3.event.translate + ")scale(1)");
}

//43x49 is the size initial pine size when scale = 1 
var pins = main.select("#pins").selectAll("g").select("image")
            .attr("width", function () {
                return 43 - (43 * (scale - 1));
            })
            .attr("height", function () {
                return 49 - (49 * (scale - 1));
            })
            .attr("x", function () {
                //Calculate new image coordinates;
            })
            .attr("y", function () {
                //Calculate new image coordinates;
            });

My question is : how do I calculate x and y based on new scale?

I hope I am clear enough.

Thanks for your help

EDIT :

Calculation of initial pins coordinates :

"translate(" + (projection([d.lon, d.lat])[0] - 20) + "," 
+ (projection([d.lon, d.lat])[1] - 45) + ")"

-20 and -45 to have the tip of the pin right on the target.


Solution

  • You need to "counter-scale" the pins, i.e. as main scales up/down you need to scale the pins down/up, in the opposite direction. The counter-scale factor is 1/scale, and you should apply it to each of the <g>s containing the pin image. This lets you remove your current width and height calculation from the <image> nodes (since the parent <g>'s scale will take care of it).

    However, for this to work properly, you'll also need to remove the x and y attributes from the <image> and apply position via the counter-scaled parent <g> as well. This is necessary because if there's a local offset (which is the case when x and y are set on the <image>) that local offset gets scaled as the parent <g> is scaled, which makes the pin move to an incorrect location.

    So:

    var pinContainers = main.select("#pins").selectAll("g")
        .attr("transform", function(d) {
            var x = ... // don't know how you calculate x (since you didn't show it)
            var y = ... // don't know how you calculate y
            var unscale = 1/scale;
            return "translate(" + x + " " + y + ") scale(" + unscale + ")";
        })
    pinContainers.select("image")
        .attr("width", 43)
        .attr("height", 49)
        // you can use non-zero x and y to get the image to scale
        // relative to some point other than the top-left of the
        // image (such as the tip of the pin)
        .attr("x", 0)
        .attr("y", 0)