Accepted answer

You can add the BBox onto the node data as a reference while adding the text elements to the document. If you add the nodes first, followed by the links, you can transfer information from the former to the later by adding a reference to the node elements onto the data elements. You can also add any other helpful positioning state that you might need on data array elements. After that you can access whatever you need in the force tick call-back via d.source and

Using font awesome fonts I noticed that the bounding box took at least one animation cycle to evolve to the correct shape so I had to add an animation timer to run a few (10) times after initial rendering to update the bounding box a few times and reposition the glyphs to be properly centered.

Made the bounding box adjustment permanent (not just run 10 times) to work around a bug in webkit whereby the glyph alignment breaks on zoom events. This however caused problems in moz so need to find another way to fix the zoom bug in webkit.

Referencing the svg element that the data is bound to, from that data element, creates a circular reference. So special care needs to be taken to break the reference chain. In the example below, the BBox reference is deleted after the required state has been copied onto the data elements.

Working example

//debug panel/////////////////////////////////////////////////////////////////////////////"#update").on("click", (function() {
	var dataSet = false;
	return function() {
		refresh(dataSets[(dataSet = !dataSet, +dataSet)])
var alpha ="#alpha").text("waiting..."),
		cog ="#wrapAlpha").insert("i", "#fdg").classed("fa fa-cog fa-spin", true),
		fdgInst ="#fdg");
elapsedTime = ElapsedTime("#panel", {margin: 0, padding: 0})
	.message(function (id) {
		return 'fps : ' + d3.format(" >8.3f")(1/this.aveLap())
elapsedTime.consoleOn = false;
var dataSets = [
		"nodes": [
			{"name": "node1", "content": "the first Node"},
			{"name": "node2", "content": "node2"},
			{"name": "node3", "content":{"fa": "fa/*-spin*/", text: "\uf013"}},
			{"name": "node4", "content":{"fa": "fa/*-spin*/", text: "\uf1ce"}}
		"edges": [
			{"source": 2, "target": 0},
			{"source": 2, "target": 1},
			{"source": 2, "target": 3}
		"nodes": [
			{"name": "node1", "content": "node1"},
			{"name": "node2", "content":{"fa": "fa/*-spin*/", text: "\uf1ce"}},
			{"name": "node3", "content":{"fa": "fa/*-spin*/", text: "\uf013"}},
			{"name": "node4", "content": "4"},
			{"name": "node5", "content": "5"},
			{"name": "node6", "content": "6"}
		"edges": [
			{"source": 2, "target": 0},
			{"source": 2, "target": 1},
			{"source": 2, "target": 3},
			{"source": 2, "target": 4},
			{"source": 2, "target": 5}
var refresh = (function(){
	var instID =,
			height = 160,
			width = 500,
			force = d3.layout.force()
				.size([width, height])
				.on("end", function(){cog.classed("fa-spin", false); elapsedTime.stop()})
				.on("start", function(){cog.classed("fa-spin", true); elapsedTime.start()});

	return function refresh(data) {
			.on("tick", (function(instID) {
				return function(e) {
					alpha.text(d3.format(" >8.4f")(e.alpha));
					fdgInst.text("fdg instance: " + instID);
					lines.attr("x1", function(d) {
						return d.source.x + + d.source.r;
					}).attr("y1", function(d) {
						return d.source.y +;
					}).attr("x2", function(d) {
						return +;
					}).attr("y2", function(d) {
						return +;
					node.attr("transform", function(d) {
						return "translate(" + [d.x, d.y] + ")"

		var svg ="body").selectAll("svg").data([data]);

			.attr({height: height, width: width});

		var lines = svg.selectAll(".links")
				linesEnter = lines.enter()
					.insert("line","#nodes") ? "#nodes" : null)
					.attr("class", "links")
					.attr({stroke: "steelblue", "stroke-width": 3});

		var nodes = svg.selectAll("#nodes").data(nodesData),
				nodesEnter = nodes.enter().append("g")
					.attr("id", "nodes"),
				node = nodes.selectAll(".node")
				newNode = node.enter().append("g")
			.attr({class: "content", fill: "steelblue"})
		newNode.insert("circle", ".node .content");

		var glyphs ="text")
					.each(function(d) {
						var node =;
						if(d.content.fa){'font-family': 'FontAwesome', 'font-size': '32px', 'dominant-baseline': 'central'})
								.classed(d.content.fa, true)
								.attr({"class": "content", style: null});
				backGround  ="circle").each(function(d) {
			}).style({"fill": "red", opacity: 0.8});

			//adjust the bounding box after the font loads
			var count = 0;
			d3.timer(function() {
				backGround.each(function(d) {
				return /*false || id != instID*/++count > 10;	//needs to keep running due to webkit zoom bug


		function nodesData(d) {
			return [d.nodes];

		function linksData(d) {
			return d.edges;
	function getBB(selection) {
		this.each(function(d) { = this.getBBox();

	function makeCircleBB(d, i, j) {
		var bb =;
		d.r = Math.max(bb.width, bb.height) / 2;
		delete; //plug potential memory leak! = bb.height / 2 + bb.y; = bb.width / 2;
		return {
			r: d.r, cx:, cy:, height: bb.height, width: bb.width
	function id(d) {
		return d;

svg {
      outline: 1px solid #282f51;
      pointer-events: all;
      overflow: visible;

    g.outline {
      outline: 1px solid red;

    #panel div {
      display: inline-block;
      margin: 0 .25em 3px 0;

    #panel div div {
      white-space: pre;
      margin: 0 .25em 3px 0;

    div#inputDiv {
      white-space: normal;
      display: inline-block;

    .node {
      cursor: default;

    .content {
      transform-origin: 50% 50%;
<link href="" rel="stylesheet"/>
<script src=""></script>
<script src=""></script>
<div id="panel">
  <div id="inputDiv">
    <input id="update" type="button" value="update">
  <div id="wrapAlpha">alpha:<div id="alpha"></div></div>
  <div id="fdg"></div>
<div id="viz"></div>

Related Query