Skip to content

Instantly share code, notes, and snippets.

@0thernet
Last active July 3, 2023 14:07
Show Gist options
  • Save 0thernet/4370043 to your computer and use it in GitHub Desktop.
Save 0thernet/4370043 to your computer and use it in GitHub Desktop.
Force Editor + Pan/Zoom

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>
@lukemt
Copy link

lukemt commented Mar 9, 2014

that's funny :)

@owendall
Copy link

Nice work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment