d3: rotations and force directed layouts - javascript

When I use rotations in my d3 applications tick function the entire application slows to a crawl.
As an example: If you uncomment the line //var angle = 0; in the following jsfiddle it runs 20x faster.
Why is this? Are rotations just very expensive or am I doing something wrong?
function tick() {
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; });
linktext.attr("transform", function(d) {
var xDiff = d.source.x - d.target.x;
var yDiff = d.source.y - d.target.y;
var angle = Math.atan2(yDiff, xDiff) * (180.0 / Math.PI);
//var angle = 0;
return "translate(" + (d.source.x + d.target.x) / 2 + ","
+ (d.source.y + d.target.y) / 2 + ")rotate(" + angle + ")";
});
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
Note: I modified the origional jsfiddle found here

To track down the source of the problem, I played around with making various aspects of the text the same/different. See this version of your fiddle. Note that the text is different and the angle is different for each text element (so no optimization possible there) but the angle for each element is constant -- it doesn't change on every tick.
The result? Slightly sluggish at first (when there is a lot of overlap in the graph), but it quickly progresses to smooth animation under 30fps.
The same is true (with a final frame rate just slightly over 30fps) even if the text content changes every tick, as in this version.
This contradicts the usual rule of optimizing animation, that changing transformations should be more efficient than changing content.
According to the Chrome frame-rate inspector, most of the time being consumed in each repaint for your original fiddle (which clocks in around 4fps on my computer) is being taken up by the "Paint setup" step -- i.e., in calculating each "layer" of the image.
This blog has a quick-and-easy recap of the different steps of a repaint. Quote:
The following steps render the elements in the DOM into images on your screen:
Trigger - Elements are loaded into the DOM, or are modified in some way
Recalculate Styles - Styles are applied to elements (or re-calculated)
Layout - Elements are laid out geometrically, according to their positions on the screen
Paint Setup - The DOM is split up into render layers, which will be used to fill out the pixels for each element
Paint - Each layer is painted into a bitmap and is uploaded to the GPU as a texture by a software rasterizer
Composite Layers - The layers are composited together by the GPU and drawn to a final screen image
Normally, transformations can be done efficiently by the GPU in the final "composition" step (and a modern browser on a modern OS will automatically shift the work to the GPU).
There are two reasons why this might not be happening. The first is simply that this optimization might not even be applied for SVG (although I'm pretty sure the default setting for the latest Chrome is to optimize SVG transforms). However, even if the browser uses some GPU optimization for SVG transforms, your GPU can only handle a limited number of layers before it runs out of memory. With nearly 200 separately-transformed text elements (and the untransformed content layered above and below), that would be a likely bottleneck. See this HTML5Rocks post or this MSDN article, which gives some examples of performance limits that will cancel out independent layer composition.
Whatever's going on under the hood, the end result is that your CPU, not your GPU, is calculating the rotations and layering the text together each time, and that's not efficient.
So, what can you do about it?
I tried optimizing the code by using a matrix transformation instead of first calculating an angle and then making the browser calculate the rotation (see live version) ...but that didn't make a noticeable difference. Changing to a simple skew transform instead of a rotation helped a little (frame rates up to 11fps), but that just added ugly text on top of laggy animation.
Unfortunately, it looks you're really going to have to compromise one way or the other. Some options:
Hide the text until the force layout has stopped, and only then calculate the rotation. Working example
Key code (Javascript):
var vis = d3.select(".intgraph").append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g")
.on("click", function(){
if ( force.alpha() )
force.stop();
else
force.resume();
});
force.on("start", function(){
vis.classed("running", true);
})
.on("end", function () {
linktext.attr("transform", function (d) {
var xDiff = d.source.x - d.target.x,
xMid = d.source.x - xDiff / 2;
var yDiff = d.source.y - d.target.y,
yMid = d.source.y - yDiff / 2;
var hyp = Math.sqrt(xDiff * xDiff + yDiff * yDiff),
cos = xDiff / hyp,
sin = yDiff / hyp;
return "matrix(" +
[cos, sin, -sin, cos, xMid, yMid] + ")";
});
vis.classed("running", false);
});
CSS:
.running text {
display:none;
}
Show the text, but don't rotate it (optionally, rotate it in to place when the force layout stops, as above).

When you do //var angle = 0; the compiler of this code probably realizes the result is meaningless and is optimized out since it never changes. That would explain why it runs 20x faster when commenting that code.
I suspect even if you kept the translate part and removed the rotation it would be slower (though translation is clearly less computationally expensive than rotation since translations are just 3 additions compared with rotations ).
While i am not so familiar with d3.js, it would seem you are now performing translation and rotation in a function which probably gets called many times on the cpu. (generally you would want to do that on the gpu via a shader - though im not sure if/how that would apply to d3.js).

Related

Invisible vectors? Combining d3.tile(), d3.zoom() and TopoJSON vectors

I've made effective D3 maps using rasters (d3.tile and map libraries) and vectors (TopoJSON in SVG shapes). But I hit a bug when I combine them.
I followed Mike Bostock's raster-and-vector examples, especially his "Raster & Vector III", which changes the transform and stroke width to update how the vectors are displayed.
My map almost works perfectly. However, upon loading, only the raster tiles are displayed; the vectors are invisible:
But as soon as I trigger the d3.zoom event (by panning or zooming), the vectors are displayed:
I don't understand this, because I explicitly tell the browser, independently of the zoom event, to draw the vectors. This is the relevant snippet:
// read in the topojson
d3.json("ausElectorates.json", function(error, mapData) {
if (error) throw error;
var electorates = topojson.feature(mapData, mapData.objects.tracts);
// apply a zoom transform equivalent to projection{scale,translate,center}
map.call(zoom)
.call(zoom.transform, d3.zoomIdentity
.translate(mapWidth / 2, mapHeight / 2)
.scale(1 << 12)
.translate(-centre[0], -centre[1]));
// draw the electorate vectors
vector.selectAll("path")
.data(electorates.features)
.enter().append("path")
.attr("class", "electorate")
.attr("d", path);
});
For some reason, that last line of the d3.json() function -- .attr("d", path") -- isn't visualising the vectors.
Click here to see the map. Click here to access the full code and the TopoJSON it uses.
I'd love advice on this one, which is baffling me!
(PS Apologies for omitting copyright attributions for the map tiles, D3.js library, etc - I'm just trying to minimise the code for this example.)
It is drawing the vectors - however, you can't rely on solely scaling and translating your vector with the d3 geoProjection as when you zoom you apply the translate and scale to the path itself - not the projection:
vector.selectAll("path")
.attr("transform", "translate(" + [change.x, change.y] + ")scale(" + change.k + ")")
.style("stroke-width", 1 / change.k);
Since you don't set scale and translate, when loading your vectors they just aren't drawn correctly. They are drawn very small - as your projection scale is 1/tau, with a translation of [0,0]. Inspecting the svg on page load shows that they are there, and they are tiny.
The solution is to draw your vectors prior to map.call("zoom") - this way you can apply the base transform (center, transform, and scale) on the path before manually zooming:
// read in the topojson
d3.json("ausElectorates.json", function(error, mapData) {
if (error) throw error;
var electorates = topojson.feature(mapData, mapData.objects.tracts);
// draw the electorate vectors
vector.selectAll("path")
.data(electorates.features)
.enter().append("path")
.attr("class", "electorate")
.attr("d", path);
// apply a zoom transform equivalent to projection{scale,translate,center}
map.call(zoom)
.call(zoom.transform, d3.zoomIdentity
.translate(mapWidth / 2, mapHeight / 2)
.scale(1 << 12)
.translate(-centre[0], -centre[1]));
});

brush on rotated lines using d3 to create zoom effect

I am working on this plnkr. I have three lines at angle 30, 45 and 60. I want to apply a brush on these lines so that when the chart is brushed the lines get redrawn at where it crossed the brushed rectangle with appropriate the values on the axis. Any help or hint to solve this problem is greatly appreciated.
EDIT: If you have different solutions to draw the rotated lines and brush on top of them it is welcomed too. Please help.
var ga = d3.select("svg")
.append("g")
.attr("class", "a axis")
.attr("transform", "translate(" + margin.left + "," + (height + margin.top) + ")")
.selectAll("g")
.data([30, 45, 60])
.enter()
.append("g")
.attr("class", "rotatedlines")
.attr("transform", function(d) { return "rotate(" + -d + ")"; })
.attr("stroke-width", 1)
.attr("stroke", "black")
.attr("stroke-dasharray", "5,5");
To explain my solution:
The fundamental steps to take are as follows:
update the domains of the x and y scales to the brush extent
redraw the axes
compute the scale factor and translation for the lines
scale and translate the line containers accordingly
reset the brush
Note that steps 3 and 4 are only necessary because you're not using the scales to draw everything -- a better approach would be to define two points for each line as the data that's bound to the elements and then use the scales to redraw. This would make the code simpler.
With your approach it's still possible though. In order to facilitate it, I've made a few modifications to your code -- in particular, I've cleaned up the various nested g elements with different translations and defined the lines through their x1, x2, y1, y2 attributes rather than through translation of the containers. Both of these changes make the functionality you want easier to implement as only a single transformation takes places that doesn't need to consider multiple other transformations. I've also nested the lines in multiple g elements so that they can be scaled and translated more easily.
The brush handler function now looks like this:
// update scales, redraw axes
var extent = brush.extent();
x.domain(brush.empty() ? x2.domain() : [ extent[0][0], extent[1][0] ]);
y.domain(brush.empty() ? y2.domain() : [ extent[0][1], extent[1][1] ]);
xAxisG.call(xAxis);
yAxisG.call(yAxis);
This code should be fairly self-explanatory -- the domains of the scales are updated according to the current extent of the brush and the axes are redrawn.
// compute and apply scaling and transformation of the g elements containing the lines
var sx = (x2.domain()[1] - x2.domain()[0])/(x.domain()[1] - x.domain()[0]),
sy = (y2.domain()[1] - y2.domain()[0])/(y.domain()[1] - y.domain()[0]),
dx = -x2(x.domain()[0]) - x2.range()[0],
dy = -y2(y.domain()[1]) - y2.range()[1];
d3.selectAll("g.container")
.attr("transform", "translate(" + [sx * dx, sy * dy] + ")scale(" + [sx, sy] + ")");
This is the tricky part -- based on the new domains of the scales, we need to compute the scale and translation for the lines. The scaling factors are simply the ratio of the old extent to the new extent (note that I have made copies of the scales that are not modified), i.e. a number greater than 1. The translation determines the shift of the (0,0) coordinate and is computed through the difference of the old (0,0) coordinate (I get this from the range of the original scales) and the position of the new domain origin according to the original scales.
When applying the translation and scale at the same time, we need to multiply the offsets with the scaling factors.
// reset brush
brush.clear();
d3.select(".brush").call(brush);
Finally, we clear the brush and reset it to get rid of the grey rectangle.
Complete demo here.
You can access the brush extent via d3.event.target.extent(). The flow for drawing the scale is this:
Set scale
Set axis
Draw axis
As soon as the brush is done, you have to modify the scale and then re-draw the axis according to the current x and y domain. Is that what you meant?
I cleaned up the code a bit and made a little demonstration: http://plnkr.co/edit/epKbXbcBR2MiwUOMlU5A?p=preview

how to control the simulation speed of d3 force layout

I am following a d3 force laytout example like this.
I want to control the speed of the dots flying to the cluster. In other words, I want some dots take more time to get to their final positions, while some dots take less time.
I tried to add a timer function to control the time of each tick, but it did not work.
this.force = d3.layout.force()
.on("tick", setTimeout(tick(d), 50));
I need help for this.
Don't set a timer to call the tick function, this is done automatically by the force layout.
There are however a number of parameters you can set to modify the behaviour of the force layout. The ones most relevant to what you're trying to do are the following.
.friction() corresponds to how quickly the velocity decays and therefore directly controls how fast nodes move. The default is 0.9, to make everything slower, set it to a lower value.
.charge() controls how strong the attraction/repulsion between nodes is. This doesn't control the velocity directly, but affects it.
Of these parameters, only the latter can be set on a node-by-node basis. This makes achieving what you want a bit tricky, as you would have to carefully balance the forces. As a start, setting the charge of the nodes that you want to move slower closer to 0 should help.
There are a few other parameters of the force layout that I think would not be useful in your particular case (I'm thinking of .linkStrength() and .linkDistance()), but you may want to have a look nevertheless.
I came to a possible solution. I can just manually call tick function for each node, say 100 times, and record the path. Then I use a timer function to redraw the node according to the path. In this way, I can control the time of drawing for each node. Does this make sense?
Change the setIntervel function , intervel timing
Try this code:
Fiddle:
setInterval(function(){
nodes.push({id: ~~(Math.random() * foci.length)});
force.start();
node = node.data(nodes);
node.enter().append("circle")
.attr("class", "node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 8)
.style("fill", function(d) { return fill(d.id); })
.style("stroke", function(d) { return d3.rgb(fill(d.id)).darker(2); })
.call(force.drag);
}, 50); //change the intervel time

d3.js force directed layout constrained by a shape

I was wondering if there is a way to create a force directed layout with d3.js and restrict it by an arbitrary shape in such a way that
all the nodes are equivalently distributed within the shape and
the distance between the border and the nodes is equally to the distance between the nodes
I hope there is already such a solution out there. Otherwise my idea is to start with the force directed layout and check the distances from the nodes to the borders in each iteration. Any suggestions from yourside?
Your idea is mine too. In the tick function you could add additional forces. This is my suggestion (not tested):
force.on('tick', function(e) {
node
.each(calcBorderDistance)
.attr('transform', function(d) {
d.x -= e.alpha*(1/Math.pow(d.borderDistance.x,2);
d.y -= e.alpha*(1/Math.pow(d.borderDistance.y,2);
return 'translate(' + d.x + ',' + d.y + ')'; // Move node
});
});
function calcBorderdistance(d) {
// Insert code here to calculate the distance to the nearest border of your shape
var x = ..., y = ...;
d.borderDistance = {'x':x,'y':y};
}
I have the inverse quadratic distance to the nearest border function loosely based on the formulas in excelent paper Drawing Graphs Nicely using Simulated Annealing. Following picture illustrates how methods from this paper affect drawing nodes bounded by a box:
And this picture illustrate case with different constraints, involving links between nodes:

Tree/dendrogram with elbow connectors in d3

I'm very new to d3.js (and SVG in general), and I want to do something simple: a tree/dendrogram with angled connectors.
I have cannibalised the d3 example from here:http://mbostock.github.com/d3/ex/cluster.html
and I want to make it more like the protovis examples here:
http://mbostock.github.com/protovis/ex/indent.html
http://mbostock.github.com/protovis/ex/dendrogram.html
I have made a start here: http://jsbin.com/ugacud/2/edit#javascript,html and I think it's the following snippet that's wrong:
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
However there's no obvious replacement, I could use d3.svg.line, but I don't know how to integrate it properly, and ideally I'd like an elbow connector....although I am wondering if I am using the wrong library for this, as a lot of the d3 examples I've seen are using the gravitational force to do graphs of objects instead of trees.
Replace the diagonal function with a custom path generator, using SVG's "H" and "V" path commands.
function elbow(d, i) {
return "M" + d.source.y + "," + d.source.x
+ "V" + d.target.x + "H" + d.target.y;
}
Note that the source and target's coordinates (x and y) are swapped. This example displays the layout with a horizontal orientation, however the layout always uses the same coordinate system: x is the breadth of the tree, and y is the depth of the tree. So, if you want to display the tree with the leaf (bottommost) nodes on the right edge, then you need to swap x and y. That's what the diagonal's projection function does, but in the above elbow implementation I just hard-coded the behavior rather than using a configurable function.
As in:
svg.selectAll("path.link")
.data(cluster.links(nodes))
.enter().append("path")
.attr("class", "link")
.attr("d", elbow);
And a working example:
http://bl.ocks.org/d/2429963/

Categories

Resources