Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
D3 Selectable Force-Directed Graph

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);
}
}

Hey, how are you doing? Awesome work here, I was trying something and I'd like to add some texts labels to each node, I've been doing something like this

    label = node.append("text")
        .attr("id",function(d) { return d.id_discogs})
        .text(function(d) { return "\u00A0 \u00A0 \u00A0"+d.name; });

    $("text").attr("dominant-baseline","before-edge");

What am I doing wrong? Do I need to change something in your code? I'm newbie using d3.js

Thanks!

Owner

pkerpedjiev commented Jan 31, 2016

Hey, to get labels take a look at this example:

http://bl.ocks.org/mbostock/950642

Instead of just having circles whose cx and cy attributes are set using the force layout, you'll want to create g elements whose transform attribute will be set in the tick function. This g will then contain a circle and text children for the node and label, respectively.

Thanks for this. Learn a lot from it.

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