|
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, to get labels take a look at this example:
http://bl.ocks.org/mbostock/950642
Instead of just having circles whose
cx
andcy
attributes are set using the force layout, you'll want to createg
elements whosetransform
attribute will be set in thetick
function. Thisg
will then contain a circle and text children for the node and label, respectively.