Skip to content

Instantly share code, notes, and snippets.

@ngminhtrung
Forked from pkerpedjiev/ SelectableForceDirectedGraph
Last active May 24, 2018 07:20
Show Gist options
  • Save ngminhtrung/839cd195a7598b3d0b75a2efc5999f84 to your computer and use it in GitHub Desktop.
Save ngminhtrung/839cd195a7598b3d0b75a2efc5999f84 to your computer and use it in GitHub Desktop.
D3 Selectable Force-Directed Graph

Forked from https://gist.github.com/pkerpedjiev/0389e39fad95e1cf29ce

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