6

So i've been working on a collapsible force-directed graph based of the following example.

Im trying to advance from that and add titles to each node. I've followed a similar answer here on stackoverflow, but I cannot implement the solution from that answer into the example above and other similar solutions.

Please can someone point me in the correct direction.

See project code below before implementation.

enter image description here

JS

var w = 600,
    h = 600,
    radius = 10,
    node,
    link,
    root;

var force = d3.layout.force()
    .on("tick", tick)
    .charge(function(d) { return -500; })
    .linkDistance(function(d) { return d.target._children ? 100 : 50; })
    .size([w, h - 160]);

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

root = words[0]; //set root node
root.fixed = true;
root.x = w / 2;
root.y = h / 2 - 80;
update();

function update() {
    var nodes = flatten(root),
    links = d3.layout.tree().links(nodes);

    // Restart the force layout.
    force
        .nodes(nodes)
        .links(links)
        .start();

    // Update the links…
    link = svg.selectAll(".link")
        .data(links);

    // Enter any new links.
    link.enter().insert("svg:line", ".node")
        .attr("class", "link")
        .attr("x1", function(d) { return d.source.x; })
        .attr("y1", function(d) { return d.source.y; })
        .attr("x2", function(d) { return d.target.x; })
        .attr("y2", function(d) { return d.target.y; });

    // Exit any old links.
    link.exit().remove();

    // Update the nodes…
    node = svg.selectAll("circle.node")
        .data(nodes)
        .style("fill", color);

    node.transition()
        .attr("r", radius);

    node.append("title")
        .text(function(d) { return d.name; });

    // Enter any new nodes.
    node.enter().append("svg:circle")
        .attr("class", "node")
        .attr("cx", function(d) { return d.x; })
        .attr("cy", function(d) { return d.y; })
        .attr("r", radius)
        .style("fill", color)
        .on("click", click)
        .call(force.drag);

    // Exit any old nodes.
    node.exit().remove();
}

function tick() {
    link.attr("x1", function(d) { return d.source.x; })
        .attr("y1", function(d) { return d.source.y; })
        .attr("x2", function(d) { return d.target.x; })
        .attr("y2", function(d) { return d.target.y; });

    node.attr("cx", function(d) { return d.x; })
          .attr("cy", function(d) { return d.y; });
}

// Color leaf nodes orange, and packages white or blue.
function color(d) {
    if(d._children){
        return "#95a5a6";
    }else{
        switch(d.group) {
            case 'r': //adverb
                return "#e74c3c";
                break;
            case 'n': //noun
                return "#3498db";
                break;
            case 'v': //verb
                return "#2ecc71";
                break;
            case 's': //adjective
                return "#e78229";
                break;
            default:
                return "#9b59b6";
        }
    }
}

// Toggle children on click.
function click(d) {
    if (d.children) {
        d._children = d.children;
        d.children = null;
    } else {
        d.children = d._children;
        d._children = null;
    }
    update();
}

// Returns a list of all nodes under the root.
function flatten(root) {
    var nodes = [], i = 0;

    function recurse(node) {
        if (node.children) node.size = node.children.reduce(function(p, v) { return p + recurse(v); }, 0);
        if (!node.id) node.id = ++i;
        nodes.push(node);
        return node.size;
    }

    root.size = recurse(root);
    return nodes;
}

CSS

circle.node {
    cursor: pointer;
    stroke: #34495e;
    stroke-width: 2px;
    box-sizing: border-box;
    stroke-location: inside;
}

line.link {
    fill: none;
    stroke: #34495e;
    stroke-width: 1.5px;
}

HTML

