Drag from an existing node to add a new node or link. Hit the DELETE key to remove the selected node or link.
Built with D3.js.
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<title>Force Editor</title> | |
<style> | |
body { | |
font: 13px sans-serif; | |
position: relative; | |
width: 960px; | |
height: 500px; | |
} | |
form { | |
position: absolute; | |
bottom: 10px; | |
left: 10px; | |
} | |
.node { | |
fill: #000; | |
cursor: crosshair; | |
} | |
.node_selected { | |
fill: #ff7f0e; | |
stroke: #ff7f0e; | |
} | |
.drag_line { | |
stroke: #999; | |
stroke-width: 5; | |
pointer-events: none; | |
} | |
.drag_line_hidden { | |
stroke: #999; | |
stroke-width: 0; | |
pointer-events: none; | |
} | |
.link { | |
stroke: #999; | |
stroke-width: 5; | |
cursor: crosshair; | |
} | |
.link_selected { | |
stroke: #ff7f0e; | |
} | |
</style> | |
<form> | |
</form> | |
<script src="http://d3js.org/d3.v3.min.js"></script> | |
<script> | |
var width = 960, | |
height = 500; | |
var fill = d3.scale.category20(); | |
// mouse event vars | |
var dragged = null, | |
selected_node = null, | |
selected_link = null, | |
mousedown_link = null, | |
mousedown_node = null, | |
mouseup_node = null; | |
// init force layout | |
var force = d3.layout.force() | |
.size([width, height]) | |
.nodes([{}]) // initialize with a single node | |
.linkDistance(50) | |
.charge(-200) | |
.on("tick", tick); | |
// init svg | |
var svg = d3.select("body").append("svg") | |
.attr("width", width) | |
.attr("height", height) | |
.on("mousemove", mousemove) | |
.on("mousedown", mousedown) | |
.on("mouseup", mouseup); | |
// line displayed when dragging new nodes | |
var drag_line = svg.append("line") | |
.attr("class", "drag_line") | |
.attr("x1", 0) | |
.attr("y1", 0) | |
.attr("x2", 0) | |
.attr("y2", 0); | |
// get layout properties | |
var nodes = force.nodes(), | |
links = force.links(), | |
node = svg.selectAll(".node"), | |
link = svg.selectAll(".link"); | |
// add keyboard callback | |
d3.select(window) | |
.on("keydown", keydown); | |
redraw(); | |
// focus on svg? | |
svg.node().focus(); | |
function mousedown() { | |
if (!mousedown_node && !mousedown_link) { | |
selected_node = null; | |
selected_link = null; | |
redraw(); | |
return; | |
} | |
if (mousedown_node) { | |
// reposition drag line | |
drag_line | |
.attr("class", "link") | |
.attr("x1", mousedown_node.x) | |
.attr("y1", mousedown_node.y) | |
.attr("x2", mousedown_node.x) | |
.attr("y2", mousedown_node.y); | |
} | |
redraw(); | |
} | |
function mousemove() { | |
if (!mousedown_node) return; | |
dragged = true; | |
// update drag line | |
drag_line | |
.attr("x1", mousedown_node.x) | |
.attr("y1", mousedown_node.y) | |
.attr("x2", d3.mouse(this)[0]) | |
.attr("y2", d3.mouse(this)[1]); | |
} | |
function mouseup() { | |
// hide drag line | |
drag_line | |
.attr("class", "drag_line_hidden") | |
if (mouseup_node == mousedown_node) { resetMouseVars(); return; } | |
if (mouseup_node) { | |
// add link | |
var link = {source: mousedown_node, target: mouseup_node}; | |
links.push(link); | |
// select new link | |
selected_link = link; | |
selected_node = null; | |
} | |
else { | |
// add node | |
var point = d3.mouse(this), | |
node = {x: point[0], y: point[1]}, | |
n = nodes.push(node); | |
// select new node | |
selected_node = node; | |
selected_link = null; | |
// add link to mousedown node | |
links.push({source: mousedown_node, target: node}); | |
} | |
// clear mouse event vars | |
resetMouseVars(); | |
redraw(); | |
} | |
function resetMouseVars() { | |
dragged = null; | |
mousedown_node = null; | |
mouseup_node = null; | |
mousedown_link = null; | |
} | |
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; }); | |
} | |
// redraw force layout | |
function redraw() { | |
link = link.data(links); | |
link.enter().insert("line", ".node") | |
.attr("class", "link") | |
.on("mousedown", | |
function(d) { | |
mousedown_link = d; | |
selected_link = d; | |
selected_node = null; | |
redraw(); }); | |
link.exit().remove(); | |
link | |
.classed("link_selected", function(d) { return d === selected_link; }); | |
node = node.data(nodes); | |
node.enter().insert("circle") | |
.attr("class", "node") | |
.attr("r", 5) | |
.on("mousedown", | |
function(d) { | |
mousedown_node = d; | |
selected_node = d; | |
selected_link = null; | |
redraw(); | |
}) | |
.on("mouseup", | |
function(d) { | |
selected_node = d; | |
mouseup_node = d; | |
redraw(); | |
}) | |
.transition() | |
.duration(750) | |
.ease("elastic") | |
.attr("r", 6.5); | |
node.exit().transition() | |
.attr("r", 0) | |
.remove(); | |
node | |
.classed("node_selected", function(d) { return d === selected_node; }); | |
if (d3.event) { | |
// prevent browser's default behavior | |
d3.event.preventDefault(); | |
} | |
force.start(); | |
} | |
function spliceLinksForNode(node) { | |
toSplice = links.filter( | |
function(l) { | |
return (l.source === node) || (l.target === node); }); | |
toSplice.map( | |
function(l) { | |
links.splice(links.indexOf(l), 1); }); | |
} | |
function keydown() { | |
if (!selected_node && !selected_link) return; | |
switch (d3.event.keyCode) { | |
case 8: // backspace | |
case 46: { // delete | |
if (selected_node) { | |
nodes.splice(nodes.indexOf(selected_node), 1); | |
spliceLinksForNode(selected_node); | |
} | |
else if (selected_link) { | |
links.splice(links.indexOf(selected_link), 1); | |
} | |
selected_link = null; | |
selected_node = null; | |
redraw(); | |
break; | |
} | |
} | |
} | |
</script> |