D3js: use center viewport to increase focus point

We're trying to implement zoom buttons on top of the map created in D3, essentially since it works on google maps. The scaling event can be programmed with

d3ZoomBehavior.scale(myNewScale);
d3ZoomBehavior.event(myContainer);

      

and the map will zoom in using the current translation for presentation. When using the zoom buttons, the focus (center of zoom) is no longer the translation, but the center of the viewport. For scaling with the scroll wheel, we can use zoom.center

- but that doesn't seem to affect the dispatch of our own event.

I am confused on how to calculate the next translation given the new zoom factor and viewport center.

Given that I know the current scale, next scale, current translation, and map display port sizes, how can I calculate the next translation so that the center of the view port does not change?

+2


source to share


3 answers


I found this to be quite difficult to do in practice. The approach I have taken here is to simply create a mouse event that triggers the zoom when using the zoom buttons. This event is created in the center of the map.

Here's the relevant code:



.on("click", function() {
                var evt = document.createEvent("MouseEvents");
                evt.initMouseEvent(
                  'dblclick', // in DOMString typeArg,
                   true,  // in boolean canBubbleArg,
                   true,  // in boolean cancelableArg,
                   window,// in views::AbstractView viewArg,
                   120,   // in long detailArg,
                   width/2,     // in long screenXArg,
                   height/2,     // in long screenYArg,
                   width/2,     // in long clientXArg,
                   height/2,     // in long clientYArg,
                   0,     // in boolean ctrlKeyArg,
                   0,     // in boolean altKeyArg,
                   (by > 0 ? 0 : 1),     // in boolean shiftKeyArg,
                   0,     // in boolean metaKeyArg,
                   0,     // in unsigned short buttonArg,
                   null   // in EventTarget relatedTargetArg
                );
                this.dispatchEvent(evt);
            });

      

This is all a bit of a hack, but it works in practice and I found it much easier than calculating the correct center for each offset / scaling.

+4


source


I recently had to do the same and have a working example here http://bl.ocks.org/linssen/7352810 . Basically, it uses animation to smoothly scale to the desired target scale and also to translate through calculating the required difference after scaling to center.

I've included the gist of this below, but it's probably worth looking at a working example to get the full effect.

Html



<button id="zoom_in">+</button>
<button id="zoom_out">-</button>

      

Js

var zoom = d3.behavior.zoom().scaleExtent([1, 8]).on("zoom", zoomed);

function zoomed() {
    svg.attr("transform",
        "translate(" + zoom.translate() + ")" +
        "scale(" + zoom.scale() + ")"
    );
}

function interpolateZoom (translate, scale) {
    var self = this;
    return d3.transition().duration(350).tween("zoom", function () {
        var iTranslate = d3.interpolate(zoom.translate(), translate),
            iScale = d3.interpolate(zoom.scale(), scale);
        return function (t) {
            zoom
                .scale(iScale(t))
                .translate(iTranslate(t));
            zoomed();
        };
    });
}

function zoomClick() {
    var clicked = d3.event.target,
        direction = 1,
        factor = 0.2,
        target_zoom = 1,
        center = [width / 2, height / 2],
        extent = zoom.scaleExtent(),
        translate = zoom.translate(),
        translate0 = [],
        l = [],
        view = {x: translate[0], y: translate[1], k: zoom.scale()};

    d3.event.preventDefault();
    direction = (this.id === 'zoom_in') ? 1 : -1;
    target_zoom = zoom.scale() * (1 + factor * direction);

    if (target_zoom < extent[0] || target_zoom > extent[1]) { return false; }

    translate0 = [(center[0] - view.x) / view.k, (center[1] - view.y) / view.k];
    view.k = target_zoom;
    l = [translate0[0] * view.k + view.x, translate0[1] * view.k + view.y];

    view.x += center[0] - l[0];
    view.y += center[1] - l[1];

    interpolateZoom([view.x, view.y], view.k);
}

d3.selectAll('button').on('click', zoomClick);

      

+11


source


A more concise version of Wil's solution:

  var vis = d3.select('.vis');
  var zoom = d3.behavior.zoom()...
  var width = .., height = ..;

  function zoomByFactor(factor) {
    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(vis.transition().duration(350));
    }
  };

  function zoomIn() { zoomByFactor(1.2); }
  function zoomOut() { zoomByFactor(0.8); }

      

+8


source







All Articles