Skip to content

Instantly share code, notes, and snippets.

@TWiStErRob
Last active January 11, 2017 07:03
Show Gist options
  • Save TWiStErRob/b1c62730e01fe33baa2dea0d0aa29359 to your computer and use it in GitHub Desktop.
Save TWiStErRob/b1c62730e01fe33baa2dea0d0aa29359 to your computer and use it in GitHub Desktop.
Zoom to fit

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment