D3 Force Layout - Text Wrapping and Node Overlaps - javascript

This is my first question to stack overflow so please bear with me if I make some newbie mistakes. I have searched a lot of questions here and have not found exactly what I'm looking for (in one case I have, but do not know how to implement it). And it seems the only people who have asked similar questions have not received any answers.
I've created a force layout with D3 and things are almost working the way I want them to. Two things that I am having trouble editing for:
1) Keep nodes from overlapping: yes, I have read and re-read Mike Bostock's code for clustered force layouts. I do not know how to implement this into my code without something going terribly wrong! I tried this code from a tutorial, but it fixed my nodes in a corner and splayed the links all over the canvas:
var padding = 1, // separation between circles
radius=8;
function collide(alpha) {
var quadtree = d3.geom.quadtree(graph.nodes);
return function(d) {
var rb = 2*radius + padding,
nx1 = d.x - rb,
nx2 = d.x + rb,
ny1 = d.y - rb,
ny2 = d.y + rb;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y);
if (l < rb) {
l = (l - rb) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
You can see the addition to the tick function in my fiddle (linked below) commented out.
2) Wrap my text labels so they fit inside the nodes. Right now they expand to the node's full name over hover but I am going to change that into a tooltip eventually (once I get these kinks worked out I'll figure out a tooltip) - right now I just want the original, short names to wrap inside the nodes. I've looked at this answer and this answer (http://jsfiddle.net/Tmj7g/4/) but when I try to implement this into my own code, it is not responding or ends up clustering all the nodes in the top left corner (??).
Any and all input is GREATLY appreciated, and feel free to edit my fiddle here: https://jsfiddle.net/lilyelle/496c2bmr/
I also know that all of my language is not entirely consistent or the simplest way of writing the D3 code - that's because I've copied and spliced together lots of things from different sources and am still trying to figure out the best way to write this stuff for myself. Any advice in this regard is also appreciated.

1) Collision detection: Here's an updated, working jsFiddle, which was guided by this example from mbostock. Adding collision detection was largely a copy/paste of the important bits. Specifically, in the tick function, I added the code that loops over all those nodes and tweaks their positions if they collide:
var q = d3.geom.quadtree(nodes),
i = 0,
n = nodes.length;
while (++i < n) q.visit(collide(nodes[i]));
Since your jsFiddle didn't have a variable nodes set, I added it right above the last snipped:
var nodes = force.nodes()
Also, that loop requires the function collide to be defined, just as it is in Bostock's example, so I added that to your jsFiddle as well:
function collide(node) {
var r = node.radius + 16,
nx1 = node.x - r,
nx2 = node.x + r,
ny1 = node.y - r,
ny2 = node.y + r;
return function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = node.x - quad.point.x,
y = node.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = node.radius + quad.point.radius;
if (l < r) {
l = (l - r) / l * .5;
node.x -= x *= l;
node.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
};
}
The last necessary bit comes from the fact that detecting node collision requires knowing their size. Bostock's code accesses node.radius inside the collide function above. Your example doesn't set the nodes' radii, so in that collide function node.radius is undefined. One way to set this radius is to add it to your json, e.g.
{radius: 30, "name":"AATF", "full_name":"African Agricultural Technology Foundation", "type":1}
If all your nodes will have the same radius, that's overkill.
Another way is to replace the 2 occurrences of node.radius with a hardcoded number, like 30.
I chose to do something between those two options: assign a constant node.radius to each node by looping over the loaded json:
json.nodes.forEach(function(node) {
node.radius = 30;
})
That's it for getting collision detection working. I used a radius of 30 because that's the radius you used for rendering these nodes, as in .attr("r", 30). That'll keep all the nodes bunched up — not overlapping but still touching each other. You can experiment with a larger value for node.radius to get some white space between them.
2) Text Wrapping: That's a tough one. There's no easy way to make the SVG <text> wrap at some width. Only regular html div/span can do that automatically, but even html elements can't wrap to fit a circle, only to a constant width.
You might be able to come up with some compromise that'll allow you to always fit some text. For example, if your data is all known ahead of time and the size of the circles is always the same fixed value, then you know ahead of time which labels can fit and which don't. The ones that don't, you can either shorten, by adding say a short_name attribute to each node in your JSON, and setting it to something that'll definitely fit. Alternatively, still if the size and labels are known in advance, you can pre-determine how to break up the labels into multiple lines and hard code that into your JSON. Then, when you render, you can render that text using multiple SVG <text> elements that you manually position as multiple lines. Finally, if nothing is known ahead of time, then you might be able to get a good solution by switching to rendering the text as absolutely positioned divs on top of (and outside of) the SVG, with a widths that match the circles' widths, so that the text would automatically wrap.

Related

Check collision within circle bounds

I am trying to fill circle with some line alike sprites.
I generate random position x and y within +- radius, but I olny want to draw sprite when it not intersects with circle bounds like:
Green lines I want to draw, and the red ones - I don't.
I am wondering on some ideas that can help to detect unwanted sprites fast. Is there anything I can use for this purposes?
I am using pixi.js and sprite`s height and width are always the same.
A simple idea could be the following one:
For segment (line?), defined by the two point P1 = (x0,y0) and P2 = (x1, y1).
The circle is defined by center R = (xc, yc) and a radius r.
Now check
distance P1 - C < r, also meaning point is IN the circle
distance P2 - C < r, (same)
If both true, green line ! Depending on the library you used, you'll probably find a method such pointIsInCircle that will do half of the job.
Here is a simple exemple from w3resource
function check_a_point(a, b, x, y, r) {
var dist_points = (a - x) * (a - x) + (b - y) * (b - y);
r *= r;
if (dist_points < r) {
return true;
}
return false;
}

How to cluster bubble chart with many groups

I'm trying to imitate the following effects:
Orginal version V3:
https://bl.ocks.org/mbostock/7881887
Converted to V4:
https://bl.ocks.org/lydiawawa/1fe3c80d35e046c1636663442f34680b/86d1bda1dabb7f3a6d11cb1a16053564078ed964
An example used dataset:
https://jsfiddle.net/hf998do7/1/
This is what I have so far :
https://blockbuilder.org/lydiawawa/0899a02cc86f2274f52e27064bc86500
I want to make a bubble graph that shows clusters of Race, the size of the bubbles are assigned by BMI. The dots will not overlap (collide). There is a toggle control on the left top corner; when it is turned to the right, bubbles cluster into groups separated by Race, when it is turned to the left the dots combine and mix together and centers on the canvas ( in a large circle).
I tried to code for a clustered bubbles, but with the dataset I have there are too many jitters, takes a while for the split to stop. So I found Bostock's versions of clustered bubbles, they are intended to reduce jitters.
What I struggled the most is to fit my dataset with Bostock's version III layout, and I accidentally used the wrong version of forceCluster(alpha) code because I was looking through his different versions (I updated the code in the following section).
In Bostock's original code, he created his own bubbles by defining the following variable, nodes. He used a custom formula to define the distribution of nodes. However, in my case, I will be using my own dataset, bubbleChart.csv with a toggle that combines and splits the cluster.
Bostock's custom defined nodes:
var nodes = d3.range(n).map(function() {
var i = Math.floor(Math.random() * m),
r = Math.sqrt((i + 1) / m * -Math.log(Math.random())) * maxRadius,
d = {
cluster: i,
radius: r,
x: Math.cos(i / m * 2 * Math.PI) * 200 + width / 2 + Math.random(),
y: Math.sin(i / m * 2 * Math.PI) * 200 + height / 2 + Math.random()
};
if (!clusters[i] || (r > clusters[i].radius)) clusters[i] = d;
return d;
});
He then defined force function to reduce jitter:
var force = d3.layout.force()
.nodes(nodes) // not sure how to redefine this
.size([width, height])
.gravity(.02)
.charge(0)
.on("tick", tick)
.start();
Code for clustering (updated):
function forceCluster(alpha) {
return function(d) {
var cluster = clusters[d.Race];
if (cluster === d) return;
var x = d.x - cluster.x,
y = d.y - cluster.y,
l = Math.sqrt(x * x + y * y),
r = d.Race + cluster.Race;
if (l != r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
cluster.x += x;
cluster.y += y;
}
};
}
I'm having trouble to fit everything together in particularly to adapt to my own dataset and would like to have some help. Thank you!

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});

straight line between two points

On a HTML canvas I have multiple points starting from 1 to N, this is basically a connect the dots application and is activated on touchstart.
There is validation so that they can only connect the dots from 1 and go to 2 (.. n). The issue is that right now is there is no validation that the line is a straight line and I am looking for an algorithm to do this, Here is what I have thought so far
For 2 points (x1,y1) to (x2,y2) get all the coordinates by finding the slope and using the formula y = mx + b
on touchmove get the x,y co-oridnates and make sure it is one of the points from the earlier step and draw a line else do not draw the line.
Is there a better way to do this or are there any different approaches that I can take ?
Edit: I originally misunderstood the question, it seems.
As far as validating the path: I think it would be easier just to have a function that determines whether a point is valid than calculating all of the values beforehand. Something like:
function getValidatorForPoints(x1, y1, x2, y2) {
var slope = (y2 - y1) / (x2 - x1);
return function (x, y) {
return (y - y1) == slope * (x - x1);
}
}
Then, given two points, you could do this:
var isValid = getValidatorForPoints(x1, y1, x2, y2);
var x = getX(), y = getY();// getX and getY get the user's new point.
if (isValid(x, y)) {
// Draw
}
This approach also gives you some flexibility—you could always modify the function to be less precise to accommodate people who don't quite draw a straight line but are tolerably close.
Precision:
As mentioned in my comment, you can change the way the function behaves to make it less exacting. I think a good way to do this is as follows:
Right now, we are using the formula (y - y1) == slope * (x - x1). This is the same as (slope * (x - x1)) - (y - y1) == 0. We can change the zero to some positive number to make it accept points "near" the valid line as so:
Math.abs((slope * (x - x1)) - (y - y1)) <= n
Here n changes how close the point has to be to the line in order to count.
I'm pretty sure this works as advertised and helps account for people's drawing the line a little crooked, but somebody should double check my math.
function drawGraphLine(x1, y1, x2, y2, color) {
var dist = Math.ceil(Math.sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2)));
var angle = Math.atan2(y2-y1, x2-x1)*180/Math.PI;
var xshift = dist - Math.abs(x2-x1);
var yshift = Math.abs(y1-y2)/2;
var div = document.createElement('div');
div.style.backgroundColor = color;
div.style.position = 'absolute';
div.style.left = (x1 - xshift/2) + 'px';
div.style.top = (Math.min(y1,y2) + yshift) + 'px';
div.style.width = dist+'px';
div.style.height = '3px';
div.style.WebkitTransform = 'rotate('+angle+'deg)';
div.style.MozTransform = 'rotate('+angle+'deg)';
}
// By Tomer Almog

Categories