See SO answer.
You can pan (click background), zoom (mousewheel), drag nodes around. Middle click to add more nodes (at mouse).
forked from TWiStErRob's block: Zoom to fit
xxxxxxxxxx
<html>
<head>
<meta charset="utf-8" />
<title>Navigate and Zoom around graph</title>
<style>
body, html, svg {
padding: 0;
margin: 0;
width: 100%;
height: 100%;
}
#menu {
position: absolute;
bottom: 1em;
left: 1em;
}
.node {
cursor: pointer;
}
.node > rect {
fill: rgba(255,255,255,.9);
stroke-width: 3px;
stroke: #000;
rx: 4px;
ry: 4px;
shape-rendering: crispEdges;
}
.node > text.label {
fill: black;
text-anchor: middle;
alignment-baseline: central;
font-size: 13px;
font-family: sans-serif;
letter-spacing: -1px;
}
.node:hover > rect {
fill: black;
stroke: red;
}
.node:hover > text {
fill: white;
}
.link {
stroke: black;
stroke-width: 2px;
stroke-antialiasing: true;
}
</style>
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body>
<svg xmlns="https://www.w3.org/2000/svg" xmlns:xlink="https://www.w3.org/1999/xlink" pointer-events="all">
<defs>
<radialGradient id="background-gradient" cx="70%" cy="100%" r="90%" fy="60%">
<stop offset="5%" stop-color="#EEFFFF" />
<stop offset="95%" stop-color="#DDEEFF" />
</radialGradient>
</defs>
<rect id="background" width="100%" height="100%" fill="url(#background-gradient)" pointer-events="all" />
<g id="root">
<g id="links"></g>
<g id="nodes"></g>
</g>
</svg>
<div id="menu">
<button onclick="force.stop()">Freeze</button>
<button onclick="force.resume()">Thaw</button>
<button onclick="force.stop(); zoomFit(0.95, 500)">Fit</button>
</div>
<script>//<![CDATA[
var zoom = d3.behavior
.zoom()
.scaleExtent([1/4, 4])
.on('zoom.zoom', function () {
console.trace("zoom", d3.event.translate, d3.event.scale);
root.attr('transform',
'translate(' + d3.event.translate + ')'
+ 'scale(' + d3.event.scale + ')');
})
;
var svg = d3
.select('svg')
.call(zoom)
;
svg.select('#background')
.on('mousedown', function () {
if(d3.event.which != 2) return;
d3.event.preventDefault();
var point = d3.mouse(node_group.node());
var label = 'user@' + Math.round(point[0]) + ',' + Math.round(point[1]);
nodes.push(createNode(label, point[0], point[1]));
restart();
})
;
var root = svg.select('#root');
var node_group = svg.select('#nodes');
var link_group = svg.select('#links');
var force = d3.layout
.force()
.gravity(0.03)
.linkStrength(0.3)
.charge(-400)
.on('tick', 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('transform', function translate(d) {
return 'translate(' + d.x + ',' + d.y + ')';
})
;
})
;
var graph = {};
var nodes = force.nodes();
var links = force.links();
var node = node_group.selectAll('.node');
var link = link_group.selectAll('.link');
var uiNodes, uiLinks;
d3.select(window).on('resize', resize);
function resize() {
var width = window.innerWidth, height = window.innerHeight;
console.trace("Resize", force.size(), [width, height]);
force.size([width, height]).resume();
lapsedZoomFit(5, 0);
}
function restart(first) {
link = link.data(links);
link.exit().remove();
uiLinks = link
.enter()
.append('line')
.attr('id', function(d) {
return 'link_' + d.source.id + '_' + d.target.id;
})
.attr('class', 'link')
;
node = node.data(nodes);
node.exit().remove();
uiNodes = createNodes(node);
uiNodes
.call(force
.drag()
.on('dragstart', function() {
d3.event.sourceEvent.stopPropagation();
})
)
force.start();
if (first) {
lapsedZoomFit(undefined, 0);
}
}
function lapsedZoomFit(ticks, transitionDuration) {
for (var i = ticks || 200; i > 0; --i) force.tick();
force.stop();
zoomFit(undefined, transitionDuration);
}
function zoomFit(paddingPercent, transitionDuration) {
var bounds = root.node().getBBox();
var parent = root.node().parentElement;
var fullWidth = parent.clientWidth,
fullHeight = parent.clientHeight;
var width = bounds.width,
height = bounds.height;
var midX = bounds.x + width / 2,
midY = bounds.y + height / 2;
if (width == 0 || height == 0) return; // nothing to fit
var scale = (paddingPercent || 0.75) / Math.max(width / fullWidth, height / fullHeight);
var translate = [fullWidth / 2 - scale * midX, fullHeight / 2 - scale * midY];
console.trace("zoomFit", translate, scale);
root
.transition()
.duration(transitionDuration || 0) // milliseconds
.call(zoom.translate(translate).scale(scale).event);
}
function createNodes(nodeData) {
var uiNodes = nodeData
.enter()
.append('g')
.attr('id', function(d) { return 'node_' + d.id; })
;
nodeData
.attr("class", 'node');
;
var rect = uiNodes.append('rect')
;
var text = uiNodes.append('text')
.each(function(d) { d.label = this; })
.classed('label', true)
.text(function(d) { return d.id; })
;
var padding = {x: 5, y: 4};
rect
.attr('width', function(d) { return d.label.clientWidth + 2.0 * padding.x; })
.attr('height', function(d) { return d.label.clientHeight + 2.0 * padding.y; })
.attr('x', function(d) { return +d3.select(this).attr('width') / -2.0; })
.attr('y', function(d) { return +d3.select(this).attr('height') / -2.0; })
;
return nodeData;
}
function createNode(id, x,y) {
return {
id: id,
x: x,
y: y,
label: null,
toString: function() {
return this.id + " @ " + this.x + "," + this.y + " " + this.width + "x" + this.height;
}
};
}
function createLink(fromNode, toNode) {
return {
source: fromNode,
target: toNode,
weight: 1,
toString: function() {
return this.source + " -> " + this.target;
}
};
}
for (var i = 0; i < 20; ++i) {
var id = Math.floor(Math.random() * 10000000);
nodes.push(createNode(id, i%2? i : 0, i));
}
for (var i = 0; i < 10; ++i) {
var fromPos = Math.floor(Math.random() * nodes.length);
var toPos = Math.floor(Math.random() * nodes.length);
links.push(createLink(nodes[fromPos], nodes[toPos]));
}
// use middle click to add more nodes
restart(true);
//]]></script>
</body>
</html>
Modified http://d3js.org/d3.v3.min.js to a secure url
https://d3js.org/d3.v3.min.js