Skip to content

Instantly share code, notes, and snippets.

@sebbacon
Last active April 16, 2024 09:41
Show Gist options
  • Select an option

  • Save sebbacon/6138241 to your computer and use it in GitHub Desktop.

Select an option

Save sebbacon/6138241 to your computer and use it in GitHub Desktop.
Click-to-edit force directed graph in d3
{"nodes":[{"name":"Myriel","group":1},{"name":"Napoleon","group":1},{"name":"Mlle.Baptistine","group":1},{"name":"Mme.Magloire","group":1},{"name":"CountessdeLo","group":1},{"name":"Geborand","group":1},{"name":"Champtercier","group":1},{"name":"Cravatte","group":1},{"name":"Count","group":1},{"name":"OldMan","group":1}],"links":[{"source":0,"target":1,"value":1},{"source":1,"target":2,"value":8},{"source":1,"target":3,"value":10},{"source":3,"target":4,"value":1},{"source":3,"target":5,"value":1},{"source":4,"target":6,"value":1},{"source":6,"target":7,"value":1},{"source":1,"target":4,"value":2},{"source":7,"target":8,"value":1},{"source":8,"target":9,"value":1},{"source":1,"target":9,"value":1},{"source":3,"target":9,"value":1}]}
marker#child {
fill: red;
}
g.node circle {
stroke: #fff;
stroke-width: 1.5px;
}
g.node.selected circle {
fill: red;
stroke-width: 1.5px;
}
g.node.selected_target circle {
fill: pink;
stroke-width: 1.5px;
}
g.node text, g.node a {
pointer-events: none;
font: 10px sans-serif;
}
line.link {
stroke: #999;
stroke-opacity: .8;
stroke-width: 6px;
}
line.new_line {
stroke: yellow;
stroke-opacity: .8;
stroke-width: 6px;
}
line.selected {
stroke: red;
stroke-opacity: 1;
}
rect {
fill: white;
}
var width = 960,
height = 500,
selected_node, selected_target_node,
selected_link, new_line,
circlesg, linesg,
should_drag = false,
drawing_line = false,
nodes = [],
links = [],
link_distance = 90;
var default_name = "new node"
var force = d3.layout.force()
.charge(-340)
.linkDistance(link_distance)
.size([width, height]);
var svg = d3.select("#chart").append("svg")
.attr("width", width)
.attr("height", height);
d3.select(window)
.on("mousemove", mousemove)
.on("mouseup", mouseup)
.on("keydown", keydown)
.on("keyup", keyup);
svg.append("rect")
.attr("width", width)
.attr("height", height)
.on("mousedown", mousedown);
// Arrow marker
svg.append("svg:defs").selectAll("marker")
.data(["child"])
.enter().append("svg:marker")
.attr("id", String)
.attr("markerUnits", "userSpaceOnUse")
.attr("viewBox", "0 -5 10 10")
.attr("refX", link_distance)
.attr("refY", -1.1)
.attr("markerWidth", 10)
.attr("markerHeight", 10)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
linesg = svg.append("g");
circlesg = svg.append("g");
d3.json("./bob.json", function(json) {
// decorate a node with a count of its children
nodes = json.nodes;
links = json.links;
update();
force = force
.nodes(nodes)
.links(links);
force.start();
});
function update() {
var link = linesg.selectAll("line.link")
.data(links)
.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; })
.classed("selected", function(d) { return d === selected_link; });
link.enter().append("line")
.attr("class", "link")
.attr("marker-end", "url(#child)")
.on("mousedown", line_mousedown);
link.exit().remove();
var node = circlesg.selectAll(".node")
.data(nodes, function(d) {return d.name;})
.classed("selected", function(d) { return d === selected_node; })
.classed("selected_target", function(d) { return d === selected_target_node; })
var nodeg = node.enter()
.append("g")
.attr("class", "node").call(force.drag)
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
nodeg.append("circle")
.attr("r", 10)
.on("mousedown", node_mousedown)
.on("mouseover", node_mouseover)
.on("mouseout", node_mouseout);
nodeg
.append("svg:a")
.attr("xlink:href", function (d) { return d.url || '#'; })
.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) {return d.name});
node.exit().remove();
force.on("tick", function(e) {
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("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
});
}
// select target node for new node connection
function node_mouseover(d) {
if (drawing_line && d !== selected_node) {
// highlight and select target node
selected_target_node = d;
}
}
function node_mouseout(d) {
if (drawing_line) {
selected_target_node = null;
}
}
// select node / start drag
function node_mousedown(d) {
if (!drawing_line) {
selected_node = d;
selected_link = null;
}
if (!should_drag) {
d3.event.stopPropagation();
drawing_line = true;
}
d.fixed = true;
force.stop()
update();
}
// select line
function line_mousedown(d) {
selected_link = d;
selected_node = null;
update();
}
// draw yellow "new connector" line
function mousemove() {
if (drawing_line && !should_drag) {
var m = d3.mouse(svg.node());
var x = Math.max(0, Math.min(width, m[0]));
var y = Math.max(0, Math.min(height, m[1]));
// debounce - only start drawing line if it gets a bit big
var dx = selected_node.x - x;
var dy = selected_node.y - y;
if (Math.sqrt(dx * dx + dy * dy) > 10) {
// draw a line
if (!new_line) {
new_line = linesg.append("line").attr("class", "new_line");
}
new_line.attr("x1", function(d) { return selected_node.x; })
.attr("y1", function(d) { return selected_node.y; })
.attr("x2", function(d) { return x; })
.attr("y2", function(d) { return y; });
}
}
update();
}
// add a new disconnected node
function mousedown() {
m = d3.mouse(svg.node())
nodes.push({x: m[0], y: m[1], name: default_name + " " + nodes.length, group: 1});
selected_link = null;
force.stop();
update();
force.start();
}
// end node select / add new connected node
function mouseup() {
drawing_line = false;
if (new_line) {
if (selected_target_node) {
selected_target_node.fixed = false;
var new_node = selected_target_node;
} else {
var m = d3.mouse(svg.node());
var new_node = {x: m[0], y: m[1], name: default_name + " " + nodes.length, group: 1}
nodes.push(new_node);
}
selected_node.fixed = false;
links.push({source: selected_node, target: new_node})
selected_node = selected_target_node = null;
update();
setTimeout(function () {
new_line.remove();
new_line = null;
force.start();
}, 300);
}
}
function keyup() {
switch (d3.event.keyCode) {
case 16: { // shift
should_drag = false;
update();
force.start();
}
}
}
// select for dragging node with shift; delete node with backspace
function keydown() {
switch (d3.event.keyCode) {
case 8: // backspace
case 46: { // delete
if (selected_node) { // deal with nodes
var i = nodes.indexOf(selected_node);
nodes.splice(i, 1);
// find links to/from this node, and delete them too
var new_links = [];
links.forEach(function(l) {
if (l.source !== selected_node && l.target !== selected_node) {
new_links.push(l);
}
});
links = new_links;
selected_node = nodes.length ? nodes[i > 0 ? i - 1 : 0] : null;
} else if (selected_link) { // deal with links
var i = links.indexOf(selected_link);
links.splice(i, 1);
selected_link = links.length ? links[i > 0 ? i - 1 : 0] : null;
}
update();
break;
}
case 16: { // shift
should_drag = true;
break;
}
}
}
<!DOCTYPE html>
<html>
<head>
<title>Editable Force-Directed Layout</title>
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
<link type="text/css" rel="stylesheet" href="./force.css"/>
</head>
<body>
Click to add a node; drag from a node to connect nodes; shift-drag to move nodes around.
<div id="chart"></div>
<script type="text/javascript" src="./force.js"></script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment