Saturday, October 6, 2012

Zoom and center with d3

I've been spending lots of time with Data Driven Documents (d3) for building data visualizations.  At its simplest, it's a bare bones library for binding data to SVG to create visualizations.  For anyone who has used it, the warnings are usually the same, "It's a steep learning curve".  The funny thing is that d3 is fairly simple.  The problem is that you need to be fairly intimate with drawing and transforming shapes, lines and nodes using SVG.  That was the most difficult part for me and it's nothing that d3 has brought to the table.  If your work already involves things like transformation matrices, you'd be in familiar territory and d3 would be a pushover.

One of the first things I needed to do was select a circle and have it "fit to screen" which was essentially zooming and centering the circle.  There were a couple gotchas that I had to hammer through before I figured it out.

  1. Scale the view THEN translate.  It fits my mental model better to make the view the size I need via scale and then to move around it with a translation.  I'm sure you could translate first and then scale if you want.
  2. Translations performed on a scaled shape do not need to be scaled themselves.  So, if I scale a circle with a diameter of 40 by 2 then now the coordinate system has doubled and its diameter is 80.  If I wanted to translate the shape by (10,20) I wouldn't have to scale it first and make it (20, 40).
  3. The top left of your viewport is your origin (0,0) and its positive expansion is down and right.
Here's a quick example I worked up here: https://github.com/nswarr/zoom-center-example.  

First, we need to know how much we need to scale the view.
var scale = height / (radius * 2)
We divide the height of the viewport by the diameter of the circle so we know how many times we need to magnify the circle to make it fit the screen.  In the case of my example, we're scaling based on the height of the viewport since it's shorter than the width.  We can only zoom as far as the shortest dimension so as not to cut off the shape.
scaledCenterX = (width / scale) / 2
scaledCenterY = (height / scale) / 2
When you scale, the viewport is being expanded beyond the visible boundaries downwards and to the right.  What you see in the viewport if you only scaled would be the top left of the newly scaled view.  You need to find out what the X and Y coordinates are  for the center of your smaller "window" since that's where you want to put your zoomed in shape.
x = -(node.x - scaledCenterX)
y = -(node.y - scaledCenterY)

var transform = "scale(" + scale + ")";
transform += " translate(" + x + "," + y + ")";

vis.transition().duration(500).attr("transform", transform);
The rest is simple.  Just subtract the shape's position from the center of your window to get your translation.  Please note again, the scaling comes before the translation.