d3wannabe
d3wannabe

Reputation: 1317

d3 zoom not centering on mouse position with radial tree

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>

Upvotes: 1

Views: 2948

Answers (1)

bvj
bvj

Reputation: 3392

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.

Upvotes: 2

Related Questions