Skip to content

Instantly share code, notes, and snippets.

@mrcslws mrcslws/.block forked from mbostock/.block
Last active May 7, 2019

What would you like to do?
Pan & Zoom Axes
license: gpl-3.0

This demonstrates one way d3-zoom is quirky if you apply the zoom to an element within an SVG. Zoom in on this. Watch how it jerks to the side.

Now click "Fix it" to change away from d3-zoom's default behavior, specifying your own extent function. This fixes the issue. Zooming works as expected.

<!DOCTYPE html>
<meta charset="utf-8">
.axis path {
display: none;
.axis line {
stroke-opacity: 0.3;
shape-rendering: crispEdges;
.view {
fill: url(#gradient);
stroke: #000;
button {
position: absolute;
top: 20px;
left: 20px;
<button>Fix it</button>
<svg width="960" height="500">
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0.0%" stop-color="#2c7bb6"></stop>
<stop offset="12.5%" stop-color="#00a6ca"></stop>
<stop offset="25.0%" stop-color="#00ccbc"></stop>
<stop offset="37.5%" stop-color="#90eb9d"></stop>
<stop offset="50.0%" stop-color="#ffff8c"></stop>
<stop offset="62.5%" stop-color="#f9d057"></stop>
<stop offset="75.0%" stop-color="#f29e2e"></stop>
<stop offset="87.5%" stop-color="#e76818"></stop>
<stop offset="100.0%" stop-color="#d7191c"></stop>
<script src="//"></script>
// d3 uses the size of the entire <svg/> node to determine the zoom extent. This
// causes it to incorrectly handle the translateExtent if you're catching zoom
// events with a rect that's smaller than the SVG. But d3 lets you specify your
// own extent function, so there's a solution!
function svgRectExtent() {
var e = this, w, h;
return [[0, 0],
var padding = {top: 100, right: 100, bottom: 100, left: 100};
var svg ="svg"),
width = +svg.attr("width") - - padding.bottom,
height = +svg.attr("height") - padding.left - padding.right;
.attr('id', "myClipPath")
.attr('width', width)
.attr('height', height);
var zoom = d3.zoom()
.scaleExtent([1, 40])
.translateExtent([[0, 0], [width, height]])
.on("zoom", zoomed);
var x = d3.scaleLinear()
.domain([0, width])
.range([0, width]);
var y = d3.scaleLinear()
.domain([0, height])
.range([0, height]);
var xAxis = d3.axisBottom(x)
.tickPadding(8 - height);
var yAxis = d3.axisRight(y)
.tickPadding(8 - width);
var view = svg
.attr("transform", "translate(" + padding.left + "," + + ")")
.style("clip-path", "url(#myClipPath)")
.attr("class", "view")
.attr("width", width)
.attr("height", height);
var gX = svg.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(" + padding.left + "," + + ")")
var gY = svg.append("g")
.attr("class", "axis axis--y")
.attr("transform", "translate(" + padding.left + "," + + ")")
.on("click", fixIt);
var zoomCatcher = svg.append("rect")
.attr("class", "zoomCatcher")
.attr("x", padding.left)
.attr("width", width)
.attr("height", height)
.attr("fill", "transparent")
.attr("stroke", "none")
function zoomed() {
view.attr("transform", d3.event.transform);;;
function fixIt() {
.call(zoom.transform, d3.zoomIdentity);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.