Fix misleading circle sizes calculated by circle packing - javascript

I have created a circle packing layout with a large hierarchical dataset. The image below shows what it looks like. The number in each circle corresponds to the number of leaf nodes.
Each leaf node is given an initial value of 1. The layout then calculates r, x, and y for each node.
let root = d3.stratify()
.id((d) => d.id)
.parentId((d) => d.parent)
(data)
root
.sum(d => d.children ? 0 : 1)
.sort((a, b) => b.height - a.height || b.value - a.value)
pack(root)
The problem here is that the size of the largest circle is incorrect. It only has a value of 1271, hence it should be smaller than the circle with a value of 1364. Therefore, the visualisation is misleading.
Now if I prune the hierarchy and remove all the children of the roots children, the size of each circle is correct. But this will not allow me to zoom in and show the children of a node. As their x, y, and r, have not been calculated.
root
.sum(d => d.children ? 0 : 1)
.sort((a, b) => b.height - a.height || b.value - a.value)
root.children.forEach( child => {
delete child.children
})
pack(root)
I understand that the problem stated above is jsut a limitation of the space filling circle packing algorithm. In fact, this post explains it quite well.
Circle packing can only represent either one generation with a constant areal scale factor or all leaf nodes - not both. Either approach can be accomplished with d3.pack
Circle packing can represent diameters proportionally for either leaves or one generation. Again either approach can be accomplished with d3.pack.
This question was posted a could years ago, so I am curious if it is somehow possible to overcome this problem?
This is what I have tried:
Create hierarchy
Remove children of roots children
Calculate the layout
Add children of roots children back
Recalculate layout for Each child
root
.sum(d => d.children ? 0 : 1)
.sort((a, b) => b.height - a.height || b.value - a.value)
root.children.forEach( child => {
if(child.children) {
child.temp = child.children
delete child.children
}
})
pack(root)
root.children.forEach(child => {
if (child.temp) {
child.children = child.temp
delete child.temp
let pack = d3.pack()
.size([child.r * 2, child.r * 2])
pack(child)
child.descendants().forEach(d => {
//d.x = d.x - child.r // + (child.x - child.r)
//d.y = d.y - child.r //(child.y - child.r)
})
}
})
Which produces this:
So some calculation is missing to position the nodes correctly.
The second iteration of the pack layout recalculates the childrens positions. So if I add this:
root.children.forEach(child => {
if (child.temp) {
child.children = child.temp
delete child.temp
let pack = d3.pack()
.size([child.r * 2, child.r * 2])
let tempX = child.x
let tempY = child.y
pack(child)
child.x = tempX
child.y = tempY
}
})
It produces the image below, where the children are in the correct position.
But if I zoom in on a node, then it's children are not positioned correctly. So if I add the following:
root.children.forEach(child => {
if (child.temp) {
child.children = child.temp
delete child.temp
let pack = d3.pack()
.size([child.r * 2, child.r * 2])
let tempX = child.x
let tempY = child.y
pack(child)
child.x = tempX
child.y = tempY
child.descendants().forEach(d => {
if(d.id == child.id) { return }
d.x = d.x - child.r
d.y = d.y - child.r
})
}
})
I get this when I zoom in on a node:
The position is off, but only by a small bit.
I am guessing you would need to do this type of this recursively in the final version. I am also curious if anyone has tried something like this before?
Is this a reverse circle packing algorithm? Instead of calculating the size of the parent by the combined size of the nested nodes, we need to calculate the size of the nested nodes by using the parents size and then scaling them accordingly.
The visualisation will not let you compare leaf nodes, but rather compare the structure of a hierarchy?

Related

How to zoom via brush in d3 with non-standart axises

