Skip to content Skip to sidebar Skip to footer

D3js Outer Limits

I would like to use the zoom listener (scale) in combination with translate to fit all nodes, texts and paths nicely into the viewport/d3 container. I'm using the tree layout in c

Solution 1:

I tried a few ways when trying to solve this. I tried getBoundingClientRect() and getBBox() through D3 but neither gave the correct coordinates.

So what I did was to loop through each circle and go into it's data. I had some logic to get the lowest left value, the highest right value, lowest top value and highest bottom value.

To do this I just used this logic :

var thisNodeData = allNodes[i].__data__;

    var thisLeft = thisNodeData.x;
    var thisRight = thisNodeData.x;
    var thisTop = thisNodeData.y;
    var thisBottom = thisNodeData.y;

    if (i == 0) { //set it on first one
      left = thisLeft;
      right = thisRight;
      top = thisTop;
      bottom = thisBottom;
    };
    //overwrite values where neededif (left > thisLeft) {
      left = thisLeft
    }
    if (right < thisRight) {
      right = thisRight
    }
    if (top > thisTop) {
      top = thisTop
    }
    if (bottom < thisBottom) {
      bottom = thisBottom
    }

Now these left,right,bottom and top values will be the values of your rect. However, this way gets the centre points of each circle so to make up for this I made up a radius value but this can be found programatically :

So I used these to create a rectangle like so :

varcircleRadius=20;varrectAttr= [{
    x:top-circleRadius/2,
    y:left-circleRadius/2,
    width:bottom-top+circleRadius,
    height:right-left+circleRadius,
  }]

I must say, I messed about this these values. You would think that x would be left, y would be top but this didn't get the correct outcome. If anyone can tell me what I have done wrong here, will be appreciated. But for now this works fine, it just doesn't seem like the correct logic.

Now use rectAttr to create the boundary rectangle :

 svg.selectAll('rectangle')
    .data(rectAttr)
    .enter() //.append('svg')
    .append('rect')
    .attr('x', function(d) {
      return d.x; 
    })
    .attr('y', function(d) {
      return d.y; 
    })
    .attr('width', function(d) {
      return d.width; 
    })
    .attr('height', function(d) {
      return d.height; 
    })
    .style('stroke', 'red').style('fill', 'none')

I have added this function to be called on click of a node so I can show you it working.

Updated fiddle : http://jsfiddle.net/thatOneGuy/JnNwu/916/

EDIT:

Now to zoom according to size.

What you have to do here is get the difference in scale of the new rect compared to the old.

First I got the biggest value from the rectangles width and height to give the correct scale like so :

var testScale = Math.max(rectAttr[0].width,rectAttr[0].height)
var widthScale = width/testScale
var heightScale = height/testScale 
var scale = Math.max(widthScale,heightScale);

And then used this scale in the translate. To zoom into the rectangle only you have to get the center point and adjust it accordingly like so :

var transX = -(parseInt(d3.select('#invisRect').attr("x")) + parseInt(d3.select('#invisRect').attr("width"))/2) *scale + width/2;
var transY = -(parseInt(d3.select('#invisRect').attr("y")) + parseInt(d3.select('#invisRect').attr("height"))/2) *scale + height/2;

return'translate(' + transX + ','+ transY + ')scale('+scale+')' ;

I also added this line :

d3.select('#invisRect').remove();

Before creating a new one otherwise I would be getting the wrong rectangle when getting the translate dimensions above.

Final working fiddle : http://jsfiddle.net/thatOneGuy/JnNwu/919/

var json = {
  "name": "Base",
  "children": [{
    "name": "Type A",
    "children": [{
      "name": "Section 1",
      "children": [{
        "name": "Child 1"
      }, {
        "name": "Child 2"
      }, {
        "name": "Child 3"
      }]
    }, {
      "name": "Section 2",
      "children": [{
        "name": "Child 1"
      }, {
        "name": "Child 2"
      }, {
        "name": "Child 3"
      }]
    }]
  }, {
    "name": "Type B",
    "children": [{
      "name": "Section 1",
      "children": [{
        "name": "Child 1"
      }, {
        "name": "Child 2"
      }, {
        "name": "Child 3"
      }]
    }, {
      "name": "Section 2",
      "children": [{
        "name": "Child 1"
      }, {
        "name": "Child 2"
      }, {
        "name": "Child 3"
      }]
    }]
  }]
};

var width = 700;
var height = 650;
var maxLabel = 150;
var duration = 500;
var radius = 5;

var i = 0;
var root;

var tree = d3.layout.tree()
  .size([height, width]);

var diagonal = d3.svg.diagonal()
  .projection(function(d) {
    return [d.y, d.x];
  });

var svg = d3.select("body").append("svg")
  .attr("width", width)
  .attr("height", height)
  .append("g")
  .attr("transform", "translate(" + maxLabel + ",0)");

root = json;
root.x0 = height / 2;
root.y0 = 0;

root.children.forEach(collapse);

