Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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).

like image 499
Jared Avatar asked Apr 08 '18 20:04

Jared


People also ask

What format does D3 use to create graphics?

D3 uses SVG to create and modify the graphical elements of the visualization. Because SVG has a structured form, D3 can make stylistic and attribute changes to the shapes being drawn.

What Cannot you use D3 for?

D3 cannot easily conceal original data. If you're using data that you don't want shared, it can be challenging to use D3. D3 doesn't generate predetermined visualizations for you.


1 Answers

You can get the type of event using d3.event.sourceEvent. In the code you shared, the dragging anywhere in the white space will rotate and dragging on the bars will move.

With d3.event.sourceEvent you can check whether the ctrl key is pressed and move/rotate accordingly. You don't even need the drag function for your svg. It can be handled using the zoom functions alone.

Here's the fiddle:

var origin = [100, 85],
  scale = 5,
  j = 10,
  cubesData = [];
var alpha = 0,
  beta = 0,
  startAngle = Math.PI / 6;
var zoom = d3.zoom()
  .scaleExtent([1, 40])
  .on("zoom", zoomed)
  .on('start', zoomStart)
  .on('end', zoomEnd);
var svg = d3.select('svg').call(zoom)
  .append('g');

var color = d3.scaleOrdinal(d3.schemeCategory20);
var cubesGroup = svg.append('g').attr('class', 'cubes').attr('transform', 'translate(0,0) scale(1)');
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);

function zoomStart() {
  mx = d3.event.sourceEvent.x;
  my = d3.event.sourceEvent.y;
  if (d3.event.sourceEvent !== null && d3.event.sourceEvent.type == 'mousemove' && d3.event.sourceEvent.ctrlKey == true) {
    cubesGroup.attr("transform", d3.event.transform);
  }
}

function zoomEnd() {
  if (d3.event.sourceEvent == null) return;
  mouseX = d3.event.sourceEvent.x - mx + mouseX
  mouseY = d3.event.sourceEvent.y - my + mouseY
}

function zoomed(d) {
  if (d3.event.sourceEvent == null) return;

  if (d3.event.sourceEvent !== null && d3.event.sourceEvent.type == 'wheel') {
    cubesGroup.attr("transform", "scale(" + d3.event.transform['k'] + ")");
  } else if (d3.event.sourceEvent !== null && d3.event.sourceEvent.type == 'mousemove' && d3.event.sourceEvent.ctrlKey == true) {
    cubesGroup.attr("transform", "translate(" + d3.event.transform['x'] + "," + d3.event.transform['y'] + ") scale(" + d3.event.transform['k'] + ")");
  } else if (d3.event.sourceEvent !== null && d3.event.sourceEvent.type == 'mousemove' && d3.event.sourceEvent.ctrlKey == false) {
    mouseX = mouseX || 0;
    mouseY = mouseY || 0;
    beta = (d3.event.sourceEvent.x - mx + mouseX) * Math.PI / 230;
    alpha = (d3.event.sourceEvent.y - my + mouseY) * Math.PI / 230 * (-1);
    processData(cubes3D.rotateY(beta + startAngle)
      .rotateX(alpha - startAngle)(cubesData), 0);

  };
}

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() {
  console.log('dragStart')
  mx = d3.event.x;
  my = d3.event.y;
}

function dragged() {
  console.log('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() {
  console.log('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="500" height="500"></svg>
</body>

On JSFiddle

like image 145
Aditya Avatar answered Oct 17 '22 06:10

Aditya