Andron
Andron

Reputation: 6621

Infinite zooming in force layout d3.js

There is an example where we can click on a circle and see inner circles.
Also there are different examples of the force layout.

Is it possible to have a force layout and each node of it will/can be a circle with inner force layout?
So it will work as infinite zoom (with additional data loading) for these circles.

Any ideas/examples are welcome.

Upvotes: 1

Views: 1274

Answers (1)

couchand
couchand

Reputation: 2659

I would approach the problem like this: Build a force-directed layout, starting with one of the tutorials (maybe this one since it build something that uses circle packing for initialization). Add D3's zoom behavior.

var force = d3.layout.force()
// force layout settings

var zoom = d3.behavior.zoom()
// etc.

So far, so good. Except that the force layout likes to hang out around [width/2, height/2], but it makes the zooming easier if you center around [0, 0]. Fight with geometric zooming for a little while until you realize that this problem really demands semantic zooming. Implement semantic zooming. Go get a coffee.

Figure out a relationship between the size of your circles and the zoom level that will let you tell when the next level needs to be uncovered. Something like this:

// expand when this percent of the screen is covered
var coverageThreshold = 0.6;
// the circles should be scaled to this size
var maxRadius = 20;
// the size of the visualization
var width = 960;
// which means this is the magic scale factor
var scaleThreshold = maxRadius / (coverageThreshold * width)
// note: the above is probably wrong

Now, implement a spatial data filter. As you're zooming down, you basically want to hide any data points that have zoomed out of view so that you don't waste gpu time computing their representation. Also, figure out an algorithm that will determine which node the user is zooming in on. This very well might use a Voronoi tessalation. Learn way more than you thought you needed to about geometry.

One more math thing we need to work out. We'll have the child nodes take the place of the parent node, so we need to scale their size based on the total size of the parent. This is going to be annoying and require some tweaking to get right, unless you know the right algorithm... I don't.

// size of the parent node
var parentRadius = someNumberPossiblyCalculated;
// area of the parent node
var parentArea = 2 * Math.PI * parentRadius;
// percent of the the parent's area that will be covered by children
//   (here be dragons)
var childrenCoverageRatio = 0.8;
// total area covered by children
var childrenArea = parentArea * childrenCoverageArea;
// the total of the radiuses of the children
var childTotal = parent.children
    .map(radiusFn)
    .reduce(function(a, b) { return a + b; });

// the child radius function
//   use this to generate the child elements with d3
//   (optimize that divide in production!)
var childRadius = function(d) {
    return maxRadius * radiusFn(d) / childTotal;
};
// note: the above is probably wrong

Ok, now we have the pieces in place to make the magic sauce. In the zoom handler, check d3.event.scale against your reference point. If the user has zoomed in past it, perform the following steps very quickly:

  • hide the parent elements that are off-screen
  • remove the parent node that is being zoomed into
  • add the child nodes of that parent to the layout at the x and y location of the parent
  • explicitly run force.tick() a handful of times so the children move apart a bit
  • potentially use the circle-packing layout to make this process cleaner

Ok, so now we have a nice little force layout with zoom. As you zoom in you'll hit some threshold, hopefully computed automatically by the visualization code. When you do, the node you're zooming in on "explodes" into all it's constituent nodes.

Now figure out how to structure your code so that you can "reset" things, to allow you to continue zooming in and have it happen again. That could be recursive, but it might be cleaner to just shrink the scales by a few orders of magnitude and simultaneously expand the SVG elements by the inverse factor.

Now zooming out. First of all, you'll want a distinct zoom threshold for the reverse process, a hysteresis effect in the control which will help prevent a jumpy visualization if someone's riding the mousewheel. You zoom in and it expands, then you have to zoom back just a little bit further before it contracts again.

Okay, when you hit the zoom out threshold you just drop the child elements and add the parent back at the centroid of the children's locations.

var parent.x = d3.mean(parent.children, function(d) { return d.x; });
var parent.y = d3.mean(parent.children, function(d) { return d.y; });

Also, as you're zooming out start showing those nodes that you hid while zooming in.

As @Lars mentioned, this would probably take a little while.

Upvotes: 1

Related Questions