Smooth transitioning between tree, cluster, radial tree, and radial cluster layouts - d3.js

For a project, I need to interactively change hierarchical data layout of a visualization - without any change of the underlying data whatsoever. The layouts capable of switching between themselves should be tree, cluster, radial tree, and radial cluster. And transitioning should be preferably an animation.
I thought that would be relatively easy task with D3. I started, but I got lost in translations and rotations, data bindings, and similar, so I am asking you for help. Also, probably I am doing something not in the spirit of D3, which is bad since I am seeking a clean solution.
I put together a jsfidle, but it is just a starting point, with added radio buttons, convenient small data set, and initial cluster layout - just to help anybody who wants to take a look at this. Thanks in advance!
UPDATE:
I wanted to focus on links only, so I temporary disabled other elements. Building on #AmeliaBR method, following animations are obtained:
Here is updated jsfiddle.
UPDATE 2:
Now with circles: (excuse my choice of colors)
{doom-duba-doom}
Here is one more updated jsfiddle.

I don't see why it would be that hard so long as all your layouts have the same overall structure of link-paths, circle nodes and text labels.
Just make sure all your objects, including your link paths, have a good data-key that is independent of the data attributes created by the layout functions. Then for each transition, update the data with the results of the appropriate layout function and draw that layout.
I've got the transition to radial tree implemented here: http://jsfiddle.net/YV2XX/5/
Key code:
//Radial Tree layout//
var diameter = 500;
var radialTree = d3.layout.tree()
.size([360, diameter / 2 ])
.separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; });
var radialDiagonal = d3.svg.diagonal.radial()
.projection(function(d) { return [d.y, d.x / 180 * Math.PI]; });
function transitionToRadialTree() {
var nodes = radialTree.nodes(root), //recalculate layout
links = radialTree.links(nodes);
svg.transition().duration(1500)
.attr("transform", "translate(" + (diameter/2)
+ "," + (diameter/2) + ")");
//set appropriate translation (origin in middle of svg)
link.data(links, function(d){
return d.source.name + d.target.name;})
.transition().duration(1500)
.attr("d", radialDiagonal); //get the new radial path
node.data(nodes, function(d){
return d.name ;})
.transition().duration(1500)
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; })
node.select("circle")
.transition().duration(1500)
.attr("r", 4.5);
node.select("text")
.transition().duration(1500)
.attr("dy", ".31em")
.attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
.attr("transform", function(d) { return d.x < 180 ? "translate(8)" : "rotate(180)translate(-8)"; });
};
The layout code is all from http://bl.ocks.org/mbostock/4063550, I've just changed it to be an update instead of an initialization.
Also note that I have moved the variable declaration for root outside of the data-reading method, so it can be re-accessed by the transition functions.
Layout still needs some finessing, but you get the idea.
Now, if you wanted one of the transitions to be a partition, treemap or other layout that doesn't use the node-link structure, they it gets more complicated...

I don't have enough reputation to make a comment...so, I am just giving this tiny contribution as a pseudo-answer. After looking at this post, and based on #VividD's perfect comment on how simple the transitions turned out to be, I simply added the Tree Vertical option to the transformations in this fiddle.
The addition is simply this:
var diagonalVertical = d3.svg.diagonal()
.projection(function (d) {
return [d.x, d.y];
});
Anyways, I have bookmarked this highly instructional interaction.

Related

Avoiding Tangles in Long Networks, D3 Force Diagram

