title: Using d3-3d with pan & zoom while retaining rotation
body: <p>I am using the <a href="https://github.com/Niekes/d3-3d" rel="noreferrer">d3-3d plugin</a> to graph 3d bar charts, but I'd like to add the pan &amp; zoom functionality while keeping the rotation. Just adding in <code>d3.zoom()</code> seems to conflict with the <code>d3.drag()</code> behavior - it appears to be random which one takes precedence and adds a lot of "jitter".</p>
  
  <p><div class="snippet" data-lang="js" data-hide="false" data-console="true" data-babel="false">
  <div class="snippet-code">
  <pre class="snippet-code-js lang-js prettyprint-override"><code>var origin = [100, 85], scale = 5, j = 10, cubesData = [];
  var alpha = 0, beta = 0, startAngle = Math.PI/6;
  
  var svg = d3.select('svg')
    .call(d3.drag()
    .on('drag', dragged)
    .on('start', dragStart)
    .on('end', dragEnd))
    .append('g');
  
  var color  = d3.scaleOrdinal(d3.schemeCategory20);
  var cubesGroup = svg.append('g').attr('class', 'cubes');
  var mx, my, mouseX, mouseY;
  
  var cubes3D = d3._3d()
      .shape('CUBE')
      .x(function(d){ return d.x; })
      .y(function(d){ return d.y; })
      .z(function(d){ return d.z; })
      .rotateY( startAngle)
      .rotateX(-startAngle)
      .origin(origin)
      .scale(scale);
  
  var zoom = d3.zoom()
      .scaleExtent([1, 40])
      .on("zoom", zoomed);
  
  cubesGroup.call(zoom);
  
  function zoomed() {
    cubesGroup.attr("transform", d3.event.transform);
  
  }   
  
  function processData(data, tt){
  
      /* --------- CUBES ---------*/
  
      var cubes = cubesGroup.selectAll('g.cube')
         .data(data, function(d){ return d.id });
  
      var ce = cubes
        .enter()
        .append('g')
        .attr('class', 'cube')
        .attr('fill', function(d){ return color(d.id); })
        .attr('stroke', function(d){
           return d3.color(color(d.id)).darker(2);
        })
        .merge(cubes)
        .sort(cubes3D.sort);
  
          cubes.exit().remove();
  
          /* --------- FACES ---------*/
  
          var faces = cubes.merge(ce)
            .selectAll('path.face')
            .data(function(d){ return d.faces; },
              function(d){ return d.face; }
            );
  
          faces.enter()
              .append('path')
              .attr('class', 'face')
              .attr('fill-opacity', 0.95)
              .classed('_3d', true)
              .merge(faces)
              .transition().duration(tt)
              .attr('d', cubes3D.draw);
  
          faces.exit().remove();
  
          /* --------- TEXT ---------*/
  
          var texts = cubes.merge(ce)
            .selectAll('text.text').data(function(d){
          var _t = d.faces.filter(function(d){
              return d.face === 'top';
          });
  
          return [{height: d.height, centroid: _t[0].centroid}];
      });
  
      texts.enter()
        .append('text')
        .attr('class', 'text')
        .attr('dy', '-.7em')
        .attr('text-anchor', 'middle')
        .attr('font-family', 'sans-serif')
        .attr('font-weight', 'bolder')
        .attr('x', function(d){
          return origin[0] + scale * d.centroid.x
        })
        .attr('y', function(d){
          return origin[1] + scale * d.centroid.y
        })
        .classed('_3d', true)
        .merge(texts)
        .transition().duration(tt)
        .attr('fill', 'black')
        .attr('stroke', 'none')
        .attr('x', function(d){
          return origin[0] + scale * d.centroid.x
        })
        .attr('y', function(d){
          return origin[1] + scale * d.centroid.y
        })
        .tween('text', function(d){
          var that = d3.select(this);
          var i = d3.interpolateNumber(+that.text(), Math.abs(d.height));
          return function(t){
            that.text(i(t).toFixed(1));
          };
        });
  
      texts.exit().remove();
  
      /* --------- SORT TEXT &amp; FACES ---------*/
      ce.selectAll('._3d').sort(d3._3d().sort);
  }
  
  function init(){
      cubesData = [];
      var cnt = 0;
      for(var z = -j/2; z &lt;= j/2; z = z + 5){
          for(var x = -j; x &lt;= j; x = x + 5){
              var h = d3.randomUniform(-2, -7)();
              var _cube = makeCube(h, x, z);
              _cube.id = 'cube_' + cnt++;
              _cube.height = h;
              cubesData.push(_cube);
          }
      }
      processData(cubes3D(cubesData), 1000);
  }
  
  function dragStart(){
      mx = d3.event.x;
      my = d3.event.y;
  }
  
  function dragged(){
      mouseX = mouseX || 0;
      mouseY = mouseY || 0;
      beta   = (d3.event.x - mx + mouseX) * Math.PI / 230 ;
      alpha  = (d3.event.y - my + mouseY) * Math.PI / 230  * (-1);
      processData(cubes3D.rotateY(beta + startAngle)
        .rotateX(alpha - startAngle)(cubesData), 0);
  }
  
  function dragEnd(){
      mouseX = d3.event.x - mx + mouseX;
      mouseY = d3.event.y - my + mouseY;
  }
  
  function makeCube(h, x, z){
      return [
          {x: x - 1, y: h, z: z + 1}, // FRONT TOP LEFT
          {x: x - 1, y: 0, z: z + 1}, // FRONT BOTTOM LEFT
          {x: x + 1, y: 0, z: z + 1}, // FRONT BOTTOM RIGHT
          {x: x + 1, y: h, z: z + 1}, // FRONT TOP RIGHT
          {x: x - 1, y: h, z: z - 1}, // BACK  TOP LEFT
          {x: x - 1, y: 0, z: z - 1}, // BACK  BOTTOM LEFT
          {x: x + 1, y: 0, z: z - 1}, // BACK  BOTTOM RIGHT
          {x: x + 1, y: h, z: z - 1}, // BACK  TOP RIGHT
      ];
  }
  
  d3.selectAll('button').on('click', init);
  
  init();</code></pre>
  <pre class="snippet-code-css lang-css prettyprint-override"><code>button {
      position: absolute;
      right: 10px;
      top: 10px;
  }</code></pre>
  <pre class="snippet-code-html lang-html prettyprint-override"><code>&lt;!DOCTYPE html&gt;
  &lt;meta charset="utf-8"&gt;
  &lt;script src="https://d3js.org/d3.v4.min.js"&gt;&lt;/script&gt;
  &lt;script src="https://unpkg.com/d3-3d/build/d3-3d.min.js"&gt;&lt;/script&gt;
  &lt;body&gt;
  &lt;svg width="200" height="175"&gt;&lt;/svg&gt;
  &lt;/body&gt;</code></pre>
  </div>
  </div>
  </p>
  
  <p>I'd like to mimic the behavior from <a href="http://visjs.org/examples/graph3d/playground/index.html" rel="noreferrer">vis.js</a>.</p>
  
  <p>(1) <code>Ctrl+drag</code> would translate the origin (two finger drag on mobile)</p>
  
  <p>(2) <code>drag</code> would rotate (one finger drag on mobile)</p>
  
  <p>(3) <code>zoom</code> would scale (two finger pinch on mobile)</p>
  
  <p>How do I stop the propagation and only handle these events specifically?</p>
  
  <p>Edit: It appears that the <a href="https://bl.ocks.org/niekes/613d43d39372f99ae2dcea14f0f90617" rel="noreferrer">bar chart example</a> has a <code>scale()</code> and <code>origin()</code> that can be set - but I would prefer to work with <code>transforms</code> for speed and efficiency of the update (as opposed to re-drawing).</p>