d3.js force directed layout constrained by a shape - javascript

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:

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

Issue w/ Programmatic Zoom v4

I'm using d3.js v4. I'm currently implementing programmatic zoom. This Panning and Zooming Tutorial has helped tremendously. My zoom works with scrolling wheel, but I want to create buttons to zoom. I know what necessary for zooming and panning is a translation [tx, ty] and a scale factor k. I'm using timescale for my x-Axis. I've managed to get tx and scale factor of k, by getting the pixel value of p1 (point 1) and p2(point 2) on the x-axis and then using those values to get a k (Scale factor). Like such:
var k = 500 / (xScale(p2) - xScale(p1)); //500 is desired pixel diff. between p1 and p2, and xScale is my d3.scaleTime() accessor function.
// for this zoom i just want the first value and last value to be at scale difference of the entire width.
Then I calculate tx by this:
var tx = 0 - k * p1;
Then feeding it into a d3.zoomIdentity() and rescaling my xdomain. I created a button to zoom back out. The issue is when I zoom in and then try to use the button to zoom out, it zooms out, but shrinks the x-axis. I can't seem to findout why its shrinking the x-axis instead of zooming back out correctly.
My JSFiddle
https://jsfiddle.net/codekhalifah/Lmdfrho7/2/
What I've Tried
Read zoom Documentation
Read through chapter on zoom in D3.js in action
My Code
After wheel zoom is applied I run this function:
function zoomed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
var t = d3.event.transform;
console.log(t);
console.log(xScale.domain());
xScale.domain(t.rescaleX(x2).domain());
usageAreaPath.attr("d", area);
usageLinePath.attr('d',line);
weatherAreaPath.attr('d',weatherChart.area);
focus.select(".axis--x").call(xAxis);
focus.selectAll('.circle')
.attr('cx', function(d) { return xScale(getDate(d)); })
.attr('cy', function(d) { return yScale(d.kWh); })
.attr("transform", "translate(0," + 80 + ")");
... other non related items
}
The zoom works properly, but after zooming in and then manually attempting to zoom back normal position I want.
My manual zoom button function
function programmaticZoom(){
var currDataSet = usageLinePath.data()[0], //current data set
currDataSetLength = currDataSet.length,//current data set length
x1 = currDataSet[0].usageTime, //getting first data item
x2 = currDataSet[currDataSetLength-1].usageTime, //2nd data item
x1px = xScale(moment(x1)), //Get current point 1
x2px = xScale(moment(x2)); // Get current point 2
// calculate scale factor
var k = width / (x2px - x1px); // get scale factor
var tx = 0 - k * x1px; // get tx
var t = d3.zoomIdentity.translate(tx).scale(k); //create zoom identity
xScale.domain(t.rescaleX(window.x2).domain());
usageAreaPath.attr("d", area);
usageLinePath.attr('d',line);
weatherAreaPath.attr('d',weatherChart.area);
focus.select(".axis--x").call(xAxis);
focus.selectAll('.circle')
.attr('cx', function(d) { return xScale(getDate(d)); })
.attr('cy', function(d) { return yScale(d.kWh); })
.attr("transform", "translate(0," + 80 + ")");
}
I've been looking at this for a little while now, and I got it sort of working, there's one thing I can't figure out but you might be able to since you seem more familiar with d3 than I am. In your programmaticZoom() function, I noticed that tx is the offset for where the start of the graph is, and k is the scale. Using this, I changed the block:
var k = width / (x2px - x1px); // get scale
var tx = 0 - k * x1px; // get tx
var t = d3.zoomIdentity.translate(tx).scale(k);
to
var k = 1;
var t = d3.zoomIdentity.scale(k);
First, when k = 1.0, the graph will fit in the window perfectly. The reason I believe setting k to its former value was wrong is that, when you increase the value of k so that k > 1.0, it stretches the width of the graph past the screen. The content on the graph has to be readjusted so that it can take up as much space on the graph as possible, while still being within the bounds of the screen. With k < 1, which is what happens with width / (x2px - x1px);, the graph shrinks to be less than the size of the screen. Readjusting like this will only make the graph's content fit to take up the maximum that it can within the graph, but since the graph is smaller than the screen, it will be readjusted to fit the shrunken graph and appear smaller than the screen.
I got rid of tx entirely because it offsets where the graph starts at. When the graph is zoomed in, it's width is stretched past the screen. It makes sense to offset here because you need to have your offset equal to where you want to begin viewing the graph, and allow the parts you don't need to remain off of the screen. In the case where you're zooming out all the way, the graph is the size of the screen, so offsetting is going to cause your graph to not start at the beginning of the screen and instead push part of it off of the screen. In your case, with k already shrinking the graph, the offset causes the shrunken graph to appear in the middle of the screen. However if the graph was not shrunken, the offset would push part of the graph off of the screen.
By changing that, the zoom out button appears to work, however there is still a problem. In the zoomed() function, you set var t = d3.event.transform;. The d3.event.transform contains values of k, x, and y that need to be 1, 0, and 0 respectively when completely zoomed out. I cannot change these values in the programmaticZoom() function though, because d3.event.transform only exists after an event was fired, specifically the mouse wheel. If you are able to get these values to be k = 1, x = 0, y = 0 only when the zoom button is clicked, the issue should be completely fixed.
Hopefully that helped some, I'm not very familiar with d3 but this should solve most of your problem and hopefully give you an idea of what was going wrong.

