Search code examples
d3.jszoomingscaledata-visualization

Using .translateExtent in D3 returns 'NaN' x & y values when zooming


I want to prevent users from panning too far away from the maps I create. However, I'm struggling to apply the .translateExtent() function successfully.

The simplified example below (I replaced the map with a square) replicates the problem. The code works if I remove the .translateExtent() function.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <style>
      #svg {
        background-color: rgb(239, 239, 244);
      }
      #shape {
        fill: rgb(0, 75, 122);
        stroke: white;
        stroke-width: 3px;
      }
      #shape:hover {
        fill: rgb(150, 30, 27);
      }
    </style>
  </head>
  <body>
    <script>
      var width = 300,
          height = 300;

      var container = d3.select("body").append("div");

      var svg = container.append("svg")
             .attr("id", "svg")
             .attr("width", width)
             .attr("height", height);

      var group = svg.append("g");

      var shape = group.append("rect")
               .attr("id", "shape")
               .attr("width", 150)
               .attr("height", 150)
               .attr("x", 75)
               .attr("y", 75);

      zoom = d3.zoom()
          .scaleExtent([1, 3])
          .translateExtent([0, 0], [width, height])
          .on("zoom", zoomed);

      svg.call(zoom);

      function zoomed() {
        change = d3.event.transform;
        console.log(change)
        group.attr("transform", "translate(" + [change.x, change.y] + ")scale(" + change.k + ")")
        group.select("#shape").style("stroke-width", (3 / change.k) + "px");
      }
    </script>
  </body>
</html>

The error message in the console is:

Error: attribute transform: Expected number, "translate(NaN,NaN)scale(1)".

I can't understand why panning/zooming is returning 'NaN' values for x and y, but it has something to do with .translateExtent(). I feel there's an extra step I need to take that transforms the extent using the current scale value, but none of examples I saw online seemed to need it.


Solution

  • A D3.zoom translateExtent takes one array for input

    If extent is specified, sets the translate extent to the specified array of points [[x0, y0], [x1, y1]]

    you've provided two arrays:

    .translateExtent([0, 0], [width, height])
    

    Try nesting these in an array:

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <script src="https://d3js.org/d3.v4.min.js"></script>
        <style>
          #svg {
            background-color: rgb(239, 239, 244);
          }
          #shape {
            fill: rgb(0, 75, 122);
            stroke: white;
            stroke-width: 3px;
          }
          #shape:hover {
            fill: rgb(150, 30, 27);
          }
        </style>
      </head>
      <body>
      <body>
        <script>
          var width = 300,
              height = 300;
    
          var container = d3.select("body").append("div");
    
          var svg = container.append("svg")
                 .attr("id", "svg")
                 .attr("width", width)
                 .attr("height", height);
    
          var group = svg.append("g");
    
          var shape = group.append("rect")
                   .attr("id", "shape")
                   .attr("width", 150)
                   .attr("height", 150)
                   .attr("x", 75)
                   .attr("y", 75);
    
          zoom = d3.zoom()
              .scaleExtent([1, 3])
              .translateExtent([[0, 0], [width, height]])
              .on("zoom", zoomed);
    
          svg.call(zoom);
    
          function zoomed() {
            change = d3.event.transform;
            console.log(change)
            group.attr("transform", "translate(" + [change.x, change.y] + ")scale(" + change.k + ")")
            group.select("#shape").style("stroke-width", (3 / change.k) + "px");
          }
        </script>
      </body>
    </html>