Skip to content

Instantly share code, notes, and snippets.

@pkerpedjiev
Last active May 25, 2023 04:59
Show Gist options
  • Select an option

  • Save pkerpedjiev/0389e39fad95e1cf29ce to your computer and use it in GitHub Desktop.

Select an option

Save pkerpedjiev/0389e39fad95e1cf29ce to your computer and use it in GitHub Desktop.
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>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="plot.js"></script>
var width = 960,
height = 500,
shiftKey, ctrlKey;
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("body")
.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");
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) {
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;
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);
}
@rafaelbecks
Copy link
Copy Markdown

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!

@pkerpedjiev
Copy link
Copy Markdown
Author

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.

@owendall
Copy link
Copy Markdown

Thanks for this. Learn a lot from it.

@schoobani
Copy link
Copy Markdown

Thank you for the code. I followed your instruction but couldn't get what I expected:

I defined the transform function outside:

         var transform = d3.transform(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; });

        })

and then:

        node = node.data(graph.nodes).enter()
        .append("g")
        .attr(transform) 
        .append("circle")
        .attr("r", 8)
        .append("text).text(function(d) { return d.label; })
        .on("dblclick", function(d) { d3.event.stopPropagation(); })
        ."rest of the code"

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