Tuesday, April 17, 2018

Using d3-3d with pan & zoom while retaining rotation

Leave a Comment

I am using the d3-3d plugin to graph 3d bar charts, but I'd like to add the pan & zoom functionality while keeping the rotation. Just adding in d3.zoom() seems to conflict with the d3.drag() behavior - it appears to be random which one takes precedence and adds a lot of "jitter".

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 & FACES ---------*/      ce.selectAll('._3d').sort(d3._3d().sort);  }    function init(){      cubesData = [];      var cnt = 0;      for(var z = -j/2; z <= j/2; z = z + 5){          for(var x = -j; x <= 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();
button {      position: absolute;      right: 10px;      top: 10px;  }
<!DOCTYPE html>  <meta charset="utf-8">  <script src="https://d3js.org/d3.v4.min.js"></script>  <script src="https://unpkg.com/d3-3d/build/d3-3d.min.js"></script>  <body>  <svg width="200" height="175"></svg>  </body>

I'd like to mimic the behavior from vis.js.

(1) Ctrl+drag would translate the origin (two finger drag on mobile)

(2) drag would rotate (one finger drag on mobile)

(3) zoom would scale (two finger pinch on mobile)

How do I stop the propagation and only handle these events specifically?

Edit: It appears that the bar chart example has a scale() and origin() that can be set - but I would prefer to work with transforms for speed and efficiency of the update (as opposed to re-drawing).

0 Answers

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment