Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save michiel/d4fafcba3580579acf19 to your computer and use it in GitHub Desktop.
Save michiel/d4fafcba3580579acf19 to your computer and use it in GitHub Desktop.

This is an extension of Mike Bostock's Draggable Network II example, allowing one to drag multiple nodes in a force-directed graph. Nodes can be selected by holding the shift key and either dragging on the canvas or clicking on specific nodes. The selection and dragging semantics aim to mirror those of most window managers:

  1. Shift clicking on a node toggles whether it is selected
  2. Clicking (without shift) on a node, selects it and deselects everything else.
  3. Shift dragging on the canvas toggles the selection status of the nodes enclosed within it.
  4. Dragging a set of selected nodes drags all of them.
  5. Clicking on the canvas de-selects everything.
{
"nodes":[
{
"x":444,
"y":275
},
{
"x":378,
"y":324
},
{
"x":478,
"y":278
},
{
"x":471,
"y":256
},
{
"x":382,
"y":269
},
{
"x":371,
"y":247
},
{
"x":359,
"y":276
},
{
"x":364,
"y":302
},
{
"x":400,
"y":330
},
{
"x":388,
"y":298
},
{
"x":524,
"y":296
},
{
"x":570,
"y":243
},
{
"x":552,
"y":159
},
{
"x":502,
"y":287
},
{
"x":511,
"y":313
},
{
"x":513,
"y":265
},
{
"x":602,
"y":132
},
{
"x":610,
"y":90
},
{
"x":592,
"y":91
},
{
"x":575,
"y":89
},
{
"x":607,
"y":73
},
{
"x":591,
"y":68
},
{
"x":574,
"y":73
},
{
"x":589,
"y":149
},
{
"x":620,
"y":205
},
{
"x":621,
"y":230
},
{
"x":589,
"y":234
},
{
"x":602,
"y":223
},
{
"x":548,
"y":188
},
{
"x":532,
"y":196
},
{
"x":548,
"y":114
},
{
"x":575,
"y":174
},
{
"x":497,
"y":250
},
{
"x":576,
"y":196
},
{
"x":504,
"y":201
},
{
"x":494,
"y":186
},
{
"x":482,
"y":199
},
{
"x":505,
"y":219
},
{
"x":486,
"y":216
},
{
"x":590,
"y":306
},
{
"x":677,
"y":169
},
{
"x":657,
"y":258
},
{
"x":667,
"y":205
},
{
"x":552,
"y":227
},
{
"x":518,
"y":173
},
{
"x":473,
"y":125
},
{
"x":796,
"y":260
},
{
"x":731,
"y":272
},
{
"x":642,
"y":288
},
{
"x":576,
"y":269
},
{
"x":605,
"y":187
},
{
"x":559,
"y":289
},
{
"x":544,
"y":356
},
{
"x":505,
"y":365
},
{
"x":579,
"y":289
},
{
"x":619,
"y":282
},
{
"x":574,
"y":329
},
{
"x":664,
"y":306
},
{
"x":627,
"y":304
},
{
"x":643,
"y":327
},
{
"x":664,
"y":348
},
{
"x":665,
"y":327
},
{
"x":653,
"y":317
},
{
"x":650,
"y":338
},
{
"x":622,
"y":321
},
{
"x":633,
"y":338
},
{
"x":647,
"y":357
},
{
"x":718,
"y":362
},
{
"x":636,
"y":240
},
{
"x":640,
"y":227
},
{
"x":617,
"y":249
},
{
"x":631,
"y":254
},
{
"x":566,
"y":213
},
{
"x":713,
"y":322
},
{
"x":716,
"y":298
},
{
"x":666,
"y":241
},
{
"x":627,
"y":355
}
],
"links":[
{
"source":1,
"target":0
},
{
"source":2,
"target":0
},
{
"source":3,
"target":0
},
{
"source":3,
"target":2
},
{
"source":4,
"target":0
},
{
"source":5,
"target":0
},
{
"source":6,
"target":0
},
{
"source":7,
"target":0
},
{
"source":8,
"target":0
},
{
"source":9,
"target":0
},
{
"source":11,
"target":10
},
{
"source":11,
"target":3
},
{
"source":11,
"target":2
},
{
"source":11,
"target":0
},
{
"source":12,
"target":11
},
{
"source":13,
"target":11
},
{
"source":14,
"target":11
},
{
"source":15,
"target":11
},
{
"source":17,
"target":16
},
{
"source":18,
"target":16
},
{
"source":18,
"target":17
},
{
"source":19,
"target":16
},
{
"source":19,
"target":17
},
{
"source":19,
"target":18
},
{
"source":20,
"target":16
},
{
"source":20,
"target":17
},
{
"source":20,
"target":18
},
{
"source":20,
"target":19
},
{
"source":21,
"target":16
},
{
"source":21,
"target":17
},
{
"source":21,
"target":18
},
{
"source":21,
"target":19
},
{
"source":21,
"target":20
},
{
"source":22,
"target":16
},
{
"source":22,
"target":17
},
{
"source":22,
"target":18
},
{
"source":22,
"target":19
},
{
"source":22,
"target":20
},
{
"source":22,
"target":21
},
{
"source":23,
"target":16
},
{
"source":23,
"target":17
},
{
"source":23,
"target":18
},
{
"source":23,
"target":19
},
{
"source":23,
"target":20
},
{
"source":23,
"target":21
},
{
"source":23,
"target":22
},
{
"source":23,
"target":12
},
{
"source":23,
"target":11
},
{
"source":24,
"target":23
},
{
"source":24,
"target":11
},
{
"source":25,
"target":24
},
{
"source":25,
"target":23
},
{
"source":25,
"target":11
},
{
"source":26,
"target":24
},
{
"source":26,
"target":11
},
{
"source":26,
"target":16
},
{
"source":26,
"target":25
},
{
"source":27,
"target":11
},
{
"source":27,
"target":23
},
{
"source":27,
"target":25
},
{
"source":27,
"target":24
},
{
"source":27,
"target":26
},
{
"source":28,
"target":11
},
{
"source":28,
"target":27
},
{
"source":29,
"target":23
},
{
"source":29,
"target":27
},
{
"source":29,
"target":11
},
{
"source":30,
"target":23
},
{
"source":31,
"target":30
},
{
"source":31,
"target":11
},
{
"source":31,
"target":23
},
{
"source":31,
"target":27
},
{
"source":32,
"target":11
},
{
"source":33,
"target":11
},
{
"source":33,
"target":27
},
{
"source":34,
"target":11
},
{
"source":34,
"target":29
},
{
"source":35,
"target":11
},
{
"source":35,
"target":34
},
{
"source":35,
"target":29
},
{
"source":36,
"target":34
},
{
"source":36,
"target":35
},
{
"source":36,
"target":11
},
{
"source":36,
"target":29
},
{
"source":37,
"target":34
},
{
"source":37,
"target":35
},
{
"source":37,
"target":36
},
{
"source":37,
"target":11
},
{
"source":37,
"target":29
},
{
"source":38,
"target":34
},
{
"source":38,
"target":35
},
{
"source":38,
"target":36
},
{
"source":38,
"target":37
},
{
"source":38,
"target":11
},
{
"source":38,
"target":29
},
{
"source":39,
"target":25
},
{
"source":40,
"target":25
},
{
"source":41,
"target":24
},
{
"source":41,
"target":25
},
{
"source":42,
"target":41
},
{
"source":42,
"target":25
},
{
"source":42,
"target":24
},
{
"source":43,
"target":11
},
{
"source":43,
"target":26
},
{
"source":43,
"target":27
},
{
"source":44,
"target":28
},
{
"source":44,
"target":11
},
{
"source":45,
"target":28
},
{
"source":47,
"target":46
},
{
"source":48,
"target":47
},
{
"source":48,
"target":25
},
{
"source":48,
"target":27
},
{
"source":48,
"target":11
},
{
"source":49,
"target":26
},
{
"source":49,
"target":11
},
{
"source":50,
"target":49
},
{
"source":50,
"target":24
},
{
"source":51,
"target":49
},
{
"source":51,
"target":26
},
{
"source":51,
"target":11
},
{
"source":52,
"target":51
},
{
"source":52,
"target":39
},
{
"source":53,
"target":51
},
{
"source":54,
"target":51
},
{
"source":54,
"target":49
},
{
"source":54,
"target":26
},
{
"source":55,
"target":51
},
{
"source":55,
"target":49
},
{
"source":55,
"target":39
},
{
"source":55,
"target":54
},
{
"source":55,
"target":26
},
{
"source":55,
"target":11
},
{
"source":55,
"target":16
},
{
"source":55,
"target":25
},
{
"source":55,
"target":41
},
{
"source":55,
"target":48
},
{
"source":56,
"target":49
},
{
"source":56,
"target":55
},
{
"source":57,
"target":55
},
{
"source":57,
"target":41
},
{
"source":57,
"target":48
},
{
"source":58,
"target":55
},
{
"source":58,
"target":48
},
{
"source":58,
"target":27
},
{
"source":58,
"target":57
},
{
"source":58,
"target":11
},
{
"source":59,
"target":58
},
{
"source":59,
"target":55
},
{
"source":59,
"target":48
},
{
"source":59,
"target":57
},
{
"source":60,
"target":48
},
{
"source":60,
"target":58
},
{
"source":60,
"target":59
},
{
"source":61,
"target":48
},
{
"source":61,
"target":58
},
{
"source":61,
"target":60
},
{
"source":61,
"target":59
},
{
"source":61,
"target":57
},
{
"source":61,
"target":55
},
{
"source":62,
"target":55
},
{
"source":62,
"target":58
},
{
"source":62,
"target":59
},
{
"source":62,
"target":48
},
{
"source":62,
"target":57
},
{
"source":62,
"target":41
},
{
"source":62,
"target":61
},
{
"source":62,
"target":60
},
{
"source":63,
"target":59
},
{
"source":63,
"target":48
},
{
"source":63,
"target":62
},
{
"source":63,
"target":57
},
{
"source":63,
"target":58
},
{
"source":63,
"target":61
},
{
"source":63,
"target":60
},
{
"source":63,
"target":55
},
{
"source":64,
"target":55
},
{
"source":64,
"target":62
},
{
"source":64,
"target":48
},
{
"source":64,
"target":63
},
{
"source":64,
"target":58
},
{
"source":64,
"target":61
},
{
"source":64,
"target":60
},
{
"source":64,
"target":59
},
{
"source":64,
"target":57
},
{
"source":64,
"target":11
},
{
"source":65,
"target":63
},
{
"source":65,
"target":64
},
{
"source":65,
"target":48
},
{
"source":65,
"target":62
},
{
"source":65,
"target":58
},
{
"source":65,
"target":61
},
{
"source":65,
"target":60
},
{
"source":65,
"target":59
},
{
"source":65,
"target":57
},
{
"source":65,
"target":55
},
{
"source":66,
"target":64
},
{
"source":66,
"target":58
},
{
"source":66,
"target":59
},
{
"source":66,
"target":62
},
{
"source":66,
"target":65
},
{
"source":66,
"target":48
},
{
"source":66,
"target":63
},
{
"source":66,
"target":61
},
{
"source":66,
"target":60
},
{
"source":67,
"target":57
},
{
"source":68,
"target":25
},
{
"source":68,
"target":11
},
{
"source":68,
"target":24
},
{
"source":68,
"target":27
},
{
"source":68,
"target":48
},
{
"source":68,
"target":41
},
{
"source":69,
"target":25
},
{
"source":69,
"target":68
},
{
"source":69,
"target":11
},
{
"source":69,
"target":24
},
{
"source":69,
"target":27
},
{
"source":69,
"target":48
},
{
"source":69,
"target":41
},
{
"source":70,
"target":25
},
{
"source":70,
"target":69
},
{
"source":70,
"target":68
},
{
"source":70,
"target":11
},
{
"source":70,
"target":24
},
{
"source":70,
"target":27
},
{
"source":70,
"target":41
},
{
"source":70,
"target":58
},
{
"source":71,
"target":27
},
{
"source":71,
"target":69
},
{
"source":71,
"target":68
},
{
"source":71,
"target":70
},
{
"source":71,
"target":11
},
{
"source":71,
"target":48
},
{
"source":71,
"target":41
},
{
"source":71,
"target":25
},
{
"source":72,
"target":26
},
{
"source":72,
"target":27
},
{
"source":72,
"target":11
},
{
"source":73,
"target":48
},
{
"source":74,
"target":48
},
{
"source":74,
"target":73
},
{
"source":75,
"target":69
},
{
"source":75,
"target":68
},
{
"source":75,
"target":25
},
{
"source":75,
"target":48
},
{
"source":75,
"target":41
},
{
"source":75,
"target":70
},
{
"source":75,
"target":71
},
{
"source":76,
"target":64
},
{
"source":76,
"target":65
},
{
"source":76,
"target":66
},
{
"source":76,
"target":63
},
{
"source":76,
"target":62
},
{
"source":76,
"target":48
},
{
"source":76,
"target":58
}
]
}
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.node .selected {
stroke: black;
}
.link {
stroke: #999;
}
.brush .extent {
fill-opacity: .1;
stroke: #fff;
shape-rendering: crispEdges;
}
</style>
<body>
<div align='center' id="d3_selectable_force_directed_graph"></div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="plot.js"></script>
<script>selectableForceDirectedGraph();</script>
function selectableForceDirectedGraph() {
var width = 960,
height = 500,
shiftKey, ctrlKey;
var nodeGraph = null;
var xScale = d3.scale.linear()
.domain([0,width]).range([0,width]);
var yScale = d3.scale.linear()
.domain([0,height]).range([0, height]);
var svg = d3.select("#d3_selectable_force_directed_graph")
.attr("tabindex", 1)
.on("keydown.brush", keydown)
.on("keyup.brush", keyup)
.each(function() { this.focus(); })
.append("svg")
.attr("width", width)
.attr("height", height);
var zoomer = d3.behavior.zoom().
scaleExtent([0.1,10]).
x(xScale).
y(yScale).
on("zoomstart", zoomstart).
on("zoom", redraw);
function zoomstart() {
node.each(function(d) {
d.selected = false;
d.previouslySelected = false;
});
node.classed("selected", false);
}
function redraw() {
vis.attr("transform",
"translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
}
var brusher = d3.svg.brush()
//.x(d3.scale.identity().domain([0, width]))
//.y(d3.scale.identity().domain([0, height]))
.x(xScale)
.y(yScale)
.on("brushstart", function(d) {
node.each(function(d) {
d.previouslySelected = shiftKey && d.selected; });
})
.on("brush", function() {
var extent = d3.event.target.extent();
node.classed("selected", function(d) {
return d.selected = d.previouslySelected ^
(extent[0][0] <= d.x && d.x < extent[1][0]
&& extent[0][1] <= d.y && d.y < extent[1][1]);
});
})
.on("brushend", function() {
d3.event.target.clear();
d3.select(this).call(d3.event.target);
});
var svg_graph = svg.append('svg:g')
.call(zoomer)
//.call(brusher)
var rect = svg_graph.append('svg:rect')
.attr('width', width)
.attr('height', height)
.attr('fill', 'transparent')
//.attr('opacity', 0.5)
.attr('stroke', 'transparent')
.attr('stroke-width', 1)
//.attr("pointer-events", "all")
.attr("id", "zrect")
var brush = svg_graph.append("g")
.datum(function() { return {selected: false, previouslySelected: false}; })
.attr("class", "brush");
var vis = svg_graph.append("svg:g");
vis.attr('fill', 'red')
.attr('stroke', 'black')
.attr('stroke-width', 1)
.attr('opacity', 0.5)
.attr('id', 'vis')
brush.call(brusher)
.on("mousedown.brush", null)
.on("touchstart.brush", null)
.on("touchmove.brush", null)
.on("touchend.brush", null);
brush.select('.background').style('cursor', 'auto');
var link = vis.append("g")
.attr("class", "link")
.selectAll("line");
var node = vis.append("g")
.attr("class", "node")
.selectAll("circle");
center_view = function() {
// Center the view on the molecule(s) and scale it so that everything
// fits in the window
if (nodeGraph === null)
return;
var nodes = nodeGraph.nodes;
//no molecules, nothing to do
if (nodes.length === 0)
return;
// Get the bounding box
min_x = d3.min(nodes.map(function(d) {return d.x;}));
min_y = d3.min(nodes.map(function(d) {return d.y;}));
max_x = d3.max(nodes.map(function(d) {return d.x;}));
max_y = d3.max(nodes.map(function(d) {return d.y;}));
// The width and the height of the graph
mol_width = max_x - min_x;
mol_height = max_y - min_y;
// how much larger the drawing area is than the width and the height
width_ratio = width / mol_width;
height_ratio = height / mol_height;
// we need to fit it in both directions, so we scale according to
// the direction in which we need to shrink the most
min_ratio = Math.min(width_ratio, height_ratio) * 0.8;
// the new dimensions of the molecule
new_mol_width = mol_width * min_ratio;
new_mol_height = mol_height * min_ratio;
// translate so that it's in the center of the window
x_trans = -(min_x) * min_ratio + (width - new_mol_width) / 2;
y_trans = -(min_y) * min_ratio + (height - new_mol_height) / 2;
// do the actual moving
vis.attr("transform",
"translate(" + [x_trans, y_trans] + ")" + " scale(" + min_ratio + ")");
// tell the zoomer what we did so that next we zoom, it uses the
// transformation we entered here
zoomer.translate([x_trans, y_trans ]);
zoomer.scale(min_ratio);
};
function dragended(d) {
//d3.select(self).classed("dragging", false);
node.filter(function(d) { return d.selected; })
.each(function(d) { d.fixed &= ~6; })
}
d3.json("graph.json", function(error, graph) {
nodeGraph = graph;
graph.links.forEach(function(d) {
d.source = graph.nodes[d.source];
d.target = graph.nodes[d.target];
});
link = link.data(graph.links).enter().append("line")
.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; });
var force = d3.layout.force()
.charge(-120)
.linkDistance(30)
.nodes(graph.nodes)
.links(graph.links)
.size([width, height])
.start();
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
if (!d.selected && !shiftKey) {
// if this node isn't selected, then we have to unselect every other node
node.classed("selected", function(p) { return p.selected = p.previouslySelected = false; });
}
d3.select(this).classed("selected", function(p) { d.previouslySelected = d.selected; return d.selected = true; });
node.filter(function(d) { return d.selected; })
.each(function(d) { d.fixed |= 2; })
}
function dragged(d) {
node.filter(function(d) { return d.selected; })
.each(function(d) {
d.x += d3.event.dx;
d.y += d3.event.dy;
d.px += d3.event.dx;
d.py += d3.event.dy;
})
force.resume();
}
node = node.data(graph.nodes).enter().append("circle")
.attr("r", 4)
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.on("dblclick", function(d) { d3.event.stopPropagation(); })
.on("click", function(d) {
if (d3.event.defaultPrevented) return;
if (!shiftKey) {
//if the shift key isn't down, unselect everything
node.classed("selected", function(p) { return p.selected = p.previouslySelected = false; })
}
// always select this node
d3.select(this).classed("selected", d.selected = !d.previouslySelected);
})
.on("mouseup", function(d) {
//if (d.selected && shiftKey) d3.select(this).classed("selected", d.selected = false);
})
.call(d3.behavior.drag()
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended));
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; });
};
force.on("tick", tick);
});
function keydown() {
shiftKey = d3.event.shiftKey || d3.event.metaKey;
ctrlKey = d3.event.ctrlKey;
console.log('d3.event', d3.event)
if (d3.event.keyCode == 67) { //the 'c' key
center_view();
}
if (shiftKey) {
svg_graph.call(zoomer)
.on("mousedown.zoom", null)
.on("touchstart.zoom", null)
.on("touchmove.zoom", null)
.on("touchend.zoom", null);
//svg_graph.on('zoom', null);
vis.selectAll('g.gnode')
.on('mousedown.drag', null);
brush.select('.background').style('cursor', 'crosshair')
brush.call(brusher);
}
}
function keyup() {
shiftKey = d3.event.shiftKey || d3.event.metaKey;
ctrlKey = d3.event.ctrlKey;
brush.call(brusher)
.on("mousedown.brush", null)
.on("touchstart.brush", null)
.on("touchmove.brush", null)
.on("touchend.brush", null);
brush.select('.background').style('cursor', 'auto')
svg_graph.call(zoomer);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment