1

I'm trying to apply fairly standard d3 drag/zoom functionality to a radial tree layout.

The problem is that if I define my zoomhandler as this...

svg.attr("transform","translate("+d3.event.translate+")scale("+d3.event.scale+")");

...then the zoom follows the mouse perfectly but the whole radial tree starts in the wrong place on first zoom (i.e. the (0,0) coordinate).

Whereas if I define my zoomhandler as this...

svg.attr("transform", "translate(" + (w/2 + d3.event.translate[0]) +
     "," + (h/2 + d3.event.translate[1]) + ")scale(" + d3.event.scale + ")" );

...then the tree behaves correctly but the zoom doesn't follow the mouse (in fact in order to zoom in/out on the tree without moving it my mouse would need to be positioned in the 0,0 coordinate at the top-left of the screen)

I appreciate that this is a topic that's been discussed before (I think most notably here: Using D3, can semantic zoom be applied to a radial tree?), but I'm still unclear how to get around this problem so would hugely appreciate any input from anyone who's specifically resolved the problem of getting a radial tree to both zooms towards a mouse position, and remain anchored to the center of the screen at the same time. Thanks!

Here's the complete code in detail...

var w = 1200;
var h = 1000;

var data = [{'parent_id' : '1', 'items_count' : '2'}
  , {'parent_id' : '2', 'items_count' : '4'}
  , {'parent_id' : '3', 'items_count' : '3'}
  , {'parent_id' : '4', 'items_count' : '2'}
  , {'parent_id' : '5', 'items_count' : '1'}
  , {'parent_id' : '6', 'items_count' : '6'}
  , {'parent_id' : '7', 'items_count' : '2'}
  , {'parent_id' : '8', 'items_count' : '4'}
  , {'parent_id' : '9', 'items_count' : '5'}
  , {'parent_id' : '10', 'items_count' : '7'}
];

var treeRadius = 300;
var searchCircleRadius = 60;

var circleRadiusScale = d3.scale.linear()
  .domain([0, d3.max(data, function(d) { return d.items_count; })])
  .range([10, 40]);

var dataTree = {
  children: data.map(function(d) { return { parent_id: d.parent_id, items_count: d.items_count}; })
};

var tree = d3.layout.tree()
  .size([360, treeRadius]);

var mainSvg = d3.select("body").append("svg")
  .attr("width", w)
  .attr("height", h);

var svg = mainSvg
  .append("g")
  .attr("transform", "translate(" + (w / 2) + "," + (h / 2) + ")");

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

var zoomListener = d3.behavior.zoom()
  .scaleExtent([0.1, 1.75])
  .on("zoom", zoomHandler);

function zoomHandler() {
  //1) for both of these, the tree starts in centre of screen, drag works nicely, but zoom doesn't follow mouse
  //childGroupZoom.attr("transform", "translate(" + (d3.event.translate[0]) + "," + (d3.event.translate[1]) + ") scale(" + d3.event.scale + ")"); 
  childGroupZoom.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");

  //2) follows the mouse on zoom but jump to top-left on first zoom/drag (because it's applied to "svg" which already has a translate applied)...
  //svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")"); //follows mouse on zoom but starts at wrong place

  //3) same as the first category - the tree doesn't jump, but the zoom doesn't follow the mouse
  //svg.attr("transform", "translate(" + (w/2 + d3.event.translate[0]) + "," + (h/2 + d3.event.translate[1]) + ")scale(" + d3.event.scale + ")" ); //starts in centre but doesn't follow mouse!
}

zoomListener(mainSvg);

var nodes = tree.nodes(dataTree);

var basicNode = childGroupZoom.selectAll(".node");

var node = basicNode
  .data(nodes)
  .enter().append("g")
  .attr("class", "node")
  .attr("transform", function(d) {
    return "rotate(" + (d.x - 90) + ") translate(" + d.y + ")";
  });