Im trying to do the zooming via brush like in this block
https://bl.ocks.org/FrissAnalytics/539a46e17640613ebeb94598aad0c92d
The difference is that I need to define axis values manually due to zooming, cause I need to keep the distance between ticks the same (like scaleOrdinal, but I did it with scaleLinear).
Im stuck with the brushing - it works fine when it zoom via brush first time, but if I want to go deeper, the zoom is lagging - the scale is calculating well, but translation goes at any place, but not at right.
There is my fiddle (it is a bit messy, I called getRange several times for defining boundaries)
https://jsfiddle.net/Celeritas/y2u06kpm/2/
So I have this code now for brush_end event
function brush_endEvent() {
const s = d3.event.selection;
if (!s && lastSelection !== null) {
//this doing the same thing
let scaleX = lastTransform.rescaleX(x);
let scaleY = lastTransform.rescaleY(y);
gxAxis.call(xAxis.scale(scaleX));
gyAxis.call(yAxis.scale(scaleY));
//here I getting the current domain for zoomed area, which will set with the equal distance between ticks
getRange({
x1: scaleX.domain()[0],
x2: scaleX.domain()[scaleX.domain().length - 1],
y1: scaleY.domain()[0],
y2: scaleY.domain()[scaleY.domain().length - 1],
})
let kWidth = Math.ceil(tickTextWidth / (width / t.length)) + 1
let kHeight = Math.ceil(tickTextHeight / (height / b.length))
//im redefining domains manually
scaleX.domain(t)
scaleY.domain(b)
xAxis.tickValues(t.filter((e, i) => i % kWidth === 0)).tickFormat(d3.format('d'))
yAxis.tickValues(b.filter((e, i) => i % kHeight === 0)).tickFormat(d3.format('d'))
xAxis2.tickValues(t.filter((e, i) => i % kWidth === 0)).tickFormat(d3.format('d'))
yAxis2.tickValues(b.filter((e, i) => i % kHeight === 0)).tickFormat(d3.format('d'))
let totalX = Math.abs(lastSelection.x2 - lastSelection.x1);
const originalPoint = [scaleX.invert(lastSelection.x1), scaleY.invert(lastSelection.y1)];
const tt = d3.zoomIdentity.scale(((width * lastTransform.k) / totalX));
// BUT HERE im not doing rescale, cause im already redefine domain earlier
//scaleX = tt.rescaleX(x);
//scaleY = tt.rescaleY(y);
canvasChart
.transition()
.duration(200)
.ease(d3.easeLinear)
.call(zoom_function.transform,
d3.zoomIdentity
.translate(scaleX(originalPoint[0]) * -1, scaleY(originalPoint[1]) * -1)
.scale(tt.k));
lastSelection = null;
} else {
brushSvg.call(brush.move, null);
}
}
So Im in despair, I dont getting, how to set zoom to the right position after brushing.
Thanks for any help!
d3.zoomIdentity
.translate(x(originalPoint[0]) * -1, y(originalPoint[1]) * -1)
.scale(tt.k)
Well, the mistake was in the x-y for zoom. It should be original, non-rescaled domain

How to achieve disc shape in D3 force simulation?

