Svg D3.js semantic zoom misbehaving

Svg D3.js semantic zoom misbehaving,svg,d3.js,zooming,Svg,D3.js,Zooming,I've been trying to teach myself D3.js, but I can't seem to get semantic zoom (zooming positions but not shapes) to work for me. I've read the d3 zoom docs here, and attempted to functionally copy the svg semantic zoom example code This is my code: var X, Y, circle, circles, h, i, j, svg, transform, w, zoom, _i, _j; w = 1200; h = 600; circles = []; for (j = _i = 0; _i <= 6; j = ++_i) { for (i = _j = 0; _j <= 12; i = ++_j) { circles.push({r: 25, cx: i * 50, cy: j * 50}); } } X

I've been trying to teach myself D3.js, but I can't seem to get semantic zoom (zooming positions but not shapes) to work for me.

I've read the d3 zoom docs here, and attempted to functionally copy the svg semantic zoom example code

This is my code:

var X, Y, circle, circles, h, i, j, svg, transform, w, zoom, _i, _j;

w = 1200;
h = 600;

circles = [];
for (j = _i = 0; _i <= 6; j = ++_i) {
  for (i = _j = 0; _j <= 12; i = ++_j) {
    circles.push({r: 25, cx: i * 50, cy: j * 50});
  }
}

X = d3.scale.linear()
    .domain([0, 1])
    .range([0, 1]);
Y = d3.scale.linear()
    .domain([0, 1])
    .range([0, 1]);

zoom = d3.behavior.zoom()
  .x(X)
  .y(Y)
  .on("zoom", function() {
      return circle.attr("transform", transform);
  });

transform = function(d) {
  return "translate(" + (X(d.cx)) + ", " + (Y(d.cy)) + ")";
};

svg = d3.select("body")
    .append("svg")
    .attr("width", w)
    .attr("height", h)
    .call(zoom)
    .append("g");

circle = svg.selectAll("circle")
    .data(circles)
    .enter().append("circle")
    .attr("r", function(d) {
        return d.r;
    }).attr("cx", function(d) {
        return d.cx;
    }).attr("cy", function(d) {
        return d.cy;
    }).attr("transform", transform);

Live version at jsfiddle.

This should be pretty simple. I'm creating grid of circles that should exactly touch when no zoom is applied (distance is 50 px, diameter is 50 px). When I zoom in, I expect the circles to spread apart, with the point under the mouse remaining stationary. I expect the zoom to be smooth and linear with applied mouse wheeling. The circles should remain the same size, though, so that they stop touching when I zoom in; they should overlap when I zoom out.

Instead, initially, the circles are spread out exactly twice as far as they should be. When I zoom in and out, the center point is not under the mouse (and moves around depending on how I pan). Zoom is highly nonlinear, asymptotically approaching a scale of 1 (circles touching) as I zoom out, and rapidly accelerating as I zoom in.

This seems really odd, and I can't spot significant differences between my code and the semantic zoom example, which works as expected. I conclude that I don't actually understand how D3 zoom is supposed to work. Can someone sort me out?


#1

Your code is very close to being correct: Working demo.

Use scale to map the location of objects

Instead of saving the exact location of objects in them and then using scales with range and domain set to [0, 1], use the scales to do the mapping for you:

for (j = _i = 0; _i <= 6; j = ++_i) {
  for (i = _j = 0; _j <= 12; i = ++_j) {
    circles.push({
      r: 25,
      cx: i,
      cy: j,
      color: "#000"
    });
  }
}

X = d3.scale.linear()
    .domain([0, 6])
    .range([0, w]);
Y = d3.scale.linear()
    .domain([0, 12])
    .range([0, h]);

The change here is that now D3 knows about the aspect ratio of your viewport and in what proportions it should transform the scales so as to keep the point under the svg static under the mouse. Otherwise, it was trying to zoom in and out of a square, resulting in a jarring experience.


#2

The problem was the initial position of the circles stacking up on the translation.

Live code with the problem pointed out and fixed, and a few other modifications:

var size = 600
var scale = 100

circles = []
for (var j = 0; j<6; j++) {
    for (var i = 0; i<6; i++) {
        circles.push({x: i*scale, y: j*scale })
    }
}

var X = d3.scale.linear()
    .domain([0,6*scale])
    .range([0,size])
var Y = d3.scale.linear()
    .domain([0,6*scale])
    .range([0,size])

function transform(d) {
    return "translate("+X(d.x)+", "+Y(d.y)+")"
}

var circle  /*fwd declaration*/

var zoom = d3.behavior.zoom()
    .x(X).y(Y)
    .on("zoom", function () {
        circle.attr("transform", transform)
    })

var svg = d3.select("body").append("svg")
    .attr("width", size).attr("height", size)
    .call(zoom)
    .append("g")

circle = svg.selectAll("circle")
    .data(circles)
    .enter().append("circle")
    .attr("r", 20)
/*the problem was this initial offset interfering with the
translation we were applying, resulting in very strange behavior*/
/*    .attr("cx", function (d) {return d.x})
    .attr("cy", function (d) {return d.y})*/
    .attr("transform", transform)

The "scale" parameter should do nothing, but if you add in those commented lines, it affects the initial position and causes the non-intuitive effects.

The original problems were:

  • Initial scale appeared to be more zoomed than it should have been.

  • Zooming out very var produced a noticeable nonlinear asymptotic effect.

  • Zooming out then panning around, then zooming back in did not work at all like expected, with the diagram sliding under the mouse instead of staying pinned.

All of these are straightforward consequences of the initial position:

  • The initial distances appeared bigger because we applied their original positions plus the zoom translation.

  • The nonlinear asymptotic effect was the zoom translation distances going to zero asymptotically (as expected), but the initially applied distances not going to zero, giving the appearance of a nonzero zoom asymptote.

  • While zoomed out, D3 thinks it's zoomed out more than the user does (because of the extra distances between circles), which means when a pan is applied, the center of the image as D3 tracks it is moving differently than what the user expects, which causes the effect of the zoom center not being under the mouse.

You can play with these effects to understand them by uncommenting the initial position lines and applying the same zoom actions with different scale parameters. Commenting them causes the circles to initially be all at screen-space 0,0, so that only the zoom distance translation is applied, which is what we want.

Props to musically_ut's answer for suggesting the smaller world-space coordinate scale, which shouldn't have made any difference, but did, which helped me identify the problem.


#3

Your code has the 6 and 12 flipped for the x and y, and the behavior is a lot smoother on my browser without the .transion(). Even then, it works, but I can't figure out the scale function behavior. Why is scale still nonlinear (the circles asymptotically approach being 1 pixel apart, instead of 50 pixels apart)? Why does changing the arbitrary scale I map from change where the asymptote is (it shouldn't even be there)? Why do translation and scaling become unsynced at all?