<!DOCTYPE html>
<body>
<script src="http://d3js.org/d3.v2.min.js?2.9.6"></script>
    <script>

        var words = [
    {
        "group":"n",
        "word":"main node",
        "children":[
            {
                "group":"n",
                "name":"sub node 1"
            },
            {
                "group":"n",
                "name":"sub node 2"
            },
            {
                "group":"n",
                "name":"sub node 3"
            },
            {
                "group":"v",
                "name":"sub node 4"
            },
            {
                "group":"s",
                "name":"sub node 5"
            },
            {
                "group":"s",
                "name":"sub node 6"
            },
            {
                "group":"s",
                "name":"sub node 7"
            },
            {
                "group":"s",
                "name":"sub node 8"
            },
            {
                "group":"s",
                "name":"sub node 9"
            },
            {
                "group":"s",
                "name":"sub node 10"
            },
            {
                "group":"s",
                "name":"sub node 11"
            },
            {
                "group":"r",
                "name":"sub node 12",
                "children":[
                    {
                        "group":"r",
                        "name":"sub sub node 1"
                    },
                    {
                        "group":"r",
                        "name":"sub sub node 2"
                    },
                    {
                        "group":"r",
                        "name":"sub sub node 3"
                    }
                ]
            }
        ]
    }
]
    </script>
</body>

JSFiddle

Community
  • 1
  • 1
Oliver Evans
  • 960
  • 1
  • 20
  • 43

2 Answers2

2

Interestingly, it is kind of working as a snippet here with your code as is, but only after you've collapsed or expanded a node. This should give you some idea of where your problem is. Basically, you're adding the title element to each node before the nodes actually exist. Move the

node.append("title")
    .text(function(d) { return d.name; });

to after the

node.enter().append("svg:circle")
    .attr("class", "node")
    .attr("cx", function(d) { return d.x; })
    .attr("cy", function(d) { return d.y; })
    .attr("r", radius)
    .style("fill", color)
    .on("click", click)
    .call(force.drag);

call.

Here is a snippet with that change completed:

var words = [{
  "group": "n",
  "word": "main node",
  "children": [{
    "group": "n",
    "name": "sub node 1"
  }, {
    "group": "n",
    "name": "sub node 2"
  }, {
    "group": "n",
    "name": "sub node 3"
  }, {
    "group": "v",
    "name": "sub node 4"
  }, {
    "group": "s",
    "name": "sub node 5"
  }, {
    "group": "s",
    "name": "sub node 6"
  }, {
    "group": "s",
    "name": "sub node 7"
  }, {
    "group": "s",
    "name": "sub node 8"
  }, {
    "group": "s",
    "name": "sub node 9"
  }, {
    "group": "s",
    "name": "sub node 10"
  }, {
    "group": "s",
    "name": "sub node 11"
  }, {
    "group": "r",
    "name": "sub node 12",
    "children": [{
      "group": "r",
      "name": "sub sub node 1"
    }, {
      "group": "r",
      "name": "sub sub node 2"
    }, {
      "group": "r",
      "name": "sub sub node 3"
    }]
  }]
}]


var w = 600,
  h = 600,
  radius = 10,
  node,
  link,
  root;

var force = d3.layout.force()
  .on("tick", tick)
  .charge(function(d) {
    return -500;
  })
  .linkDistance(function(d) {
    return d.target._children ? 100 : 50;
  })
  .size([w, h - 160]);

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

root = words[0]; //set root node
root.fixed = true;
root.x = w / 2;
root.y = h / 2 - 80;
update();

function update() {
  var nodes = flatten(root),
    links = d3.layout.tree().links(nodes);

  // Restart the force layout.
  force
    .nodes(nodes)
    .links(links)
    .start();

  // Update the links…
  link = svg.selectAll(".link")
    .data(links);

  // Enter any new links.
  link.enter().insert("svg:line", ".node")
    .attr("class", "link")
    .attr("x1", function(d) {
      return d.source.x;
    })
    .attr("y1", function(d) {
      return d.source.y;
    })
    .attr("x2", function(d) {
      return d.target.x;
    })
    .attr("y2", function(d) {
      return d.target.y;
    });

  // Exit any old links.
  link.exit().remove();

  // Update the nodes…
  node = svg.selectAll("circle.node")
    .data(nodes)
    .style("fill", color);

  node.transition()
    .attr("r", radius);


  // Enter any new nodes.
  node.enter().append("svg:circle")
    .attr("class", "node")
    .attr("cx", function(d) {
      return d.x;
    })
    .attr("cy", function(d) {
      return d.y;
    })
    .attr("r", radius)
    .style("fill", color)
    .on("click", click)
    .call(force.drag);

  node.append("title")
    .text(function(d) {
      return d.name;
    });


  // Exit any old nodes.
  node.exit().remove();
}