I'm trying to recreate the awesome 'dot flow' visualizations from Bussed out by Nadieh Bremer and Shirely Wu.
I'm especially intrigued by the very circular shape of the 'bubbles' and the fluid-dynamics-like compression in the spot where the dots arrive to the bubble (black arrow).
My take was to create (three) fixed nodes by .fx and .fy (black dots) and link all the other nodes to a respective fixed node. The result looks quite disheveled and the bubbles even don't form around their center nodes, when I lower the forces so the animation runs a little slower.
const simulation = d3.forceSimulation(nodes)
.force("collide", d3.forceCollide((n, i) => i < 3 ? 0 : 7))
.force("links", d3.forceLink(links).strength(.06))
Any ideas on force setup which would yield more aesthetically pleasing results?
I do understand that I'll have to animate the group assignment over time to get the 'trickle' effect (otherwise all the points would just swarm to their destination), but i'd like to start with a nice and round steady state of the simulation.
EDIT
I did check the source code, and it's just replaying pre-recorded simulation data, I guess for performance reasons.
Building off of Gerardo's start,
I think that one of the key points, to avoid excessive entropy is to specify a velocity decay - this will help avoid overshooting the desired location. Too slow, you won't get an increase in density where the flow stops, too fast, and you have the nodes either get too jumbled or overshoot their destination, oscillating between too far and too short.
A many body force is useful here - it can keep the nodes spaced (rather than a collision force), with the repulsion between nodes being offset by positioning forces for each cluster. Below I have used two centering points and a node property to determine which one is used. These forces have to be fairly weak - strong forces lead to over correction quite easily.
Rather than using a timer, I'm using the simulation.find() functionality each tick to select one node from one cluster and switch which center it is attracted to. After 1000 ticks the simulation below will stop:
var canvas = d3.select("canvas");
var width = +canvas.attr("width");
var height = +canvas.attr("height");
var context = canvas.node().getContext('2d');
// Key variables:
var nodes = [];
var strength = -0.25; // default repulsion
var centeringStrength = 0.01; // power of centering force for two clusters
var velocityDecay = 0.15; // velocity decay: higher value, less overshooting
var outerRadius = 250; // new nodes within this radius
var innerRadius = 100; // new nodes outside this radius, initial nodes within.
var startCenter = [250,250]; // new nodes/initial nodes center point
var endCenter = [710,250]; // destination center
var n = 200; // number of initial nodes
var cycles = 1000; // number of ticks before stopping.
// Create a random node:
var random = function() {
var angle = Math.random() * Math.PI * 2;
var distance = Math.random() * (outerRadius - innerRadius) + innerRadius;
var x = Math.cos(angle) * distance + startCenter[0];
var y = Math.sin(angle) * distance + startCenter[1];
return {
x: x,
y: y,
strength: strength,
migrated: false
}
}
// Initial nodes:
for(var i = 0; i < n; i++) {
nodes.push(random());
}
var simulation = d3.forceSimulation()
.force("charge", d3.forceManyBody().strength(function(d) { return d.strength; } ))
.force("x1",d3.forceX().x(function(d) { return d.migrated ? endCenter[0] : startCenter[0] }).strength(centeringStrength))
.force("y1",d3.forceY().y(function(d) { return d.migrated ? endCenter[1] : startCenter[1] }).strength(centeringStrength))
.alphaDecay(0)
.velocityDecay(velocityDecay)
.nodes(nodes)
.on("tick", ticked);
var tick = 0;
function ticked() {
tick++;
if(tick > cycles) this.stop();
nodes.push(random()); // create a node
this.nodes(nodes); // update the nodes.
var migrating = this.find((Math.random() - 0.5) * 50 + startCenter[0], (Math.random() - 0.5) * 50 + startCenter[1], 10);
if(migrating) migrating.migrated = true;
context.clearRect(0,0,width,height);
nodes.forEach(function(d) {
context.beginPath();
context.fillStyle = d.migrated ? "steelblue" : "orange";
context.arc(d.x,d.y,3,0,Math.PI*2);
context.fill();
})
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<canvas width="960" height="500"></canvas>
Here's a block view (snippet would be better full page, the parameters are meant for it). The initial nodes are formed in the same ring as later nodes (so there is a bit of jostle at the get go, but this is an easy fix). On each tick, one node is created and one attempt is made to migrate a node near the middle to other side - this way a stream is created (as opposed to any random node).
For fluids, unlinked nodes are probably best (I've been using it for wind simulation) - linked nodes are ideal for structured materials like nets or cloth. And, like Gerardo, I'm also a fan of Nadieh's work, but will have to keep an eye on Shirley's work as well in the future.
Nadieh Bremer is my idol in D3 visualisations, she's an absolute star! (correction after OP's comment: it seems that this datavis was created by Shirley Wu... anyway, that doesn't change what I said about Bremer).
The first attempt to find out what's happening on that page is having a look at the source code, which, unfortunately, is an herculean job. So, the option that remains is trying to reproduce that.
The challenge here is not creating a circular pattern, that's quite easy: you only need to combine forceX, forceY and forceCollide:
const svg = d3.select("svg")
const data = d3.range(500).map(() => ({}));
const simulation = d3.forceSimulation(data)
.force("x", d3.forceX(200))
.force("y", d3.forceY(120))
.force("collide", d3.forceCollide(4))
.stop();
for (let i = 300; i--;) simulation.tick();
const circles = svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("r", 2)
.style("fill", "tomato")
.attr("cx", d => d.x)
.attr("cy", d => d.y);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="400" height="300"></svg>
The real challenge here is moving those circles to a given simulation one by one, not all at the same time, as I did here.
So, this is my suggestion/attempt:
We create a simulation, that we stop...
simulation.stop();
Then, in a timer...
const timer = d3.interval(function() {etc...
... we add the nodes to the simulation:
const newData = data.slice(0, index++)
simulation.nodes(newData);
This is the result, click the button:
const radius = 2;
let index = 0;
const limit = 500;
const svg = d3.select("svg")
const data = d3.range(500).map(() => ({
x: 80 + Math.random() * 40,
y: 80 + Math.random() * 40
}));
let circles = svg.selectAll(null)
.data(data);
circles = circles.enter()
.append("circle")
.attr("r", radius)
.style("fill", "tomato")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.style("opacity", 0)
.merge(circles);
const simulation = d3.forceSimulation()
.force("x", d3.forceX(500))
.force("y", d3.forceY(100))
.force("collide", d3.forceCollide(radius * 2))
.stop();
function ticked() {
circles.attr("cx", d => d.x)
.attr("cy", d => d.y);
}
d3.select("button").on("click", function() {
simulation.on("tick", ticked).restart();
const timer = d3.interval(function() {
if (index > limit) timer.stop();
circles.filter((_, i) => i === index).style("opacity", 1)
const newData = data.slice(0, index++)
simulation.alpha(0.25).nodes(newData);
}, 5)
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<button>Click</button>
<svg width="600" height="200"></svg>
Problems with this approach
As you can see, there is too much entropy here, particularly at the centre. Nadieh Bremer/Shirley Wu probably used a way more sofisticated code. But these are my two cents for now, let's see if other answers will show up with different approaches.
With the help of other answers here I went on experimenting, and I'd like to summarize my findings:
Disc shape
forceManyBody seems to be more stable than forceCollide. The key for using it without distorting the disc shapes is .distanceMax. With the downside that your visualization is not 'scale-free' any more and it has to be tuned by hand. As a guidance, overshooting in each direction causes distinct artifacts:
Setting distanceMax too high deforms the neighboring discs.
Setting distanceMax too low (lower than expected disc diameter):
This artifact can be seen in the Guardian visualization (when the red and blue dots form a huge disc in the end), so I'm quite sure distanceMax was used.
Node positioning
I still find using forceX with forceY and custom accessor functions too cumbersome for more complex animations. I decided to go with 'control' nodes, and with little tuning (chargeForce.strength(-4), link.strength(.2).distance(1)) it works ok.
Fluid feeling
While experimenting with the settings I noticed that the fluid feeling (incoming nodes push boundary of accepting disc) depends especially on simulation.velocityDecay, but lowering it too much adds too much entropy to the system.
Final result
My sample code splits one 'population' into three, and then into five - check it on blocks. Each of the sinks is represented by a control node. The nodes are re-assigned to new sinks in batches, which gives more control over the visual of the 'stream'. Starting to pick nodes to assign closer to the sinks looks more natural (single sort at the beginning of each animation).

Display all the leafs on the same level with D3.JS

I recently started to use D3.js in order to visualize a tree in the form of a radial tree, as presented in Colapsible Radial Tree , but I encountered some problems while modifying the code.
I used the code in 1 to display all the leafs on the same 'level'. My methodology is the following:
First, compute the maximum depth of the tree to know where to put all the leafs of the tree, by using the following code:
var maxDepth = getDepth(source);
function getDepth(obj) {
var depth = 0;
if (obj.children) {
obj.children.forEach(function (d) {
var tmpDepth = getDepth(d)
if (tmpDepth > depth) {
depth = tmpDepth
}
})
}
return 1 + depth}
Then, loop over all the nodes of the tree and whenever a node without children is encountered (= leaf), the position is set to:
nodes.forEach(function(d) {
if (d.children)
{
d.y = d.depth * 60;
}else
d.y = maxDepth * 60;})
It seems to work correctly, except that the positions of some nodes are not correct and are overlapping some times.
How can I adjust the code to spread the nodes on a more efficient way ? The full code is available here.
Use d3's cluster layout, it's designed to place leaves at the same level
https://github.com/mbostock/d3/wiki/Cluster-Layout
https://bl.ocks.org/mbostock/4063570
var tree = d3.layout.cluster()
.size([360, diameter / 2 - 80])
.separation(function(a, b) { return (a.parent == b.parent ? 1 : 5) / a.depth; });
...
// Normalize for fixed-depth.
//nodes.forEach(function(d) { d.y = d.depth * 80; }); // get rid of this line (or amend it) as it resets the leaves to one level below the parent

d3.js spreading labels for pie charts

I'm using d3.js - I have a pie chart here. The problem though is when the slices are small - the labels overlap. What is the best way of spreading out the labels.
http://jsfiddle.net/BxLHd/16/
Here is the code for the labels. I am curious - is it possible to mock a 3d pie chart with d3?
//draw labels
valueLabels = label_group.selectAll("text.value").data(filteredData)
valueLabels.enter().append("svg:text")
.attr("class", "value")
.attr("transform", function(d) {
return "translate(" + Math.cos(((d.startAngle+d.endAngle - Math.PI)/2)) * (that.r + that.textOffset) + "," + Math.sin((d.startAngle+d.endAngle - Math.PI)/2) * (that.r + that.textOffset) + ")";
})
.attr("dy", function(d){
if ((d.startAngle+d.endAngle)/2 > Math.PI/2 && (d.startAngle+d.endAngle)/2 < Math.PI*1.5 ) {
return 5;
} else {
return -7;
}
})
.attr("text-anchor", function(d){
if ( (d.startAngle+d.endAngle)/2 < Math.PI ){
return "beginning";
} else {
return "end";
}
}).text(function(d){
//if value is greater than threshold show percentage
if(d.value > threshold){
var percentage = (d.value/that.totalOctets)*100;
return percentage.toFixed(2)+"%";
}
});
valueLabels.transition().duration(this.tweenDuration).attrTween("transform", this.textTween);
valueLabels.exit().remove();
As #The Old County discovered, the previous answer I posted fails in firefox because it relies on the SVG method .getIntersectionList() to find conflicts, and that method hasn't been implemented yet in Firefox.
That just means we have to keep track of label positions and test for conflicts ourselves. With d3, the most efficient way to check for layout conflicts involves using a quadtree data structure to store positions, that way you don't have to check every label for overlap, just those in a similar area of the visualization.
The second part of the code from the previous answer gets replaced with:
/* check whether the default position
overlaps any other labels*/
var conflicts = [];
labelLayout.visit(function(node, x1, y1, x2, y2){
//recurse down the tree, adding any overlapping labels
//to the conflicts array
//node is the node in the quadtree,
//node.point is the value that we added to the tree
//x1,y1,x2,y2 are the bounds of the rectangle that
//this node covers
if ( (x1 > d.r + maxLabelWidth/2)
//left edge of node is to the right of right edge of label
||(x2 < d.l - maxLabelWidth/2)
//right edge of node is to the left of left edge of label
||(y1 > d.b + maxLabelHeight/2)
//top (minY) edge of node is greater than the bottom of label
||(y2 < d.t - maxLabelHeight/2 ) )
//bottom (maxY) edge of node is less than the top of label
return true; //don't bother visiting children or checking this node
var p = node.point;
var v = false, h = false;
if ( p ) { //p is defined, i.e., there is a value stored in this node
h = ( ((p.l > d.l) && (p.l <= d.r))
|| ((p.r > d.l) && (p.r <= d.r))
|| ((p.l < d.l)&&(p.r >=d.r) ) ); //horizontal conflict
v = ( ((p.t > d.t) && (p.t <= d.b))
|| ((p.b > d.t) && (p.b <= d.b))
|| ((p.t < d.t)&&(p.b >=d.b) ) ); //vertical conflict
if (h&&v)
conflicts.push(p); //add to conflict list
}
});
if (conflicts.length) {
console.log(d, " conflicts with ", conflicts);
var rightEdge = d3.max(conflicts, function(d2) {
return d2.r;
});
d.l = rightEdge;
d.x = d.l + bbox.width / 2 + 5;
d.r = d.l + bbox.width + 10;
}
else console.log("no conflicts for ", d);
/* add this label to the quadtree, so it will show up as a conflict
for future labels. */
labelLayout.add( d );
var maxLabelWidth = Math.max(maxLabelWidth, bbox.width+10);
var maxLabelHeight = Math.max(maxLabelHeight, bbox.height+10);
Note that I've changed the parameter names for the edges of the label to l/r/b/t (left/right/bottom/top) to keep everything logical in my mind.
Live fiddle here: http://jsfiddle.net/Qh9X5/1249/
An added benefit of doing it this way is that you can check for conflicts based on the final position of the labels, before actually setting the position. Which means that you can use transitions for moving the labels into position after figuring out the positions for all the labels.
Should be possible to do. How exactly you want to do it will depend on what you want to do with spacing out the labels. There is not, however, a built in way of doing this.
The main problem with the labels is that, in your example, they rely on the same data for positioning that you are using for the slices of your pie chart. If you want them to space out more like excel does (i.e. give them room), you'll have to get creative. The information you have is their starting position, their height, and their width.
A really fun (my definition of fun) way to go about solving this would be to create a stochastic solver for an optimal arrangement of labels. You could do this with an energy-based method. Define an energy function where energy increases based on two criteria: distance from start point and overlap with nearby labels. You can do simple gradient descent based on that energy criteria to find a locally optimal solution with regards to your total energy, which would result in your labels being as close as possible to their original points without a significant amount of overlap, and without pushing more points away from their original points.
How much overlap is tolerable would depend on the energy function you specify, which should be tunable to give a good looking distribution of points. Similarly, how much you're willing to budge on point closeness would depend on the shape of your energy increase function for distance from the original point. (A linear energy increase will result in closer points, but greater outliers. A quadratic or a cubic will have greater average distance, but smaller outliers.)
There might also be an analytical way of solving for the minima, but that would be harder. You could probably develop a heuristic for positioning things, which is probably what excel does, but that would be less fun.
One way to check for conflicts is to use the <svg> element's getIntersectionList() method. That method requires you to pass in an SVGRect object (which is different from a <rect> element!), such as the object returned by a graphical element's .getBBox() method.
With those two methods, you can figure out where a label is within the screen and if it overlaps anything. However, one complication is that the rectangle coordinates passed to getIntersectionList are interpretted within the root SVG's coordinates, while the coordinates returned by getBBox are in the local coordinate system. So you also need the method getCTM() (get cumulative transformation matrix) to convert between the two.
I started with the example from Lars Khottof that #TheOldCounty had posted in a comment, as it already included lines between the arc segments and the labels. I did a little re-organization to put the labels, lines and arc segments in separate <g> elements. That avoids strange overlaps (arcs drawn on top of pointer lines) on update, and it also makes it easy to define which elements we're worried about overlapping -- other labels only, not the pointer lines or arcs -- by passing the parent <g> element as the second parameter to getIntersectionList.
The labels are positioned one at a time using an each function, and they have to be actually positioned (i.e., the attribute set to its final value, no transitions) at the time the position is calculated, so that they are in place when getIntersectionList is called for the next label's default position.
The decision of where to move a label if it overlaps a previous label is a complex one, as #ckersch's answer outlines. I keep it simple and just move it to the right of all the overlapped elements. This could cause a problem at the top of the pie, where labels from the last segments could be moved so that they overlap labels from the first segments, but that's unlikely if the pie chart is sorted by segment size.
Here's the key code:
labels.text(function (d) {
// Set the text *first*, so we can query the size
// of the label with .getBBox()
return d.value;
})
.each(function (d, i) {
// Move all calculations into the each function.
// Position values are stored in the data object
// so can be accessed later when drawing the line
/* calculate the position of the center marker */
var a = (d.startAngle + d.endAngle) / 2 ;
//trig functions adjusted to use the angle relative
//to the "12 o'clock" vector:
d.cx = Math.sin(a) * (that.radius - 75);
d.cy = -Math.cos(a) * (that.radius - 75);
/* calculate the default position for the label,
so that the middle of the label is centered in the arc*/
var bbox = this.getBBox();
//bbox.width and bbox.height will
//describe the size of the label text
var labelRadius = that.radius - 20;
d.x = Math.sin(a) * (labelRadius);
d.sx = d.x - bbox.width / 2 - 2;
d.ox = d.x + bbox.width / 2 + 2;
d.y = -Math.cos(a) * (that.radius - 20);
d.sy = d.oy = d.y + 5;
/* check whether the default position
overlaps any other labels*/
//adjust the bbox according to the default position
//AND the transform in effect
var matrix = this.getCTM();
bbox.x = d.x + matrix.e;
bbox.y = d.y + matrix.f;
var conflicts = this.ownerSVGElement
.getIntersectionList(bbox, this.parentNode);
/* clear conflicts */
if (conflicts.length) {
console.log("Conflict for ", d.data, conflicts);
var maxX = d3.max(conflicts, function(node) {
var bb = node.getBBox();
return bb.x + bb.width;
})
d.x = maxX + 13;
d.sx = d.x - bbox.width / 2 - 2;
d.ox = d.x + bbox.width / 2 + 2;
}
/* position this label, so it will show up as a conflict
for future labels. (Unfortunately, you can't use transitions.) */
d3.select(this)
.attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y;
});
});
And here's the working fiddle: http://jsfiddle.net/Qh9X5/1237/

D3 tree vertical separation

I am using the D3 tree layout, such as this one: http://mbostock.github.com/d3/talk/20111018/tree.html
I have modified it for my needs and am running into an issue. The example has the same issue too where if you have too many nodes open then they become compact and makes reading and interacting difficult. I am wanting to defined a minimum vertical space between nodes while re-sizing stage to allow for such spacing.
I tried modifying the separation algorithm to make it work:
.separation(function (a, b) {
return (a.parent == b.parent ? 1 : 2) / a.depth;
})
That didn't work. I also tried calculating which depth had the most children then telling the height of the stage to be children * spaceBetweenNodes. That got me closer, but still was not accurate.
depthCounts = [];
nodes.forEach(function(d, i) {
d.y = d.depth * 180;
if(!depthCounts[d.depth])
depthCounts[d.depth] = 0;
if(d.children)
{
depthCounts[d.depth] += d.children.length;
}
});
tree_resize(largest_depth(depthCounts) * spaceBetweenNodes);
I also tried to change the node's x value too in the method below where it calculates the y separation, but no cigar. I would post that change too but I removed it from my code.
nodes.forEach(function(d, i) {
d.y = d.depth * 180;
});
If you can suggest a way or know a way that I can accomplish a minimum spacing vertically between nodes please post. I will be very grateful. I am probably missing something very simple.
As of 2016, I was able to achieve this using just
tree.nodeSize([height, width])
https://github.com/mbostock/d3/wiki/Tree-Layout#nodeSize
The API Reference is a bit poor, but is works pretty straight forward. Be sure to use it after tree.size([height, width]) or else you will be overriding your values again.
For more reference: D3 Tree Layout Separation Between Nodes using NodeSize
I was able to figure this out with help from a user on Google Groups. I was not able to find the post. The solution requires you to modify D3.js in one spot, which is not recommended but it was the only to get around this issue that I could find.
Starting around line 5724 or this method: d3_layout_treeVisitAfter
change:
d3_layout_treeVisitAfter(root, function(node) {
node.x = (node.x - x0) / (x1 - x0) * size[0];
node.y = node.depth / y1 * size[1];
delete node._tree;
});
to:
d3_layout_treeVisitAfter(root, function(node) {
// make sure size is null, we will make it null when we create the tree
if(size === undefined || size == null)
{
node.x = (node.x - x0) * elementsize[0];
node.y = node.depth * elementsize[1];
}
else
{
node.x = (node.x - x0) / (x1 - x0) * size[0];
node.y = node.depth / y1 * size[1];
}
delete node._tree;
});
Below add a new variable called: elementsize and default it to [ 1, 1 ] to line 5731
var hierarchy = d3.layout.hierarchy().sort(null).value(null)
, separation = d3_layout_treeSeparation
, elementsize = [ 1, 1 ] // Right here
, size = [ 1, 1 ];
Below that there is a method called tree.size = function(x). Add the following below that definition:
tree.elementsize = function(x) {
if (!arguments.length) return elementsize;
elementsize = x;
return tree;
};
Finally when you create the tree you can change the elementsize like so
var tree = d3.layout.tree()
.size(null)
.elementsize(50, 240);
I know I'm not supposed to respond to other answers, but I don't have enough reputation to add a comment.
Anyway, I just wanted to update this for people using the latest d3.v3.js file. (I assume this is because of a new version, because the line references in the accepted answer were wrong for me.)
The d3.layout.tree function that you are editing is found between lines 6236 and 6345. d3_layout_treeVisitAfter starts on line 6318. The hierarchy variable is declared on line 6237. The bit about tree.elementsize still stands - I put it on line 6343.
Lastly (I assume this was an error): when you create the tree, put the dimensions inside square brackets, like you normally do with "size". So:
var tree = d3.layout.tree()
.size(null)
.elementsize([50, 240]);
The original fix you proposed will work, you just have to make sure you do it after you add everything to the canvas. d3 recalculates the layout each time you enter, exit, append, etc. Once you've done all that, then you can fiddle with the d.y to fix the depth.
nodes.forEach(function(d) { d.y = d.depth * fixdepth});

Categories