var outlineCircles = node.append("circle")
  .attr("r", function(d,i) { if (i<1) {
    return searchCircleRadius;
  } else {
    return circleRadiusScale(d.items_count);
  }})
  .attr("stroke", "#0099FF")
  .attr("stroke-width", "3")
  .attr("transform", function(d) {return "rotate(" + (-d.x + 90) + ")";});
<!DOCTYPE html>
<html>
<head>
    <title>Demo</title>
    <script src="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>
</body>
</html>
xxx
  • 1,153
  • 1
  • 11
  • 23
d3wannabe
  • 1,207
  • 2
  • 19
  • 39
  • I suppose you're doing something like this: translate-to-object-center, scale, translate-back-to-previous-position. – bvj Mar 28 '15 at 19:19
  • thanks for the response bvj - I've added the exact details above – d3wannabe Mar 28 '15 at 20:43
  • Sorry to chase but has anyone got any ideas on this? I'm completely stuck and can't find any other resources where the concept is explained. Thanks! – d3wannabe Mar 31 '15 at 22:11
  • Here's complete working code listing the essence of the problem in the zoom handler... – d3wannabe Apr 03 '15 at 11:20

1 Answers1

2
var dragListener = d3.behavior.drag()
    .on("drag", function() {
        dragX = d3.event.dx;
        dragY = d3.event.dy;
    });
    
mainSvg.call(dragListener);

var dragging = 0;   
var dragX = 0, dragY = 0;

dragListener.on("dragstart", function() {
  dragging = 1;
}); 

dragListener.on("dragend", function() {
  dragging = 0;
  dragX = 0;
  dragY = 0;
}); 

function zoomHandler() {
    var pos = d3.mouse(this);
    var scale = d3.event.scale;

    var trans = d3.transform(childGroupZoom.attr("transform"));
    var tpos = trans.translate;
    var tscale = trans.scale;
    var tx = tpos[0];
    var ty = tpos[1];
    var mx = pos[0] - w/2;
    var my = pos[1] - h/2;

    var dx =  (mx - tx - dragX)/tscale[0];
    var dy =  (my - ty - dragY)/tscale[1];
    var dx2 = (mx - dx)/scale - dx;
    var dy2 = (my - dy)/scale - dy;

    var tform = "translate(" + dx + "," + dy + ")scale(" + scale + ")translate(" + dx2 + "," + dy2 + ")"
    childGroupZoom.attr("transform", tform); 
}

var w = 1200;
var h = 1000;

var data = [{
  'parent_id': '1',
  'items_count': '2'
}, {
  'parent_id': '2',
  'items_count': '4'
}, {
  'parent_id': '3',
  'items_count': '3'
}, {
  'parent_id': '4',
  'items_count': '2'
}, {
  'parent_id': '5',
  'items_count': '1'
}, {
  'parent_id': '6',
  'items_count': '6'
}, {
  'parent_id': '7',
  'items_count': '2'
}, {
  'parent_id': '8',
  'items_count': '4'
}, {
  'parent_id': '9',
  'items_count': '5'
}, {
  'parent_id': '10',
  'items_count': '7'
}];

var treeRadius = 300;
var searchCircleRadius = 60;

var circleRadiusScale = d3.scale.linear()
  .domain([0, d3.max(data, function(d) {
    return d.items_count;
  })])
  .range([10, 40]);

var dataTree = {
  children: data.map(function(d) {
    return {
      parent_id: d.parent_id,
      items_count: d.items_count
    };
  })
};

var tree = d3.layout.tree()
  .size([360, treeRadius]);

var mainSvg = d3.select("body").append("svg")
  .attr("width", w)
  .attr("height", h);

var svg = mainSvg
  .append("g")
  .attr("transform", "translate(" + (w / 2) + "," + (h / 2) + ")");

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

var zoomListener = d3.behavior.zoom()
  .scaleExtent([0.1, 1.75])
  .on("zoom", zoomHandler);

zoomListener(mainSvg);

var nodes = tree.nodes(dataTree);

var basicNode = childGroupZoom.selectAll(".node");

var node = basicNode
  .data(nodes)
  .enter().append("g")
  .attr("class", "node")
  .attr("transform", function(d) {
    return "rotate(" + (d.x - 90) + ") translate(" + d.y + ")";
  });

