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:  

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.


Ayesha said...

I'm trying to implement your example with a geo AlbersUsa projection using D3.

My states are still not being centered properly. The issues is that I scale the entire map not just one state. and then I try to center the selected state in the middle. Any ideas on how I can achieve this?

Nick Swarr said...

Hi, Ayesha. I'm assuming you've already seen Mike Bostock's example which is doing what you're looking for. The only difference here is that it doesn't fully scale the state so it occupies as much space as possible on the screen.

I tried to apply my method and it was (unfortunately) flawed. I tweaked it a bit and applied it to Mike's above example. I just uploaded my version of it.

It seems to work fine but there are one or two states where the scale is just off by a bit (for example, Kentucky). I don't have a ton of time at the moment so I decided I'd just share what I came up with. I hope that helps!

Nick Swarr said...

Oh yeah, I was drawing dots on the screen as reference points. Feel free to remove them!