title: D3.js idles between each mousewheel event
body: <p>I am having an issue with d3.js when I try to zoom in and out on a graph. The zoom is very slow and laggy. I am trying to debug by using the profiling tool (Opera/Chrome). I was expecting my zoom callback function to be the limiting factor but it turns out there is a lot of idle time between each mousewheel scroll events.<img src="https://i.stack.imgur.com/oMIgg.png" alt="D3 is idling for 1s between each scrolls"></p>
  
  <p>Motus operandum: I start the profiling, then give a big sharp scroll on the mousewheel (5sec on the graph). The graph lags for several seconds(from 5sec to 8.5sec on the graph) then calls my zoom callback periodically (from 8.5 to 14sec on the graph). I checked the stack calls and all my zooming callbacks are executed in order, synchronously, which makes me think the are done executing during the idle time. I think the profiler does not record some of the system/browser calls and qualifies those as idle, so I tried using interruptions ( event.preventDefault() etc...) to make sure nothing was executed on zoomend. It improved a little bit the performance, but there is still a lot of idle time:</p>
  
  <p><img src="https://i.stack.imgur.com/Geavp.png" alt="It starts off nicely but slows down"></p>
  
  <p>Can someone please help me figure out why there is so much idle time?</p>
  
  <p>Here is my relevant code:</p>
  
  <ul>
  <li><p>without interruption</p>
  
  <pre><code> d3Zoom = d3.behavior.zoom()
                  .x(element.self.xScale)
                  .y(element.self.yScale)
                  .scaleExtent([0.99, Infinity])
                  .on("zoom", semanticZoom)
                  .on("zoomend", updateSelection);
  </code></pre></li>
  <li><p>with interruption</p>
  
  <pre><code> var delayTimer=0;
   d3Zoom = d3.behavior.zoom()
                  .x(xScale)
                  .y(yScale)
                  .scaleExtent([0.99, Infinity])
                  .on("zoom", semanticZoom)
                  .on("zoomstart", function () {
                      //prevent recalculating heavyCalculations too often
                      window.clearTimeout(delayTimer);                            
                      var evt = e ? e : window.event;
                      return cancelDefaultAction(evt);
                  })
                  .on("zoomend", function () {
                      // only start heavy calculations if user hasn't zoomed for 0.75sec
                      delayTimer = window.setTimeout(updateSelection, 750);
                  });
  
    function cancelDefaultAction(e) {
                  var evt = e ? e : window.event;
                  if (evt.preventDefault) evt.preventDefault();
                  evt.returnValue = false;
                  return false;
              }`
  </code></pre></li>
  </ul>
  
  <p>EDIT: Here is an example of working code. Both semanticZoom and update selection are more complex in my project than in this example but they involve custom AngularJS directives, d3 brushes, warped geometry, aggregation etc... I have cropped semanticZoom to just perform an enter/exit/update pattern based on a quadtree (it might behave funny in this the example, but it's just to show the kind of operations I do). UpdateSelection updates the visible data to an angular directive to perform calculations (various statistics etc...). I did not populate it here but it is not actually very intensive.</p>
  
  <p><div class="snippet" data-lang="js" data-hide="false" data-console="false" data-babel="false">
  <div class="snippet-code">
  <pre class="snippet-code-js lang-js prettyprint-override"><code>var size = 100;
  
  var dataset = d3.range(10).map(function(d, idx) {
    return {
      x: d3.random.normal(size / 2, size / 4)(),
      y: d3.random.normal(size / 2, size / 4)(),
      uuid: idx
    };
  });
  
  //
  // Init Scales
  //
  
  var xScale = d3.scale.linear()
    .domain([0, size])
    .range([0, 100]);
  
  var yScale = d3.scale.linear()
    .domain([0, size])
    .range([0, 100]);
  
  //
  // Init Axes
  //
  
  var xAxis = d3.svg.axis()
    .scale(xScale)
    .ticks(10)
    .orient("bottom")
    .tickSize(-size);
  
  var yAxis = d3.svg.axis()
    .scale(yScale)
    .ticks(10)
    .orient("left")
    .tickSize(-size);
  
  //
  // Init Zoom
  //
  
  var d3Zoom = d3.behavior.zoom()
    .x(xScale)
    .y(yScale)
    .scaleExtent([0.99, Infinity])
    .on("zoom", semanticZoom)
    .on("zoomend", updateSelection);
  
  var quadtree = d3.geom.quadtree(dataset);
  
  
  //------------------------ Callbacks --------------------------------
  
  function semanticZoom() {
  
    var s = 1;
    var t = [0, 0];
    if (d3.event) {
      s = (d3.event.scale) ? d3.event.scale : 1;
      t = (d3.event.translate) ? d3.event.translate : [0, 0];
    }
  
    // set zoom boundaries
    // center of the zoom in svg coordinates
    var center = [(size / 2 - t[0]) / s, (size / 2 - t[1]) / s];
    // half size of the window in svg coordinates
    var halfsize = size / (2 * s);
    // top left corner in svg coordinates
    var tl = [center[0] - halfsize, center[1] - halfsize];
    // bottom right corner in svg coordinates
    var br = [center[0] + halfsize, center[1] + halfsize];
  
    /*
  
     //
     // Constrain zoom
     //
  
     if (!(tl[0] &gt; -10 &amp;&amp;
        tl[1] &gt; -10 &amp;&amp;
        br[0] &lt; size + 10 &amp;&amp;
        br[1] &lt; size + 10)) {
  
      // limit zoom-window corners
      tl = [Math.max(0, tl[0]), Math.max(0, tl[1])];
      br = [Math.min(size, br[0]), Math.min(size, br[1])];
      // get restrained center
      center = [(tl[0] + br[0]) / 2, (tl[1] + br[1]) / 2];
      // scale center
      t = [size / 2 - s * center[0], size / 2 - s * center[1]];
  
      // update svg
      svg.transition()
       .duration(1)
       .call( d3Zoom.translate(t).event );
  
     }
     */
  
    //
    // Store zoom extent
    //
  
    d3Zoom.extent = [tl, br];
    d3Zoom.scaleFactor = s;
    d3Zoom.translation = t;
  
    //
    // Update some heavy duty stuff 
    // (create a quadtree, search that quadtree and update an attribute for the elements found)
    //
  
    // Prune non visible data
    var displayedData = search(quadtree,
      d3Zoom.extent[0][0], d3Zoom.extent[0][1],
      d3Zoom.extent[1][0], d3Zoom.extent[1][1]);
  
    redrawSubset(displayedData);
  
    //
    // Update axes
    //
  
    d3.select(".x.axis").call(xAxis);
    d3.select(".y.axis").call(yAxis);
  
  }
  
  function redrawSubset(subset) {
  
    //Attach new data
  
    var elements = d3.select(".data_container")
      .selectAll(".datum")
      .data(subset, function(d) {
        return d.uuid;
      });
  
    //enter
  
    elements.enter()
      .append("circle")
      .attr("class", "datum")
      .attr("r", 1)
      .style("fill", "black");
  
    //exit
  
    elements.exit().remove();
  
    //update
  
    elements.attr("transform", ScaleData);
  
  }
  
  
  function updateSelection() {
    // some not so heavy duty stuff
  
  }
  
  function ScaleData(d) {
    return "translate(" + [xScale(d.x), yScale(d.y)] + ")";
  }
  
  //
  // search quadtree
  //
  
  function search(qt, x0, y0, x3, y3) {
    var pts = [];
    qt.visit(function(node, x1, y1, x2, y2) {
      var p = node.point;
  
      if ((p) &amp;&amp; (p.x &gt;= x0) &amp;&amp; (p.x &lt;= x3) &amp;&amp; (p.y &gt;= y0) &amp;&amp; (p.y &lt;= y3)) {
        pts.push(p);
      }
  
      return x1 &gt;= x3 || y1 &gt;= y3 || x2 &lt; x0 || y2 &lt; y0;
    });
  
    return pts;
  }
  
  
  
  //------------------------- DOM Manipulation -------------------------  
  
  var svg = d3.select("body").append("svg")
    .attr("width", size)
    .attr("height", size)
    .append("g")
    .attr("class", "data_container")
    .call(d3Zoom);
  
  
  
  svg.append("rect")
    .attr("class", "overlay")
    .attr("width", size)
    .attr("height", size)
    .style("fill", "none")
    .style("pointer-events", "all");
  
  
  
  var circle = svg.selectAll("circle")
    .data(dataset, function(d) {
      return d.uuid;
    }).enter()
    .append("circle")
    .attr("r", 1)
    .attr("class", "datum")
    .attr("transform", ScaleData);</code></pre>
  <pre class="snippet-code-html lang-html prettyprint-override"><code>&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"&gt;&lt;/script&gt;</code></pre>
  </div>
  </div>
  </p>
  
  <p>SemanticZoom and UpdateSelection have both been unit tested and run in times comparable to the profiler graphs above (50-100ms) for large datasets.</p>