function tick() {
  link.attr("x1", function(d) {
      return d.source.x;
    })
    .attr("y1", function(d) {
      return d.source.y;
    })
    .attr("x2", function(d) {
      return d.target.x;
    })
    .attr("y2", function(d) {
      return d.target.y;
    });

  node.attr("cx", function(d) {
      return d.x;
    })
    .attr("cy", function(d) {
      return d.y;
    });
}

// Color leaf nodes orange, and packages white or blue.
function color(d) {
  if (d._children) {
    return "#95a5a6";
  } else {
    switch (d.group) {
      case 'r': //adverb
        return "#e74c3c";
        break;
      case 'n': //noun
        return "#3498db";
        break;
      case 'v': //verb
        return "#2ecc71";
        break;
      case 's': //adjective
        return "#e78229";
        break;
      default:
        return "#9b59b6";
    }
  }
}

// Toggle children on click.
function click(d) {
  if (d.children) {
    d._children = d.children;
    d.children = null;
  } else {
    d.children = d._children;
    d._children = null;
  }
  update();
}

// Returns a list of all nodes under the root.
function flatten(root) {
  var nodes = [],
    i = 0;

  function recurse(node) {
    if (node.children) node.size = node.children.reduce(function(p, v) {
      return p + recurse(v);
    }, 0);
    if (!node.id) node.id = ++i;
    nodes.push(node);
    return node.size;
  }

  root.size = recurse(root);
  return nodes;
}
circle.node {
  cursor: pointer;
  stroke: #34495e;
  stroke-width: 2px;
  box-sizing: border-box;
  stroke-location: inside;
}
line.link {
  fill: none;
  stroke: #34495e;
  stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="viz"></div>

Note: This correction has an issue when expanding or collapsing a node, with the title being incorrectly set. This is because of the way d3 is binding the data to the DOM elements. By default d3 will bind based on index. Because adding and remove data can potentially change the order, the index values for the data could change, meaning that the relationship between DOM elements and data is broken. This can be corrected by explicitly setting the key for the binding of the data. You could use anything unique for the key, but for this example, I've just used the name property you've defined. Like so:

var words = [{
  "group": "n",
  "word": "main node",
  "children": [{
    "group": "n",
    "name": "sub node 1"
  }, {
    "group": "n",
    "name": "sub node 2"
  }, {
    "group": "n",
    "name": "sub node 3"
  }, {
    "group": "v",
    "name": "sub node 4"
  }, {
    "group": "s",
    "name": "sub node 5"
  }, {
    "group": "s",
    "name": "sub node 6"
  }, {
    "group": "s",
    "name": "sub node 7"
  }, {
    "group": "s",
    "name": "sub node 8"
  }, {
    "group": "s",
    "name": "sub node 9"
  }, {
    "group": "s",
    "name": "sub node 10"
  }, {
    "group": "s",
    "name": "sub node 11"
  }, {
    "group": "r",
    "name": "sub node 12",
    "children": [{
      "group": "r",
      "name": "sub sub node 1"
    }, {
      "group": "r",
      "name": "sub sub node 2"
    }, {
      "group": "r",
      "name": "sub sub node 3"
    }]
  }]
}]


var w = 600,
  h = 600,
  radius = 10,
  node,
  link,
  root;

var force = d3.layout.force()
  .on("tick", tick)
  .charge(function(d) {
    return -500;
  })
  .linkDistance(function(d) {
    return d.target._children ? 100 : 50;
  })
  .size([w, h - 160]);

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

root = words[0]; //set root node
root.fixed = true;
root.x = w / 2;
root.y = h / 2 - 80;
update();

function update() {
  var nodes = flatten(root),
    links = d3.layout.tree().links(nodes);

  // Restart the force layout.
  force
    .nodes(nodes)
    .links(links)
    .start();

  // Update the links…
  link = svg.selectAll(".link")
    .data(links);

  // Enter any new links.
  link.enter().insert("svg:line", ".node")
    .attr("class", "link")
    .attr("x1", function(d) {
      return d.source.x;
    })
    .attr("y1", function(d) {
      return d.source.y;
    })
    .attr("x2", function(d) {
      return d.target.x;
    })
    .attr("y2", function(d) {
      return d.target.y;
    });

  // Exit any old links.
  link.exit().remove();

  // Update the nodes…
  node = svg.selectAll("circle.node")
    .data(nodes, function(d) {
      return d.name;
    })
    .style("fill", color);

  node.transition()
    .attr("r", radius);


  // Enter any new nodes.
  node.enter().append("svg:circle")
    .attr("class", "node")
    .attr("cx", function(d) {
      return d.x;
    })
    .attr("cy", function(d) {
      return d.y;
    })
    .attr("r", radius)
    .style("fill", color)
    .on("click", click)
    .call(force.drag);

  node.append("title")
    .text(function(d) {
      return d.name;
    });


  // Exit any old nodes.
  node.exit().remove();
}

function tick() {
  link.attr("x1", function(d) {
      return d.source.x;
    })
    .attr("y1", function(d) {
      return d.source.y;
    })
    .attr("x2", function(d) {
      return d.target.x;
    })
    .attr("y2", function(d) {
      return d.target.y;
    });

  node.attr("cx", function(d) {
      return d.x;
    })
    .attr("cy", function(d) {
      return d.y;
    });
}

// Color leaf nodes orange, and packages white or blue.
function color(d) {
  if (d._children) {
    return "#95a5a6";
  } else {
    switch (d.group) {
      case 'r': //adverb
        return "#e74c3c";
        break;
      case 'n': //noun
        return "#3498db";
        break;
      case 'v': //verb
        return "#2ecc71";
        break;
      case 's': //adjective
        return "#e78229";
        break;
      default:
        return "#9b59b6";
    }
  }
}

// Toggle children on click.
function click(d) {
  if (d.children) {
    d._children = d.children;
    d.children = null;
  } else {
    d.children = d._children;
    d._children = null;
  }
  update();
}

// Returns a list of all nodes under the root.
function flatten(root) {
  var nodes = [],
    i = 0;

  function recurse(node) {
    if (node.children) node.size = node.children.reduce(function(p, v) {
      return p + recurse(v);
    }, 0);
    if (!node.id) node.id = ++i;
    nodes.push(node);
    return node.size;
  }

  root.size = recurse(root);
  return nodes;
}
circle.node {

  cursor: pointer;

  stroke: #34495e;

  stroke-width: 2px;

  box-sizing: border-box;

  stroke-location: inside;

}

line.link {

  fill: none;

  stroke: #34495e;

  stroke-width: 1.5px;

}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="viz"></div>

The difference here is the node.data call:

  node = svg.selectAll("circle.node")
            .data(nodes, function(d) {
                return d.name;
            })
            .style("fill", color);

The function in the .data(nodes, function...) call instructs d3 to use whatever is returned from the function as the key (see https://github.com/mbostock/d3/wiki/Selections#data for more info). This will ensure that the correct graphical elements are removed, added and updated when collapsing and expanding nodes.

Ben Lyall
  • 1,976
  • 1
  • 12
  • 14
2

Add titles to node like this.

title = svg.selectAll("text.title")    
     .data(nodes);

title.enter()
    .append("text") //In your code you used title instead of text
    .attr("class", "title")
    .text(function(d) { return d.name; });

title.exit().remove();

Note that titles should be appended after circle nodes. Or else titles may cut off.

Also update the position of title in tick function.

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

Here is the jsfiddle

Gilsha
  • 14,431
  • 3
  • 32
  • 47
  • This still has the second issue that I described below. Also, the title element is fine in SVG. It will just get displayed as a tooltip on hover. – Ben Lyall Nov 05 '14 at 12:17