Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save billdwhite/7749428 to your computer and use it in GitHub Desktop.
Save billdwhite/7749428 to your computer and use it in GitHub Desktop.
A Pen by Bill White.

d3 Minimap Pan and Zoom Demo - Force Directed 1

A demo that explores another way to combine a forcelayout with the pan and zoom demo with a reusable forcecircle class.

A Pen by Bill White on CodePen.

License.

<script src="http://d3js.org/d3.v3.min.js"></script>
<h5 class="title">Use mousewheel to zoom. Drag image or minimap rectangle to pan.</h5>
<button id="addButton">Add Circle</button>
<div id="canvas"></div>
d3.demo = {};
d3.demo = {};
d3.demo.canvas = function() {
"use strict";
var width = 600,
height = 800,
zoomEnabled = true,
dragEnabled = true,
scale = 1,
translation = [0,0],
base = null,
wrapperBorder = 2,
minimap = null,
minimapPadding = 20,
minimapScale = 0.25,
force = d3.layout.force().size([width,height]),
nodes = [],
circles = [];
function canvas(selection) {
base = selection;
var xScale = d3.scale.linear()
.domain([-width / 2, width / 2])
.range([0, width]);
var yScale = d3.scale.linear()
.domain([-height / 2, height / 2])
.range([height, 0]);
var zoomHandler = function(newScale) {
if (!zoomEnabled) { return; }
if (d3.event) {
scale = d3.event.scale;
} else {
scale = newScale;
}
if (dragEnabled) {
var tbound = -height * scale,
bbound = height * scale,
lbound = -width * scale,
rbound = width * scale;
// limit translation to thresholds
translation = d3.event ? d3.event.translate : [0, 0];
translation = [
Math.max(Math.min(translation[0], rbound), lbound),
Math.max(Math.min(translation[1], bbound), tbound)
];
}
d3.select(".panCanvas, .panCanvas .bg")
.attr("transform", "translate(" + translation + ")" + " scale(" + scale + ")");
minimap.scale(scale).render();
}; // startoff zoomed in a bit to show pan/zoom rectangle
var zoom = d3.behavior.zoom()
.x(xScale)
.y(yScale)
.scaleExtent([0.5, 5])
.on("zoom.canvas", zoomHandler);
var svg = selection.append("svg")
.attr("class", "svg canvas")
.attr("width", width + (wrapperBorder*2) + minimapPadding*2 + (width*minimapScale))
.attr("height", height + (wrapperBorder*2) + minimapPadding*2)
.attr("shape-rendering", "auto");
var svgDefs = svg.append("defs");
svgDefs.append("clipPath")
.attr("id", "wrapperClipPath")
.attr("class", "wrapper clipPath")
.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height);
svgDefs.append("clipPath")
.attr("id", "minimapClipPath")
.attr("class", "minimap clipPath")
.attr("width", width)
.attr("height", height)
//.attr("transform", "translate(" + (width + minimapPadding) + "," + (minimapPadding/2) + ")")
.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height);
var filter = svgDefs.append("svg:filter")
.attr("id", "minimapDropShadow")
.attr("x", "-20%")
.attr("y", "-20%")
.attr("width", "150%")
.attr("height", "150%");
filter.append("svg:feOffset")
.attr("result", "offOut")
.attr("in", "SourceGraphic")
.attr("dx", "1")
.attr("dy", "1");
filter.append("svg:feColorMatrix")
.attr("result", "matrixOut")
.attr("in", "offOut")
.attr("type", "matrix")
.attr("values", "0.1 0 0 0 0 0 0.1 0 0 0 0 0 0.1 0 0 0 0 0 0.5 0");
filter.append("svg:feGaussianBlur")
.attr("result", "blurOut")
.attr("in", "matrixOut")
.attr("stdDeviation", "10");
filter.append("svg:feBlend")
.attr("in", "SourceGraphic")
.attr("in2", "blurOut")
.attr("mode", "normal");
var minimapRadialFill = svgDefs.append("radialGradient")
.attr({
id:"minimapGradient",
gradientUnits:"userSpaceOnUse",
cx:"500",
cy:"500",
r:"400",
fx:"500",
fy:"500"
});
minimapRadialFill.append("stop")
.attr("offset", "0%")
.attr("stop-color", "#FFFFFF");
minimapRadialFill.append("stop")
.attr("offset", "40%")
.attr("stop-color", "#EEEEEE");
minimapRadialFill.append("stop")
.attr("offset", "100%")
.attr("stop-color", "#E0E0E0");
var outerWrapper = svg.append("g")
.attr("class", "wrapper outer")
.attr("transform", "translate(0, " + minimapPadding + ")");
outerWrapper.append("rect")
.attr("class", "background")
.attr("width", width + wrapperBorder*2)
.attr("height", height + wrapperBorder*2);
var innerWrapper = outerWrapper.append("g")
.attr("class", "wrapper inner")
.attr("clip-path", "url(#wrapperClipPath)")
.attr("transform", "translate(" + (wrapperBorder) + "," + (wrapperBorder) + ")")
.call(zoom);
innerWrapper.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height);
var panCanvas = innerWrapper.append("g")
.attr("class", "panCanvas")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(0,0)");
panCanvas.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height);
minimap = d3.demo.minimap()
.zoom(zoom)
.target(panCanvas)
.minimapScale(minimapScale)
.x(width + minimapPadding)
.y(minimapPadding);
svg.call(minimap);
// startoff zoomed in a bit to show pan/zoom rectangle
zoom.scale(1.5);
zoomHandler(1.5);
/** ADD SHAPE **/
canvas.addItem = function(item) {
panCanvas.node().appendChild(item.node());
minimap.render();
};
canvas.addCircle = function(circle) {
panCanvas.call(circle);
circles.push(circle);
force.nodes(circles)
.size([width/2,height/2])
.on("tick", function() {
circles.forEach(function(circle) {
circle.cx(circle.x).cy(circle.y).render();
});
minimap.render();
})
.start();
function mousedown() {
circles.forEach(function(o, i) {
o.x += (Math.random() - 0.5) * 40;
o.y += (Math.random() - 0.5) * 40;
});
force.resume();
}
circle.node().on("mousedown", mousedown);
};
/** RENDER **/
canvas.render = function() {
svgDefs
.select(".clipPath .background")
.attr("width", width)
.attr("height", height);
svg
.attr("width", width + (wrapperBorder*2) + minimapPadding*2 + (width*minimapScale))
.attr("height", height + (wrapperBorder*2));
outerWrapper
.select(".background")
.attr("width", width + wrapperBorder*2)
.attr("height", height + wrapperBorder*2);
innerWrapper
.attr("transform", "translate(" + (wrapperBorder) + "," + (wrapperBorder) + ")")
.select(".background")
.attr("width", width)
.attr("height", height);
panCanvas
.attr("width", width)
.attr("height", height)
.select(".background")
.attr("width", width)
.attr("height", height);
minimap
.x(width + minimapPadding)
.y(minimapPadding)
.render();
};
canvas.zoomEnabled = function(isEnabled) {
if (!arguments.length) { return zoomEnabled }
zoomEnabled = isEnabled;
};
canvas.dragEnabled = function(isEnabled) {
if (!arguments.length) { return dragEnabled }
dragEnabled = isEnabled;
};
canvas.reset = function() {
d3.transition().duration(750).tween("zoom", function() {
var ix = d3.interpolate(xScale.domain(), [-width / 2, width / 2]),
iy = d3.interpolate(yScale.domain(), [-height / 2, height / 2]),
iz = d3.interpolate(scale, 1);
return function(t) {
zoom.scale(iz(t)).x(x.domain(ix(t))).y(y.domain(iy(t)));
zoomed(iz(t));
};
});
};
}
//============================================================
// Accessors
//============================================================
canvas.width = function(value) {
if (!arguments.length) return width;
width = parseInt(value, 10);
return this;
};
canvas.height = function(value) {
if (!arguments.length) return height;
height = parseInt(value, 10);
return this;
};
canvas.scale = function(value) {
if (!arguments.length) { return scale; }
scale = value;
return this;
};
canvas.nodes = function(value) {
if (!arguments.length) { return nodes; }
nodes = value;
return this;
};
return canvas;
};
/** MINIMAP **/
d3.demo.minimap = function() {
"use strict";
var minimapScale = 0.15,
scale = 1,
zoom = null,
base = null,
target = null,
width = 0,
height = 0,
x = 0,
y = 0,
frameX = 0,
frameY = 0;
function minimap(selection) {
base = selection;
var container = selection.append("g")
.attr("class", "minimap")
.call(zoom);
zoom.on("zoom.minimap", function() {
scale = d3.event.scale;
});
minimap.node = container.node();
var frame = container.append("g")
.attr("class", "frame")
frame.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height)
.attr("filter", "url(#minimapDropShadow)");
var drag = d3.behavior.drag()
.on("dragstart.minimap", function() {
var frameTranslate = d3.demo.util.getXYFromTranslate(frame.attr("transform"));
frameX = frameTranslate[0];
frameY = frameTranslate[1];
})
.on("drag.minimap", function() {
d3.event.sourceEvent.stopImmediatePropagation();
frameX += d3.event.dx;
frameY += d3.event.dy;
frame.attr("transform", "translate(" + frameX + "," + frameY + ")");
var translate = [(-frameX*scale),(-frameY*scale)];
target.attr("transform", "translate(" + translate + ")scale(" + scale + ")");
zoom.translate(translate);
});
frame.call(drag);
/** RENDER **/
minimap.render = function() {
scale = zoom.scale();
container.attr("transform", "translate(" + x + "," + y + ")scale(" + minimapScale + ")");
var node = target.node().cloneNode(true);
node.removeAttribute("id");
base.selectAll(".minimap .panCanvas").remove();
minimap.node.appendChild(node);
var targetTransform = d3.demo.util.getXYFromTranslate(target.attr("transform"));
frame.attr("transform", "translate(" + (-targetTransform[0]/scale) + "," + (-targetTransform[1]/scale) + ")")
.select(".background")
.attr("width", width/scale)
.attr("height", height/scale);
frame.node().parentNode.appendChild(frame.node());
d3.select(node).attr("transform", "translate(1,1)");
};
}
//============================================================
// Accessors
//============================================================
minimap.width = function(value) {
if (!arguments.length) return width;
width = parseInt(value, 10);
return this;
};
minimap.height = function(value) {
if (!arguments.length) return height;
height = parseInt(value, 10);
return this;
};
minimap.x = function(value) {
if (!arguments.length) return x;
x = parseInt(value, 10);
return this;
};
minimap.y = function(value) {
if (!arguments.length) return y;
y = parseInt(value, 10);
return this;
};
minimap.scale = function(value) {
if (!arguments.length) { return scale; }
scale = value;
return this;
};
minimap.minimapScale = function(value) {
if (!arguments.length) { return minimapScale; }
minimapScale = value;
return this;
};
minimap.zoom = function(value) {
if (!arguments.length) return zoom;
zoom = value;
return this;
};
minimap.target = function(value) {
if (!arguments.length) { return target; }
target = value;
width = parseInt(target.attr("width"), 10);
height = parseInt(target.attr("height"), 10);
return this;
};
return minimap;
};
d3.demo.forcecircle = function() {
"use strict";
var cx = 0,
cy = 0,
r = 0,
color = "#000000",
node = null,
base = null;
function forcecircle(selection) {
console.log("forcecircle()");
base = selection;
forcecircle.base = base;
node = base.append("circle")
.attr("class", "forcecircle");
function render() {
node.attr("cx", cx)
.attr("cy", cy)
.attr("r", r)
.style("fill", color);
}
forcecircle.render = render;
render();
}
//============================================================
// Accessors
//============================================================
forcecircle.cx = function(value) {
if (!arguments.length) return cx;
cx = parseInt(value, 10);
return this;
};
forcecircle.cy = function(value) {
if (!arguments.length) return cy;
cy = parseInt(value, 10);
return this;
};
forcecircle.r = function(value) {
if (!arguments.length) return r;
r = parseInt(value, 10);
return this;
};
forcecircle.color = function(value) {
if (!arguments.length) return color;
color = value;
return this;
};
forcecircle.node = function() {
return node;
};
forcecircle.x = 0;
forcecircle.y = 0;
return forcecircle;
};
/** UTILS **/
d3.demo.util = {};
d3.demo.util.getXYFromTranslate = function(translateString) {
var split = translateString.split(",");
var x = split[0] ? ~~split[0].split("(")[1] : 0;
var y = split[1] ? ~~split[1].split(")")[0] : 0;
return [x, y];
};
var circleCount = 0;
var canvas = d3.demo.canvas().width(430).height(375);
d3.select("#canvas").call(canvas);
var colorScale = d3.scale.category10().domain([0,20]);
var nodes = d3.range(10).map(function(i) {
return {index: i};
});
nodes.forEach(function(d, i){
circleCount = i;
var newCircle = d3.demo.forcecircle()
.cx(10)
.cy(10)
.r(8)
.color(colorScale(i));
newCircle.id = i;
canvas.addCircle(newCircle);
});
d3.select("#addButton").on("click", function() {
circleCount++;
var newCircle = d3.demo.forcecircle()
.cx(10)
.cy(10)
.r(8)
.color(colorScale(circleCount));
newCircle.id = circleCount;
canvas.addCircle(newCircle);
});
html, body {
font-family: Arial, sans-serif;
}
.title {
font-size: 12px;
}
.canvas .wrapper.outer > .background {
fill: #000000;
}
.canvas .wrapper.inner > .background {
fill: #CCCCCC;
cursor: move;
}
.canvas .background {
fill: #F6F6F6;
stroke: #333333;
cursor: move;
}
.canvas .panCanvas {
cursor: move;
}
.canvas .minimap .frame .background {
stroke: #111111;
stroke-width: 4px;
fill-opacity: 0.4;
fill: #000000;
fill: url(#minimapGradient);
filter: url(#minimapDropShadow);
cursor: move;
}
.forcecircle {
stroke: black;
stoke-width: 0.5px;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment