See SO answer.
You can pan (click background), zoom (mousewheel), drag nodes around. Middle click to add more nodes (at mouse).
| <!DOCTYPE html> | |
| <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="http://d3js.org/d3.v3.min.js" charset="utf-8"></script> | |
| </head> | |
| <body> | |
| <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://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> |