Last active
April 16, 2024 09:41
-
-
Save sebbacon/6138241 to your computer and use it in GitHub Desktop.
Click-to-edit force directed graph in d3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| {"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}]} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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