I am using the D3 Force Diagram and trying to find a way to make a network more "linear" and avoid "tangles" when the network generates, especially on long networks.
Example of Tangles
No Tangles, Better but not Ideal
Ideal result, network in a left to right line
Solutions that won't work:
increasing charge
reducing gravity
The example file I built on VizHub is simple, and although increasing charge may seem to work for small networks, 20-50 items will get tangled even with a very strong charge.
I have attempted to seed the location of the nodes starting locations based on the known "rough" rank order of the network. However, this gets disregarded and overwritten immediately by the on(tick) action. I may likely be doing something wrong and may need to do the seeding at a different point in the code but I was not able to find good examples or documentation on this.
I also attempted anchoring some points (less than ideal) but the links disregarded the seeded nodes locations. Again, quite possible that I didn't do it right and that may be my problem, but I was unable to find good examples of anchoring as well.
node.attr("transform", function (d) {
return "translate(" + d.x_seed + "," + d.y_seed + ")";})
Seeding when on("tick",...) disabled
I am constrained to using D3 version 3.5.17 at the latest so I cannot use some of the more modern force tools (like forceY) that would help "flatten" the network.
Any ideas on a good way to get the "ideal" result? Seeding, anchoring, something else?
I've partially answered my question. These have been integrated into the VizHub example now.
How to seed the locations on load.
for (var i = 0; i < nodesData.length; i++){
var t = nodesData[i];
var x_seed = t.rank*width/totalNodes
t.x_seed = x_seed;
t.x = x_seed;
var y_seed = var y_seed = height/2 + Math.random()*height/20 - height/40; // randomize y starting location a little to prevent nodes getting stuck in line
t.y_seed = y_seed;
t.y = y_seed;
}
Anchoring the first, last, and middle nodes:
force.on("tick", function () {
link
.attr("x1", function (d) {
return d.source.x;})
.attr("y1", function (d) {
return d.source.y;})
.attr("x2", function (d) {
return d.target.x;})
.attr("y2", function (d) {
return d.target.y;});
node
.attr("transform", function (d) {
if(d.rank == totalNodes || d.rank == 1 || d.rank == Math.floor(totalNodes/2)){
d.x = d.x_seed
d.y = d.y_seed
}
return "translate(" + d.x + "," + d.y + ")";
});
});

D3 Transition Path Left And Shift Up/Down Simultaneously

Following the tutorial of Mike Bostock on Path transitions here, I am trying to create an interpolated line chart that not only shifts through time but also transitions the y-scale / y-axis, such that is always fits to the lower and upper bounds of the data.
Some background information: The line is clipped by a clipPath and is shifted to the left whenever a new data point is added. Each new data point is added by the tick function, which also transitions the path to slide to the left.
Now the problem is, when I update the y-axis domain, it jumps to the new position. However, I would like it to smoothly transition up or down, similar to how it shifts along the x-axis. The solution probably lies in transforming the scaling of the path based on the new maximum of the data. Is there any way to achieve this or would it require a different approach by building a custom d3.interpolator() for interpolating the path?
function tick() {
// Push a new data point onto the back.
data.push(random());
// Redraw the line.
d3.select(this)
.attr("d", line)
.attr("transform", null);
// Slide it to the left.
d3.active(this)
.attr("transform", "translate(" + x(0) + ",0)")
.transition()
.on("start", tick);
// Pop the old data point off the front.
data.shift();
let max = d3.max(data, (d) => {
return d;
});
y = d3.scaleLinear()
.domain([-1, max])
.range([height, 0]);
d3.select('g .axis.axis--y').transition().duration(500).call(d3.axisLeft(y))
d3.select('g .axis.axis--x').transition().duration(500)
.attr("transform", "translate(0," + y(0) + ")")
}
I have created a jsfiddle which demonstrates the problem here.
Found a solution by using the external d3-interpolate-path library from here. Instead of transitioning the path using a transform, I interpolate the path with the old data and the path with the new data using d3.interpolatePath. Where previous is the old path and current is the new path with the newly added data point.
d3.select(this)
.attr("d", lineOld)
.attr("transform", null)
.transition().duration(500).ease(d3.easeLinear).attrTween('d', (d) => {
let previous = d3.select(this).attr('d');
let current = line(d);
return d3.interpolatePath(previous, current)
}).on("end", tick);
The Jsfiddle with my solution can be found here

Voronoi Tessellation] Find out which point I'm referencing

I've got a project using this Voronoi Tessellation plugin with series of coordinates representing locations of temperature sensors - I'm thinking of using JSON to represent their locations and detected temperature values.
What I need to is to display the temperature value of the sensor(point) I'm referencing to(the area where I'm mouse-hovering) when mouse-hovering an area.
https://github.com/mbostock/d3/wiki/Voronoi-Geom
I've been reading this documentation again and again but still can't figure out whether detecting which specific point that the mouse-hovering area belongs to is possible.
Has anyone tried this before? Are there good examples about it?
If I'm understanding your question, you want to display text at the vertices point when a user mouses into the voronoi partition?
You could do this by handling the mouseenter/leave events of each path:
path.enter().append("path")
.attr("class", function(d, i) {
return "q" + (i % 9) + "-9";
})
.attr("d", polygon)
.on("mouseenter", function(d,i){
if (!someTexts[i]) { // get some fake value
someTexts[i] = (Math.random()*100).toFixed(1);
}
// append text
currentText = svg.append("text")
.text(someTexts[i])
.attr("transform","translate(" + vertices[i] + ")")
.attr("text-anchor","middle")
.attr("alignment-baseline", "middle");
})
.on("mouseleave", function(d,i){
// remove text
currentText.remove();
});
Example here.

How to limit the text of polygons in Voronoi diagram with D3.js?

I've see the Example of D3.js-Voronoi Tessellation.But I want to put some text in each of polygons instead of a circle,Here is my js code:
var width = 600, height = 400;
var vertices = d3.range(20).map(function(d){
return [Math.random() * width, Math.random() * height]
});
var voronoi = d3.geom.voronoi();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
path = svg.append("g").selectAll("path");
svg.selectAll("info")
.data(vertices.slice(1))
.enter().append("text")
.attr("transform", function(d) {
return "translate(" + d + ")";
})
.text("someText")
.attr("shape-rendering","crispEdges")
.style("text-anchor","middle");
redraw();
function redraw(){
path = path
.data(voronoi(vertices), polygon);
path.exit().remove();
path.enter().append("path")
.attr("class", function(d, i) {return "q" + (i % 9) + "-9";})
.attr("d", polygon);
path.order();
}
function polygon(d){
return "M" + d.join("L") + "Z";
}
I have a JSFiddle for that basic example here:
my voronoi code
now, I want each of the polygons' text in the center of the polygon, and don't cross with the polygon's border. If the polygon have not enough space to contain the all text, just contain the first part of it!
Let me know if there is anything I can do to solve this issue, thank you!
PS:I'm so sorry to my English, yes, it's so poor! :)
Have a look at this example http://bl.ocks.org/mbostock/6909318 , you probably want to place the text at the polygon centroid and not the seed (point) used to determine the voronoi tessellation.
That should fix the majority of your layout issues.
Automatically scaling the text to fit is a little bit harder, if you are willing to scale and rotate the text you can use a technique similar to the following to determine the length of the line at that point:
https://mathoverflow.net/questions/116418/find-longest-segment-through-centroid-of-2d-convex-polygon
Then you need to determine the angle of the line. I have a plugin that should help with that:
http://bl.ocks.org/stephen101/7640188/3ffe0c5dbb040f785b91687640a893bae07e36c3
Lastly you need to scale and rotate the text to fit. To determine the width of the text use getBBox() on the text element:
var text = svg.append("svg:text")
.attr("x", 480)
.attr("y", 250)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.style("font", "300 128px Helvetica Neue")
.text("Hello, getBBox!");
var bbox = text.node().getBBox();
Then you use the angle you calculated earlier to scale and rotate your text:
text.attr("transform", "rotate(40) scale(7)")
I would love to give a complete example but this is quite a bit of work to get it right.
There are other options to achieve the same effect but none of them are simple (ie you could anneal the layout similar to the way d3 does the Sankey layout)

D3: How to access an attribute of a previous item

I am using D3 to plot a rectangle for each object in an array, the height of the rectangle being dependant on the 'Size' property of the object. These rectangles are stacked on top of each other. I currently set the y position by summing the 'Size' of each subsequent rect that gets plotted - but this seems wrong - and I was wondering if there was a better way to do this, such as accessing the 'y' attribute of the previous item (and how?) or another way...
This is what the essence of my code looks like. There is a link to the fiddle below.
var cumY = 0;
var blocks1 = sampleSVG.selectAll("rect")
.data(fpp)
.enter()
.append("rect")
.sort(SortBySize)
.style("stroke", "gray")
.style("opacity", blockOpacity)
.style("fill", function (d) {return d.Colour})
.attr("width", 80)
.attr("height", function (d) {return d.Size})
.attr("x", 5)
.attr("y", function (d, i) {
var thisY = cumY;
cumY += d.Size;
// perhaps I could just return something like d.Size + previousItem.GetAttribute("y") ???
return thisY;
});
http://jsfiddle.net/ninjaPixel/bvER3/
This is tricky do! You're right that keeping track of the cumulative height 'seems wrong' - it works now but it isn't very idiomatic d3 and will get pretty messy once you start trying to do something more complicated.
I would try using d3's built in stack-layout which was created solve this problem. You might want to start working off of this example and posting an updated fiddle if you get stuck. Good luck!

Resources