Skip to content

Instantly share code, notes, and snippets.

@billdwhite
Last active September 13, 2017 11:58
Show Gist options
  • Save billdwhite/496a140e7ab26cef02635449b3563e54 to your computer and use it in GitHub Desktop.
Save billdwhite/496a140e7ab26cef02635449b3563e54 to your computer and use it in GitHub Desktop.
d3 v4 Minimap Pan and Zoom Demo
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

d3 v4 Minimap Pan and Zoom Demo

This is a d3 viewer that shows how to incorporate a minimap to provide a scaled overview of the content of the canvas.

A Pen by Bill White on CodePen.

License.

<script src="https://d3js.org/d3.v4.min.js"></script>
<h5 class="title">Use mousewheel to zoom. Drag image or minimap rectangle to pan.</h5>
<div id="canvasqPWKOg" class="canvas"></div>
<button id="resetButtonqPWKOg">Reset</button>
d3.demo = {};
/** CANVAS **/
d3.demo.canvas = function() {
"use strict";
var width = 500,
height = 500,
base = null,
wrapperBorder = 0,
minimap = null,
minimapPadding = 10,
minimapScale = 0.25;
function canvas(selection) {
base = selection;
var svgWidth = (width + (wrapperBorder*2) + minimapPadding*2 + (width*minimapScale));
var svgHeight = (height + (wrapperBorder*2) + minimapPadding*2);
var svg = selection.append("svg")
.attr("class", "svg canvas")
.attr("width", svgWidth)
.attr("height", svgHeight)
.attr("shape-rendering", "auto");
var svgDefs = svg.append("defs");
svgDefs.append("clipPath")
.attr("id", "wrapperClipPath_qwpyza")
.attr("class", "wrapper clipPath")
.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height);
svgDefs.append("clipPath")
.attr("id", "minimapClipPath_qwpyza")
.attr("class", "minimap clipPath")
.attr("width", width)
.attr("height", height)
.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height);
var filter = svgDefs.append("svg:filter")
.attr("id", "minimapDropShadow_qwpyza")
.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")
.attr('gradientUnits', "userSpaceOnUse")
.attr('cx', "500")
.attr('cy', "500")
.attr('r', "400")
.attr('fx', "500")
.attr('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_qwpyza)")
.attr("transform", "translate(" + (wrapperBorder) + "," + (wrapperBorder) + ")");
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);
var zoom = d3.zoom()
.scaleExtent([0.5, 5]);
// updates the zoom boundaries based on the current size and scale
var updateCanvasZoomExtents = function() {
var scale = innerWrapper.property("__zoom").k;
var targetWidth = svgWidth;
var targetHeight = svgHeight;
var viewportWidth = width;
var viewportHeight = height;
zoom.translateExtent([
[-viewportWidth/scale, -viewportHeight/scale],
[(viewportWidth/scale + targetWidth), (viewportHeight/scale + targetHeight)]
]);
};
var zoomHandler = function() {
panCanvas.attr("transform", d3.event.transform);
// here we filter out the emitting of events that originated outside of the normal ZoomBehavior; this prevents an infinite loop
// between the host and the minimap
if (d3.event.sourceEvent instanceof MouseEvent || d3.event.sourceEvent instanceof WheelEvent) {
minimap.update(d3.event.transform);
}
updateCanvasZoomExtents();
};
zoom.on("zoom", zoomHandler);
innerWrapper.call(zoom);
// initialize the minimap, passing needed references
minimap = d3.demo.minimap()
.host(canvas)
.target(panCanvas)
.minimapScale(minimapScale)
.x(width + minimapPadding)
.y(minimapPadding);
svg.call(minimap);
/** ADD SHAPE **/
canvas.addItem = function(item) {
panCanvas.node().appendChild(item.node());
minimap.render();
};
/** 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.reset = function() {
//svg.call(zoom.event);
//svg.transition().duration(750).call(zoom.event);
zoom.transform(panCanvas, d3.zoomIdentity);
svg.property("__zoom", d3.zoomIdentity);
minimap.update(d3.zoomIdentity);
};
canvas.update = function(minimapZoomTransform) {
zoom.transform(panCanvas, minimapZoomTransform);
// update the '__zoom' property with the new transform on the rootGroup which is where the zoomBehavior stores it since it was the
// call target during initialization
innerWrapper.property("__zoom", minimapZoomTransform);
updateCanvasZoomExtents();
};
updateCanvasZoomExtents();
}
//============================================================
// 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;
};
return canvas;
};
/** MINIMAP **/
d3.demo.minimap = function() {
"use strict";
var minimapScale = 0.15,
host = null,
base = null,
target = null,
width = 0,
height = 0,
x = 0,
y = 0;
function minimap(selection) {
base = selection;
var zoom = d3.zoom()
.scaleExtent([0.5, 5]);
// updates the zoom boundaries based on the current size and scale
var updateMinimapZoomExtents = function() {
var scale = container.property("__zoom").k;
var targetWidth = parseInt(target.attr("width"));
var targetHeight = parseInt(target.attr("height"));
var viewportWidth = host.width();
var viewportHeight = host.height();
zoom.translateExtent([
[-viewportWidth/scale, -viewportHeight/scale],
[(viewportWidth/scale + targetWidth), (viewportHeight/scale + targetHeight)]
]);
};
var zoomHandler = function() {
frame.attr("transform", d3.event.transform);
// here we filter out the emitting of events that originated outside of the normal ZoomBehavior; this prevents an infinite loop
// between the host and the minimap
if (d3.event.sourceEvent instanceof MouseEvent || d3.event.sourceEvent instanceof WheelEvent) {
// invert the outgoing transform and apply it to the host
var transform = d3.event.transform;
// ordering matters here! you have to scale() before you translate()
var modifiedTransform = d3.zoomIdentity.scale(1/transform.k).translate(-transform.x, -transform.y);
host.update(modifiedTransform);
}
updateMinimapZoomExtents();
};
zoom.on("zoom", zoomHandler);
var container = selection.append("g")
.attr("class", "minimap");
container.call(zoom);
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_qPWKOg)");
minimap.update = function(hostTransform) {
// invert the incoming zoomTransform; ordering matters here! you have to scale() before you translate()
var modifiedTransform = d3.zoomIdentity.scale((1/hostTransform.k)).translate(-hostTransform.x, -hostTransform.y);
// call this.zoom.transform which will reuse the handleZoom method below
zoom.transform(frame, modifiedTransform);
// update the new transform onto the minimapCanvas which is where the zoomBehavior stores it since it was the call target during initialization
container.property("__zoom", modifiedTransform);
updateMinimapZoomExtents();
};
/** RENDER **/
minimap.render = function() {
// update the placement of the minimap
container.attr("transform", "translate(" + x + "," + y + ")scale(" + minimapScale + ")");
// update the visualization being shown by the minimap in case its appearance has changed
var node = target.node().cloneNode(true);
node.removeAttribute("id");
base.selectAll(".minimap .panCanvas").remove();
minimap.node.appendChild(node); // minimap node is the container's node
d3.select(node).attr("transform", "translate(0,0)");
// keep the minimap's viewport (frame) sized to match the current visualization viewport dimensions
frame.select(".background")
.attr("width", width)
.attr("height", height);
frame.node().parentNode.appendChild(frame.node());
};
updateMinimapZoomExtents();
}
//============================================================
// 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.host = function(value) {
if (!arguments.length) { return host;}
host = value;
return this;
}
minimap.minimapScale = function(value) {
if (!arguments.length) { return minimapScale; }
minimapScale = 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;
};
/** RUN SCRIPT **/
var canvasWidth = 800;
var canvas = d3.demo.canvas().width(435).height(400);
d3.select("#canvasqPWKOg").call(canvas);
d3.select("#resetButtonqPWKOg").on("click", function() {
canvas.reset();
});
//d3.xml("https://upload.wikimedia.org/wikipedia/en/1/15/Logo_D3.svg",function(error, xml) {
d3.xml("https://gist.githubusercontent.com/billdwhite/496a140e7ab26cef02635449b3563e54/raw/50a49bfbcafbe1005cba39a118e8b609c4d4ca29/butterfly.svg",function(error, xml) {
if (error) throw error;
addItem(xml.documentElement);
});
function addItem(item) {
canvas.addItem(d3.select(item));
}
html, body {
font-family: Arial, sans-serif;
}
.title {
font-size: 12px;
}
.canvas {
overflow: hidden;
}
.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 {
pointer-events: all;
}
.canvas .minimap .frame .background {
stroke: #111111;
stroke-width: 4px;
fill-opacity: 0.1;
fill: #000000;
fill: url(#minimapGradient_qwpyza);
filter: url(#minimapDropShadow_qwpyza);
cursor: move;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment