I've been working on this problem for about a week, and just discovered this github issue / feature request,
https://github.com/mbostock/d3/issues/1992, yesterday.
As I mentioned on that ticket, I've got it mostly working, see this demo:
http://klortho.github.io/d3-flextree/. Nodes can vary in size in both x and y: I'm adding the ability to set nodeSize as a function that returns [x,y].
I also mentioned on that ticket a design problem with regards to reconciling this with the separation() function. In a nutshell, the problem is this: separation return values are in "node size units". So:
d3.layout.tree().separation(function() { return 1; }).nodeSize([10, 10]);
Gives you a tree in which nodes are properly spaced 10 units apart. This is because the tree is layed out first, using "node size units", and then scaled at the end. But if the node size is variable, then what are "node size units"? I.e., how should the layout algorithm work for the following?
d3.layout.tree().separation(function() { return 1; }).nodeSize(function() { return [10, 10]; })
The layout algorithm can't know that the node size is a constant -- can't assume anything about the values for any given node. It would be more convenient if we could specify them in the same, drawing units:
d3.layout.tree().separation(function() { return 10; }).nodeSize(function() { return [10, 10]; })
But that would break backwards compatibility.
Here's the solution that I've come up with. I want to throw it out, to get feedback. I decided to send it to this list instead of putting it on the GH ticket, in case the D3 authors don't appreciate me taking over that ticket.
The key point of the design is to use the size of the root node to define "node size units". Then, keep separation() working the same, for backwards compatibility, but add a new function, margin(), that is probably what people will want to use when nodes have a variable x-size.
Design constraints:
* Library needs to be backwards-compatible
* tree().nodeSize(function {return constant;}) should work exactly the same as tree.nodeSize(constant).
* Don't change the meaning/scale of the value returned by separation().
Design:
* Right now, whether or not size or nodeSize is used, the algorithm computes coordinates first in "node size units". Then, they are scaled at the end.
* Keep that policy, but now, a "node size unit" is the size of the root node.
* To do that, as part of wrapTree(), set x_size and y_size on every wrapper, regardless of the sizing scheme, in "node size units".
* In apportion(), keep the condition that every node is stepped if !variableNodeSize, even though maybe we don't need it, just to avoid potential pitfalls with floating point values not exactly matching
* The default separation function should:
* If !variableNodeSize, do exactly what it does now
* else access the wrapper nodes, and return 1/2 the sum of the two y_sizes, plus, if a.parent != b.parent, the node size y.
* That means it will need to be a closure -- its exact behavior depends on the value set on the root node of the tree. Is it okay to make the default function a closure?
* Add a margin(a, b, g) function for convenience, that allows the user to set the actual separation in the drawing units -- same units that nodeSize uses.
* When it is set, separation() is not used (they are mutually exclusive)
* During the algorithm's execution, margin() is used in place of separation, but it is rescaled to nominal "node size units"; so, instead of separation, use the value (a.x_size + b.x_size) / 2 + margin / root.y_size.
Any feedback is very welcome!
Thanks,
Chris