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.
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.
<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; | |
} |