score:1

Accepted answer

Contrary to Gerardo's answer I actually like your approach using the bounding box. You could easily just keep a reference to the <g> which holds the circles and get the bounding box from it. As the group's size and position is inherently determined by its contents, its bounding box is also the bounding box over all the circles within.

The implementation is pretty straightforward:

var circles = svg.append("g");             // Keep the reference to the enclosing g
var circle = circles.selectAll("circle")   // Append to that group

From there on you can get the overall extent of all your circles by doing:

var bbox = circles.node().getBBox();

Borrowing from Gerardo's implementation and inserting above code results in the following working demo.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
  .link {
    fill: none;
    stroke-width: 1.5px;
  }
  
  circle {
    stroke: black;
    stroke-width: 1.5px;
  }
  
  text {
    font: 10px sans-serif;
    pointer-events: none;
  }
  
  #resizebutton {
    position: absolute;
    top: 10px;
    left: 20px;
    height: 100px;
    width: 100px;
  }
  
  #graph {
    position: absolute;
    top: 30px;
    left: 100px;
  }
</style>

<body>
  <div id="root">
    <div id="resizebutton">
      <button onclick="resizeGraph()">Get size</button>
    </div>
    <div id="graph" height="400px" width="400px">
    </div>
  </div>
</body>

<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
  var nodes = [{
      name: "node1"
    },
    {
      name: "node2"
    },
    {
      name: "node3"
    },
    {
      name: "node4"
    },
    {
      name: "node5"
    },
    {
      name: "node6"
    },
    {
      name: "node7"
    },
    {
      name: "node8"
    },
    {
      name: "node9"
    }
  ];

  var links = [{
      source: 0,
      target: 8
    },
    {
      source: 1,
      target: 8
    },
    {
      source: 2,
      target: 8
    },
    {
      source: 3,
      target: 8
    },
    {
      source: 4,
      target: 8
    },
    {
      source: 5,
      target: 8
    },
    {
      source: 6,
      target: 8
    },
    {
      source: 7,
      target: 8
    }
  ];

  var width = 400,
    height = 400;

  var force = d3.layout.force()
    .nodes(nodes)
    .links(links)
    .size([width, height])
    .linkDistance(150)
    .charge(-300)
    .on("tick", tick)
    .start();

  var svg = d3.select("#graph").append("svg")
    .attr("width", width)
    .attr("height", height);

  var colors = d3.scale.category10();

  var path = svg.append("g").selectAll("path")
    .data(force.links())
    .enter().append("line")
    .attr('class', 'link')
    .attr('stroke', function(d, i) {
      return colors(i);
    })

  var circles = svg.append("g");
  var circle = circles.selectAll("circle")
    .data(force.nodes())
    .enter().append("circle")
    .attr("r", 8)
    .attr('class', 'circle')
    .attr('fill', function(d, i) {
      return colors(i);
    })
    .call(force.drag);

  var text = svg.append("g").selectAll("text")
    .data(force.nodes())
    .enter().append("text")
    .attr("x", 14)
    .attr("y", ".31em")
    .text(function(d) {
      return d.name;
    });

  function tick() {
    path.attr({
      x1: function(d) {
        return d.source.x;
      },
      y1: function(d) {
        return d.source.y;
      },
      x2: function(d) {
        return d.target.x;
      },
      y2: function(d) {
        return d.target.y;
      }
    });
    circle.attr("transform", transform);
    text.attr("transform", transform);
  }

  function transform(d) {
    return "translate(" + d.x + "," + d.y + ")";
  }

  function resizeGraph() {
    var bbox = circles.node().getBBox();
    var rect = svg.append("rect")
      .style("stroke", "red")
      .style("fill", "none")
      .attr(bbox);
  };
</script>

One benefit over Gerardo's approach is the fact that this solution will automatically take care of all the calculations, i.e. it will take into account the circles' radii. This comes in handy if the circles are of different sizes, when just knowing their center is not enough.

If you also want the texts to be part of the overall bounding box, just wrap a <g> around the circles and the texts and use this outer group instead.

score:3

Whatever your goals are, you don't need to use the actual DOM element (for instance, with getBBox()) to get the size of the force chart, you can use only its data.

In your code, this will give you the four corners of the rectangle that contains the chart:

var minX = d3.min(circle.data(), function(d){return d.x});
var minY = d3.min(circle.data(), function(d){return d.y});
var maxX = d3.max(circle.data(), function(d){return d.x});
var maxY = d3.max(circle.data(), function(d){return d.y});

Here is a demo based on your code, click Get Size to paint the container rectangle (to get the accurate rectangle, wait for the simulation to stop before clicking):

<!DOCTYPE html>
<meta charset="utf-8">
<style>
  .link {
    fill: none;
    stroke-width: 1.5px;
  }
  
  circle {
    stroke: black;
    stroke-width: 1.5px;
  }
  
  text {
    font: 10px sans-serif;
    pointer-events: none;
  }
  
  #resizebutton {
    position: absolute;
    top: 10px;
    left: 20px;
    height: 100px;
    width: 100px;
  }
  
  #graph {
    position: absolute;
    top: 30px;
    left: 100px;
  }
</style>

<body>
  <div id="root">
    <div id="resizebutton">
      <button onclick="resizeGraph()">Get size</button>
    </div>
    <div id="graph" height="400px" width="400px">
    </div>
  </div>
</body>

<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
  var nodes = [{
      name: "node1"
    },
    {
      name: "node2"
    },
    {
      name: "node3"
    },
    {
      name: "node4"
    },
    {
      name: "node5"
    },
    {
      name: "node6"
    },
    {
      name: "node7"
    },
    {
      name: "node8"
    },
    {
      name: "node9"
    }
  ];

  var links = [{
      source: 0,
      target: 8
    },
    {
      source: 1,
      target: 8
    },
    {
      source: 2,
      target: 8
    },
    {
      source: 3,
      target: 8
    },
    {
      source: 4,
      target: 8
    },
    {
      source: 5,
      target: 8
    },
    {
      source: 6,
      target: 8
    },
    {
      source: 7,
      target: 8
    }
  ];

  var width = 400,
    height = 400;

  var force = d3.layout.force()
    .nodes(nodes)
    .links(links)
    .size([width, height])
    .linkDistance(150)
    .charge(-300)
    .on("tick", tick)
    .start();

  var svg = d3.select("#graph").append("svg")
    .attr("width", width)
    .attr("height", height);

  var colors = d3.scale.category10();

  var path = svg.append("g").selectAll("path")
    .data(force.links())
    .enter().append("line")
    .attr('class', 'link')
    .attr('stroke', function(d, i) {
      return colors(i);
    })

  var circle = svg.append("g").selectAll("circle")
    .data(force.nodes())
    .enter().append("circle")
    .attr("r", 8)
    .attr('class', 'circle')
    .attr('fill', function(d, i) {
      return colors(i);
    })
    .call(force.drag);

  var text = svg.append("g").selectAll("text")
    .data(force.nodes())
    .enter().append("text")
    .attr("x", 14)
    .attr("y", ".31em")
    .text(function(d) {
      return d.name;
    });

  function tick() {
    path.attr({
      x1: function(d) {
        return d.source.x;
      },
      y1: function(d) {
        return d.source.y;
      },
      x2: function(d) {
        return d.target.x;
      },
      y2: function(d) {
        return d.target.y;
      }
    });
    circle.attr("transform", transform);
    text.attr("transform", transform);
  }

  function transform(d) {
    return "translate(" + d.x + "," + d.y + ")";
  }

  function resizeGraph() {
    var minX = d3.min(circle.data(), function(d) {
      return d.x
    });
    var minY = d3.min(circle.data(), function(d) {
      return d.y
    });
    var maxX = d3.max(circle.data(), function(d) {
      return d.x
    });
    var maxY = d3.max(circle.data(), function(d) {
      return d.y
    });
    svg.append("rect")
      .attr("x", minX - 8)
      .attr("y", minY - 8)
      .attr("width", maxX - minX + 16)
      .attr("height", maxY - minY + 16)
      .style("stroke", "red")
      .style("fill", "none");
  };
</script>


Related Query

More Query from same tag