Constraining map panning with zoom.translateExtent in D3 v4

I'm trying to display a map of a single state, with zooming and panning constrained to the boundaries of the state. It's mostly working, except for the panning constraint when the state path is scaled to fit a smaller container. I think this comes down to me not understanding what arguments to use for zoom.translateExtent (although I'm very new to this, so it could be something else).
Live example on bl.ocks.org, with links to prior art.
One notable thing is that I'm using a null projection for d3.geoPath, because I used ogr2ogr to generate a shapefile in projected coordinates for each state. That's why I used a zoom transform to fit the map to its container.
#McGiogen's solution is almost correct but misses that MIN needs to vary depending on the zoom scale factor transform.k.
I drew a diagram to see how I needed to constrain my svg to always be contained inside the zoomed view (depicted in my drawing as the LARGER of the boxes, only a portion of which is visible to the user):
(since the constraint x+kw >= w is equivalent to x >= (1-k)w, with a similar argument for y)
thus assuming your svg container size [w, h]:
function zoomed() {
var t = d3.event.transform;
t.x = d3.min([t.x, 0]);
t.y = d3.min([t.y, 0]);
t.x = d3.max([t.x, (1-t.k) * w]);
t.y = d3.max([t.y, (1-t.k) * h]);
svg.attr("transform", t);
}
I'm facing the same problem today and I've done some tests.
I've noticed that it's the same weird behaviour happening when you have a translateExtent box smaller than the content's elements.
In your (and mine) code the same behaviour is triggered by zooming out: it doesn't matter if you have the translateExtent box correctly set with no zoom, if you zoom out the box is reduced at higher rate than the elements and at some point you will have translateExtent box smaller than the content (and the weird behaviour).
I temporary solved this as said here
D3 pan+ zoom constraints
var MIN = {x: 0, y: -500}, //top-left corner
MAX = {x: 2000, y: 500}; //bottom-right corner
function zoomed() {
var transform = d3.event.transform;
// limiting tranformation by MIN and MAX bounds
transform.x = d3.max([transform.x, MIN.x]);
transform.y = d3.max([transform.y, MIN.y]);
transform.x = d3.min([transform.x, MAX.x]);
transform.y = d3.min([transform.y, MAX.y]);
container.attr("transform", transform);
}
I'm still a d3 newbie but I think that this is a bug in translateExtent code.

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

d3: rotations and force directed layouts

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).

Categories

Resources