javascriptd3.jszoomingpanning

Allow zoom with buttons only, allow pan with mouse drag


[updated to now use drag- but drag isn't functioning 100% correct, so help still needed]

I'm trying to use d3 to zoom and pan an svg a particular way. I want the user to have to use the zoom in/zoom out buttons to zoom, but then be able to pan with mouse drag. I also want the panning to be restricted to a boundary. It is the same way the NY Times has it set up on their covid maps -- https://www.nytimes.com/interactive/2021/us/covid-cases.html (scroll to the Hot Spots map)

I have the zooming within a boundary working, and I've disabled free zooming by commenting out .call(zoom). I am just zooming with the buttons and using .call(zoom.transform, initialTransform). I'm calling drag to control the panning, but it jumps a bit and isn't the smooth interaction I'd like. It also seems like it might be effecting zoom.

Any pointers or help very much appreciated.

var width = 960,
      height = 500;

    var svg = d3.select("body").append("svg")
      .attr("width", width)
      .attr("height", height)
      .style("background-color", randomColor),
      g = svg.append("g");

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

    let zoomLevel,
      tx,
      ty

    var drag = d3.drag()
      .on("drag", dragged);

    var initialTransform = d3.zoomIdentity
      .translate(0, 0)
      .scale(1)

    d3.select('#zoom-in').on('click', function () {
      zoom.scaleBy(svg.transition().duration(50), 1.3);
    });

    d3.select('#zoom-out').on('click', function () {
      zoom.scaleBy(svg.transition().duration(50), 1 / 1.3);
    });

    function dragged(event, d) {

      var e = event

      var dx = e.subject.x,
        dy = e.subject.y,
        x = e.x,
        y = e.y,
        translate = [width / 2 - zoomLevel * x, height / 2 - zoomLevel * y];

      var transform = d3.zoomIdentity
        .translate(translate[0], translate[1])
        .scale(zoomLevel);

      svg.transition()
        .duration(50)
        .call(zoom.transform, transform);

    }



    function zoomed(event) {

      zoomLevel = event.transform.k

      var e = event

      tx = Math.min(0, Math.max(e.transform.x, width - width * e.transform.k)),
        ty = Math.min(0, Math.max(e.transform.y, height - height * e.transform.k));

      g.attr("transform", [
        "translate(" + [tx, ty] + ")",
        "scale(" + e.transform.k + ")"
      ].join(" "));

    }

    svg
      //.call(zoom) // uncomment to disable free zooming. but then can't pan ?
      .call(zoom.transform, initialTransform)
      .call(drag) // use this to pan

    // random circles to fill svg
    var circle = g.selectAll("circle")
      .data(d3.range(300).map(function (i) {
        return {
          x: Math.random() * width,
          y: Math.random() * height,
          r: .01 + Math.random() * 50,
          color: randomColor()
        };
      }).sort(function (a, b) {
        return d3.descending(a.r, b.r);
      }))
      .enter()
      .append("circle")
      .attr("fill", function (d) { return d.color; })
      .attr("cx", function (d) { return d.x; })
      .attr("cy", function (d) { return d.y; })
      .attr("r", function (d) { return d.r; });

    function randomColor() {
      return "hsl(" + ~~(60 + Math.random() * 180) + ",80%,60%)";
    }
<script src="http://d3js.org/d3.v6.min.js"></script>
<div id="zoom-buttons">
  <button id="zoom-in">+</button>
  <button id="zoom-out">-</button>
</div>


Solution

  • Just disable the wheel zooming (and drop the drag):

    svg.call(zoom)
        .on("wheel.zoom", null);
    

    Here is your code with that change:

    var width = 960,
      height = 500;
    
    var svg = d3.select("body").append("svg")
      .attr("width", width)
      .attr("height", height)
      .style("background-color", randomColor),
      g = svg.append("g");
    
    var zoom = d3.zoom()
      .scaleExtent([1, 10])
      .on("zoom", zoomed);
    
    d3.select('#zoom-in').on('click', function() {
      zoom.scaleBy(svg.transition().duration(50), 1.3);
    });
    
    d3.select('#zoom-out').on('click', function() {
      zoom.scaleBy(svg.transition().duration(50), 1 / 1.3);
    });
    
    function zoomed(event) {
    
      zoomLevel = event.transform.k
    
      var e = event
    
      tx = Math.min(0, Math.max(e.transform.x, width - width * e.transform.k)),
        ty = Math.min(0, Math.max(e.transform.y, height - height * e.transform.k));
    
      g.attr("transform", [
        "translate(" + [tx, ty] + ")",
        "scale(" + e.transform.k + ")"
      ].join(" "));
    
    }
    
    svg.call(zoom)
      .on("wheel.zoom", null);
    
    // random circles to fill svg
    var circle = g.selectAll("circle")
      .data(d3.range(300).map(function(i) {
        return {
          x: Math.random() * width,
          y: Math.random() * height,
          r: .01 + Math.random() * 50,
          color: randomColor()
        };
      }).sort(function(a, b) {
        return d3.descending(a.r, b.r);
      }))
      .enter()
      .append("circle")
      .attr("fill", function(d) {
        return d.color;
      })
      .attr("cx", function(d) {
        return d.x;
      })
      .attr("cy", function(d) {
        return d.y;
      })
      .attr("r", function(d) {
        return d.r;
      });
    
    function randomColor() {
      return "hsl(" + ~~(60 + Math.random() * 180) + ",80%,60%)";
    }
    <script src="http://d3js.org/d3.v6.min.js"></script>
    <div id="zoom-buttons">
      <button id="zoom-in">+</button>
      <button id="zoom-out">-</button>
    </div>