Drag from an existing node to add a new node or link. Click to select/deselect nodes/links. Hit the DELETE key to remove the selected node or link. Drag to pan. Scroll to zoom.
Built with D3.js.
var width = 960, | |
height = 500, | |
fill = d3.scale.category20(); | |
// mouse event vars | |
var selected_node = null, | |
selected_link = null, | |
mousedown_link = null, | |
mousedown_node = null, | |
mouseup_node = null; | |
// init svg | |
var outer = d3.select("#chart") | |
.append("svg:svg") | |
.attr("width", width) | |
.attr("height", height) | |
.attr("pointer-events", "all"); | |
var vis = outer | |
.append('svg:g') | |
.call(d3.behavior.zoom().on("zoom", rescale)) | |
.on("dblclick.zoom", null) | |
.append('svg:g') | |
.on("mousemove", mousemove) | |
.on("mousedown", mousedown) | |
.on("mouseup", mouseup); | |
vis.append('svg:rect') | |
.attr('width', width) | |
.attr('height', height) | |
.attr('fill', 'white'); | |
// init force layout | |
var force = d3.layout.force() | |
.size([width, height]) | |
.nodes([{}]) // initialize with a single node | |
.linkDistance(50) | |
.charge(-200) | |
.on("tick", tick); | |
// line displayed when dragging new nodes | |
var drag_line = vis.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 = vis.selectAll(".node"), | |
link = vis.selectAll(".link"); | |
// add keyboard callback | |
d3.select(window) | |
.on("keydown", keydown); | |
redraw(); | |
// focus on svg | |
// vis.node().focus(); | |
function mousedown() { | |
if (!mousedown_node && !mousedown_link) { | |
// allow panning if nothing is selected | |
vis.call(d3.behavior.zoom().on("zoom"), rescale); | |
return; | |
} | |
} | |
function mousemove() { | |
if (!mousedown_node) return; | |
// update drag line | |
drag_line | |
.attr("x1", mousedown_node.x) | |
.attr("y1", mousedown_node.y) | |
.attr("x2", d3.svg.mouse(this)[0]) | |
.attr("y2", d3.svg.mouse(this)[1]); | |
} | |
function mouseup() { | |
if (mousedown_node) { | |
// hide drag line | |
drag_line | |
.attr("class", "drag_line_hidden") | |
if (!mouseup_node) { | |
// 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}); | |
} | |
redraw(); | |
} | |
// clear mouse event vars | |
resetMouseVars(); | |
} | |
function resetMouseVars() { | |
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; }); | |
} | |
// rescale g | |
function rescale() { | |
trans=d3.event.translate; | |
scale=d3.event.scale; | |
vis.attr("transform", | |
"translate(" + trans + ")" | |
+ " scale(" + scale + ")"); | |
} | |
// redraw force layout | |
function redraw() { | |
link = link.data(links); | |
link.enter().insert("line", ".node") | |
.attr("class", "link") | |
.on("mousedown", | |
function(d) { | |
mousedown_link = d; | |
if (mousedown_link == selected_link) selected_link = null; | |
else selected_link = mousedown_link; | |
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) { | |
// disable zoom | |
vis.call(d3.behavior.zoom().on("zoom"), null); | |
mousedown_node = d; | |
if (mousedown_node == selected_node) selected_node = null; | |
else selected_node = mousedown_node; | |
selected_link = null; | |
// 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(); | |
}) | |
.on("mousedrag", | |
function(d) { | |
// redraw(); | |
}) | |
.on("mouseup", | |
function(d) { | |
if (mousedown_node) { | |
mouseup_node = d; | |
if (mouseup_node == mousedown_node) { resetMouseVars(); return; } | |
// add link | |
var link = {source: mousedown_node, target: mouseup_node}; | |
links.push(link); | |
// select new link | |
selected_link = link; | |
selected_node = null; | |
// enable zoom | |
vis.call(d3.behavior.zoom().on("zoom"), rescale); | |
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; | |
} | |
} | |
} |
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<head> | |
<title>Force Editor</title> | |
<!-- <script src="d3.v2.min.js"></script> --> | |
<script src="http://d3js.org/d3.v2.min.js"></script> | |
<!-- <script src="jquery-1.8.3.min.js"></script> --> | |
<style> | |
body { | |
font: 13px sans-serif; | |
position: relative; | |
width: 960px; | |
height: 500px; | |
} | |
.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> | |
<head> | |
<body> | |
<div id="chart"> | |
</div> | |
<script src="force_view.js"></script> | |
</body> | |
</html> |
that's funny :)