functionupdate(source) {
  // Compute the new tree layout.var nodes = tree.nodes(root).reverse();
  var links = tree.links(nodes);

  // Normalize for fixed-depth.
  nodes.forEach(function(d) {
    d.y = d.depth * maxLabel;
  });

  // Update the nodes…var node = svg.selectAll("g.node")
    .data(nodes, function(d) {
      return d.id || (d.id = ++i);
    });

  // Enter any new nodes at the parent's previous position.var nodeEnter = node.enter()
    .append("g")
    .attr("class", "node")
    .attr("transform", function(d) {
      return"translate(" + source.y0 + "," + source.x0 + ")";
    })
    .on("click", click);

  nodeEnter.append("circle").attr('class', 'circleNode')
    .attr("r", 0)
    .style("fill", function(d) {
      return d._children ? "lightsteelblue" : "white";
    });

  nodeEnter.append("text")
    .attr("x", function(d) {
      var spacing = computeRadius(d) + 5;
      return d.children || d._children ? -spacing : spacing;
    })
    .attr("dy", "3")
    .attr("text-anchor", function(d) {
      return d.children || d._children ? "end" : "start";
    })
    .text(function(d) {
      return d.name;
    })
    .style("fill-opacity", 0);

  // Transition nodes to their new position.var nodeUpdate = node.transition()
    .duration(duration)
    .attr("transform", function(d) {
      return"translate(" + d.y + "," + d.x + ")";
    });

  nodeUpdate.select("circle")
    .attr("r", function(d) {
      returncomputeRadius(d);
    })
    .style("fill", function(d) {
      return d._children ? "lightsteelblue" : "#fff";
    });

  nodeUpdate.select("text").style("fill-opacity", 1);

  // Transition exiting nodes to the parent's new position.var nodeExit = node.exit().transition()
    .duration(duration)
    .attr("transform", function(d) {
      return"translate(" + source.y + "," + source.x + ")";
    })
    .remove();

  nodeExit.select("circle").attr("r", 0);
  nodeExit.select("text").style("fill-opacity", 0);

  // Update the links…var link = svg.selectAll("path.link")
    .data(links, function(d) {
      return d.target.id;
    });

  // Enter any new links at the parent's previous position.
  link.enter().insert("path", "g")
    .attr("class", "link")
    .attr("d", function(d) {
      var o = {
        x: source.x0,
        y: source.y0
      };
      returndiagonal({
        source: o,
        target: o
      });
    });

  // Transition links to their new position.
  link.transition()
    .duration(duration)
    .attr("d", diagonal);

  // Transition exiting nodes to the parent's new position.
  link.exit().transition()
    .duration(duration)
    .attr("d", function(d) {
      var o = {
        x: source.x,
        y: source.y
      };
      returndiagonal({
        source: o,
        target: o
      });
    })
    .remove();

  // Stash the old positions for transition.
  nodes.forEach(function(d) {
    d.x0 = d.x;
    d.y0 = d.y;
  });
}

functioncomputeRadius(d) {
  if (d.children || d._children) return radius + (radius * nbEndNodes(d) / 10);
  elsereturn radius;
}

functionnbEndNodes(n) {
  nb = 0;
  if (n.children) {
    n.children.forEach(function(c) {
      nb += nbEndNodes(c);
    });
  } elseif (n._children) {
    n._children.forEach(function(c) {
      nb += nbEndNodes(c);
    });
  } else nb++;

  return nb;
}

functionclick(d) {

  if (d.children) {
    d._children = d.children;
    d.children = null;
  } else {
    d.children = d._children;
    d._children = null;
  }
  update(d);
  getBoundingBox();
}

functioncollapse(d) {
  if (d.children) {
    d._children = d.children;
    d._children.forEach(collapse);
    d.children = null;
  }
}

update(root);
getBoundingBox();

functiongetBoundingBox() {
  var left = 0,
    right = 0,
    top = 0,
    bottom = 0;

  var allNodes = document.getElementsByTagName('circle');


  for (var i = 0; i < allNodes.length; i++) {

    var thisNodeData = allNodes[i].__data__;

    var thisLeft = thisNodeData.x;
    var thisRight = thisNodeData.x;
    var thisTop = thisNodeData.y;
    var thisBottom = thisNodeData.y;

    if (i == 0) { //set it on first one
      left = thisLeft;
      right = thisRight;
      top = thisTop;
      bottom = thisBottom;
    };
    //overwrite values where neededif (left > thisLeft) {
      left = thisLeft
    }
    if (right < thisRight) {
      right = thisRight
    }
    if (top > thisTop) {
      top = thisTop
    }
    if (bottom < thisBottom) {
      bottom = thisBottom
    }

  }
  var circleRadius = 20;
  var rectAttr = [{
    x: top - circleRadius / 2,
    y: left - circleRadius / 2,
    width: bottom - top + circleRadius,
    height: right - left + circleRadius,
  }]
d3.select('#invisRect').remove();
  svg.selectAll('rectangle')
    .data(rectAttr)
    .enter() //.append('svg')
    .append('rect').attr('id','invisRect')
    .attr('x', function(d) {
      return d.x; 
    })
    .attr('y', function(d) {
      return d.y; 
    })
    .attr('width', function(d) {
      return d.width; 
    })
    .attr('height', function(d) {
      return d.height; 
    })
    .style('stroke', 'red').style('fill', 'none')
      
svg.attr('transform',function(d){
var testScale = Math.max(rectAttr[0].width,rectAttr[0].height)
var widthScale = width/testScale
var heightScale = height/testScale 
var scale = Math.max(widthScale,heightScale);
 
var transX = -(parseInt(d3.select('#invisRect').attr("x")) + parseInt(d3.select('#invisRect').attr("width"))/2) *scale + width/2;
var transY = -(parseInt(d3.select('#invisRect').attr("y")) + parseInt(d3.select('#invisRect').attr("height"))/2) *scale + height/2;
 
return'translate(' + transX + ','+ transY + ')scale('+scale+')' ;
})




}
html {
  font: 10px sans-serif;
}

svg {
  border: 1px solid silver;
}

.node {
  cursor: pointer;
}

.node circle {
  stroke: steelblue;
  stroke-width: 1.5px;
}

.link {
  fill: none;
  stroke: lightgray;
  stroke-width: 1.5px;
}
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script><divid=tree></div>

Post a Comment for "D3js Outer Limits"