var outlineCircles = node.append("circle")
  .attr("r", function(d, i) {
    if (i < 1) {
      return searchCircleRadius;
    } else {
      return circleRadiusScale(d.items_count);
    }
  })
  .attr("stroke", "#0099FF")
  .attr("stroke-width", "3")
  .attr("transform", function(d) {
    return "rotate(" + (-d.x + 90) + ")";
  });


var dragListener = d3.behavior.drag()
  .on("drag", function() {
    dragX = d3.event.dx;
    dragY = d3.event.dy;
  });

mainSvg.call(dragListener);

var dragging = 0;
var dragX = 0,
  dragY = 0;

dragListener.on("dragstart", function() {
  dragging = 1;
});

dragListener.on("dragend", function() {
  dragging = 0;
  dragX = 0;
  dragY = 0;
});

function zoomHandler() {
  var pos = d3.mouse(this);
  var scale = d3.event.scale;

  var trans = d3.transform(childGroupZoom.attr("transform"));
  var tpos = trans.translate;
  var tscale = trans.scale;
  var tx = tpos[0];
  var ty = tpos[1];
  var mx = pos[0] - w / 2;
  var my = pos[1] - h / 2;

  var dx = (mx - tx - dragX) / tscale[0];
  var dy = (my - ty - dragY) / tscale[1];
  var dx2 = (mx - dx) / scale - dx;
  var dy2 = (my - dy) / scale - dy;

  var tform = "translate(" + dx + "," + dy + ")scale(" + scale + ")translate(" + dx2 + "," + dy2 + ")"
  childGroupZoom.attr("transform", tform);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<!DOCTYPE html>
<html>

<head>
  <title>Demo</title>
  <script src="http://d3js.org/d3.v3.min.js"></script>
</head>

<body>
</body>

</html>

Note: The zoom handler is also being called by the framework on drag operations which is facilitating object movement by virtue of dragX and dragY.

bvj
  • 3,294
  • 31
  • 30
  • Hi bvj, thank you so much for the help on that! This is working great apart from 2 small issues I noticed: 1) the tree follows the mouse when you zoom out, but goes in the opposite direction (away from the mouse) when you zoom in. 2) the tree doesn't drag anymore. I'm going to try and solve those by working from your structure, but appreciate any insights on your side if you think it's easily solvable - thanks again!! – d3wannabe Apr 05 '15 at 09:32
  • Sorry, but I was off on point 1 in my comment above - the zoom in and out work perfectly. Still struggling on the drag though so appreciate any further input on my final remaining problem! Thanks. – d3wannabe Apr 05 '15 at 18:55
  • Cool. If you could accept/upvote my answer, that would be great (given the question concerned zoom.) Try to understand what's happening with the scaling/translation, then apply that knowledge to the move requirements. Follow up if you continue to have problems. – bvj Apr 05 '15 at 19:58
  • Hi bvj, sorry but I tried to upvote you but it says I don't have the necessary 15 reputation points! If I knew how to get them I'd gladly upvote the answer since as you say it's definitely corrected the zoom behaviour. – d3wannabe Apr 05 '15 at 20:39
  • p.s. what I'm struggling with on restoring the drag behaviour is that it seems like your "pos[0]" and "pos[1]" values should already be inputting into the translation values. I tried adding an independent drag listener but it was simply overridden by the code above. Apologies that this is painfully incremental but a lot of the concepts here are new to me and I'm struggling to even connect the dots on exactly what's happening behind the code you listed. – d3wannabe Apr 05 '15 at 20:48
  • I'm done with this. Please accept the answer if both zoom and drag work for you given the above revision. – bvj Apr 05 '15 at 22:46
  • 1
    thanks again bvj, the last edit now works perfectly - where a radial tree has working zoom in/out and drag! – d3wannabe Apr 06 '15 at 15:06
  • 1
    @xxx thanks for adding the snipit. For some it was necessary to add the d3 v3.x library option. – bvj May 30 '23 at 22:56