d3 steady horizontal transition along an SVG path - javascript

I'm using a d3 attrTween to translate a circle over a path smoothly, similar to this example and as shown in the picture below:
The circle's transition is defined here:
function transition() {
circle.transition()
.duration(2051)
.ease("linear")
.attrTween("transform", translateAlong(path.node()))
}
And the attribute tween is shown here:
function translateAlong(path) {
var l = path.getTotalLength();
return function (d, i, a) {
return function (t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}
This works well thanks to the SVG method getPointAtLength, which allows us to retrieve coordinates at different lengths of the path. However, I need a different kind of behavior and I've been unable to come up with a solution so far.
I need the circle to animate along the path, but at a steady horizontal speed. Meaning that the circle ought to take as much time to navigate this slice:
As it does with this slice:
Because both slices encompass the same width. On a low level, what I need is to be able to translate any X coordinate with its corresponding Y coordinate along the path. I've looked at all the SVG path methods and I haven't found anything particularly useful here. I'm hoping there's some way in D3 to feed an X coordinate to a d3 line and retrieve its corresponding Y coordinate.
Here's a JSFiddle working as described above. I'd really appreciate any help I can get on this. Thanks!

I ended up creating a lookup array for all my points along the line using getPointAtLength:
var lookup = [];
var granularity = 1000;
var l = path.node().getTotalLength();
for(var i = 1; i <= granularity; i++) {
var p = path.node().getPointAtLength(l * (i/granularity))
lookup.push({
x: p.x,
y: p.y
})
}
Once I had all those points in my lookup table, I used a bisector in my translate tween:
var xBisect = d3.bisector(function(d) { return d.x; }).left;
function translateAlong(path) {
var l = path.getTotalLength();
return function (d, i, a) {
return function (t) {
var index = xBisect(lookup, l * t);
var p = lookup[index];
return "translate(" + p.x + "," + p.y + ")";
};
};
}
And it works as expected! Yahoo!
Fiddle

Related

how to create three-level donut chart in d3.js

I'm using trying to create a multi-level donut chart in d3 version5
This image is drawn by d3 version3. it is working fine in version3. I decided to upgrade d3 to the latest version. now, donut chart is not drawn by d3(also no errors in the console)
D3 version 3 > version 5
Here is the sample dataset I used:
Hint: first value in the array is used storage and second is free storage
{
average: [30.012, 69.988],
minimum: [10, 90],
maximum: [40, 60]
}
Note: Above data is just a sample this is not exact data.
Here is the code I tried:
var width = 300;
var height = 300;
var radius = Math.floor((width / 6) - 2);
var classFn = function(a, b) {
return a === 0 ? classes[b] : 'default';
};
var pie = d3.layout.pie().sort(null);
var arc = d3.svg.arc();
var svg = d3.select(selector).append("svg");
svg.attr("width", width);
svg.attr("height", height);
svg = svg.append("g");
svg.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var gs = svg.selectAll("g").data(d3.values(dataset)).enter().append("g");
var path = gs.selectAll("path");
path = path.data(function(d) {
return pie(d);
});
path.enter().append("path");
path.attr("class", function(d, i, j) {
return classFn(i, j);
})
path.attr("d", function(d, i, j) {
return arc.innerRadius((j === 0 ? 0 : 2) + radius * j).outerRadius(radius * (j + 1))(d);
});
Note: This code is working fine in d3 version3.
2. Update:
I've updated the answer with a better solution. I didn't do this at first, because I didn't grasp you structure. I've updated it to being more D3 idiomatic. Plus it does away with the hack I made in my first update :)
var dataset = {
average: [0, 100],
minimum: [0, 100],
maximum: [0, 100]
}
var width = 300;
var height = 300;
var radius = Math.floor((width / 6) - 2);
var pie = d3.pie().sort(null);
var arc = d3.arc();
var svg = d3.select('body').append("svg");
svg.attr("width", width);
svg.attr("height", height);
svg = svg.append("g");
svg.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var gs = svg.selectAll("g").data(d3.values(dataset)).enter().append("g");
gs.each(function (d, j) {
d3.select(this).selectAll('path')
.data(pie(d)).enter()
.append('path')
.attr("class", function(d, i) {
// return classFn(i);
})
.attr('d', function (d) {
return arc
.innerRadius((j === 0 ? 0 : 2) + radius * j)
.outerRadius(radius * (j + 1))(d);
})
})
The updated code uses the index (here j) that is available when appending the g elements, which corresponds to you original j index. This makes it possible to calculate the radii in the original way.
To achieve this, the arc appending code is wrapped into a .each function that iterates over the g elements, making j available to us.
The class application should work as well, but I've commented it out, as the classFn function doesn't work, since the classes variable is not present.
1. Update:
Besides the original answer, when calculating the arc radii you rely on a j value that is different from D3 v3 and v5. I summise that j is used the index of the d3.values array, so I've cooked up a way to reverse look-up that index based on the input values.
First create a map for reverse mapping data values into their corresponding index:
var dataValueJoinChar = 'ยค'
var datasetValuesToIndex = d3.values(dataset).reduce((acc, curr, i) => {
acc[`0${dataValueJoinChar}${curr[0]}`] = i
acc[`1${dataValueJoinChar}${curr[1]}`] = i
return acc
}, {})
Then change the last part of your code to:
path = path.data(function(d) {
return pie(d);
}).enter().append("path");
path.attr("class", function(d, i, j) {
return classFn(i, j);
})
path.attr("d", function(d, i, j) {
var orgIndex = datasetValuesToIndex[`${i}${dataValueJoinChar}${d.data}`]
return arc
.innerRadius((orgIndex === 0 ? 0 : 2) + radius * orgIndex)
.outerRadius(radius * (orgIndex + 1))(d);
});
It might not be too pretty, but it's a simple adaption of your code that works.
------- Original answer --------
In D3 v5 pie and arc are found at d3.pie and d3.arc respectively. Therefore, try changing:
var pie = d3.layout.pie().sort(null);
var arc = d3.svg.arc();
To this instead:
var pie = d3.pie().sort(null);
var arc = d3.arc();
Pie API reference: https://github.com/d3/d3-shape/blob/v1.3.4/README.md#pie
Arc API reference: https://github.com/d3/d3-shape/blob/v1.3.4/README.md#arc
If you use a bundler to bundle sub-modules, both are part of the d3-shape module. If not they are both available in the full D3 library.
Hope this helps!

Amending D3 circle animation

This is a very simple question. Here is my jsfiddle, where I animate a circle between two nodes using D3 (original code is Mike Bostock's): http://jsfiddle.net/Guill84/uxy8d9vs/5/.
How do I stop the circle from returning to node A when it's reached node B? The relevant bit of code is as follows:
var path = d3.select("path#AB"),
startPoint = pathStartPoint(path);
marker.attr("r", 7)
.attr("transform", "translate(" + startPoint + ")");
transition();
//Get path start point for placing marker
function pathStartPoint(path) {
var d = path.attr("d"),
dsplitted = d.split(" ");
return dsplitted[1].split(",");
}
function transition() {
marker.transition()
.duration(2000)
.attrTween("transform", translateAlong(path.node()));
}
function translateAlong(path) {
var l = path.getTotalLength();
return function(i) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";//Move marker
}
}
}
var l = path.getTotalLength() / 2;
Because your path is two arcs, one from A to B, the other from B to A. Traversing the full path it's always going to return to A. Doing half the length of the path it stops at B.
On another point your fiddle ends up crashing chrome on my pc, I think it's to do with calling the transition from itself ('the infinite loop' bit)

Show D3 link text right-side up

I have built a D3 force directed visualization with text labels along the links. The one problem I'm running into is these labels appearing upside down when the links are to the left of their source node. Example here:
The code where I position the path and text looks like so:
var nodes = flatten(data);
var links = d3.layout.tree().links(nodes);
var path = vis.selectAll('path.link')
.data(links, function(d) {
return d.target.id;
});
path.enter().insert('svg:path')
.attr({
class: 'link',
id: function(d) {
return 'text-path-' + d.target.id;
},
'marker-end': 'url(#end)'
})
.style('stroke', '#ccc');
var linkText = vis.selectAll('g.link-text').data(links);
linkText.enter()
.append('text')
.append('textPath')
.attr('xlink:href', function(d) {
return '#text-path-' + d.target.id;
})
.style('text-anchor', 'middle')
.attr('startOffset', '50%')
.text(function(d) {return d.target.customerId});
I know I will need to somehow determine the current angle of each path and then set the text position accordingly, but I am not sure how to.
Here is a link to a block based on this issue: http://blockbuilder.org/MattDionis/5f966a5230079d9eb9f4
The answer below has got me about 90% of the way there. Here is what my original visualization looks like with text longer than a couple digit number:
...and here is what it looks like utilizing the tips in the below answer:
So while the text is now "right-side up", it no longer follows the arc.
The arcs you draw are such that their tangent in the middle is exactly the direction of the baseline of the text, AND it is also colinear with the vector that separates the two tree nodes.
We can use that to solve the problem.
A bit of math is needed. First, let's define a function that returns the angle of a vector v with respect to the horizontal axis:
function xAngle(v) {
return Math.atan(v.y/v.x) + (v.x < 0 ? Math.PI : 0);
}
Then, at each tick, let's rotate the text in place by minus the angle of its baseline. First, a few utility functions:
function isFiniteNumber(x) {
return typeof x === 'number' && (Math.abs(x) < Infinity);
}
function isVector(v) {
return isFiniteNumber(v.x) && isFiniteNumber(v.y);
}
and then, in your tick function, add
linkText.attr('transform', function (d) {
// Checks just in case, especially useful at the start of the sim
if (!(isVector(d.source) && isVector(d.target))) {
return '';
}
// Get the geometric center of the text element
var box = this.getBBox();
var center = {
x: box.x + box.width/2,
y: box.y + box.height/2
};
// Get the tangent vector
var delta = {
x: d.target.x - d.source.x,
y: d.target.y - d.source.y
};
// Rotate about the center
return 'rotate('
+ (-180/Math.PI*xAngle(delta))
+ ' ' + center.x
+ ' ' + center.y
+ ')';
});
});
edit: added pic:
edit 2 With straight lines instead of curved arcs (simply <text> instead of <textPath> inside of <text>), you can replace the part of the tick function that concerns linkText with this:
linkText.attr('transform', function(d) {
if (!(isVector(d.source) && isVector(d.target))) {
return '';
}
// Get the geometric center of this element
var box = this.getBBox();
var center = {
x: box.x + box.width / 2,
y: box.y + box.height / 2
};
// Get the direction of the link along the X axis
var dx = d.target.x - d.source.x;
// Flip the text if the link goes towards the left
return dx < 0
? ('rotate(180 '
+ center.x
+ ' ' + center.y
+ ')')
: '';
});
and this is what you get:
Notice how the text gets flipped as the link goes from pointing more to the right to pointing more to the left.
The problem with this is that the text ends up below the link. That can be fixed as follows:
linkText.attr('transform', function(d) {
if (!(isVector(d.source) && isVector(d.target))) {
return '';
}
// Get the geometric center of this element
var box = this.getBBox();
var center = {
x: box.x + box.width / 2,
y: box.y + box.height / 2
};
// Get the vector of the link
var delta = {
x: d.target.x - d.source.x,
y: d.target.y - d.source.y
};
// Get a unitary vector orthogonal to delta
var norm = Math.sqrt(delta.x * delta.x + delta.y * delta.y);
var orth = {
x: delta.y/norm,
y: -delta.x/norm
};
// Replace this with your ACTUAL font size
var fontSize = 14;
// Flip the text and translate it beyond the link line
// if the link goes towards the left
return delta.x < 0
? ('rotate(180 '
+ center.x
+ ' ' + center.y
+ ') translate('
+ (orth.x * fontSize) + ' '
+ (orth.y * fontSize) + ')')
: '';
});
and now the result looks like this:
As you can see, the text sits nicely on top of the line, even when the link points towards the left.
Finally, in order to solve the problem while keeping the arcs AND having the text right side up curved along the arc, I reckon you would need to build two <textPath> elements. One for going from source to target, and one for going the opposite way. You would use the first one when the link goes towards the right (delta.x >= 0) and the second one when the link goes towards the left (delta.x < 0) and I think the result would look nicer and the code would not be necessarily more complicated than the original, just with a bit more logic added.

How to move a node along a vector?

I have a bunch of nodes in a circle around a centre point. I got these positions by drawing arcs first then using the arcs [X,Y] position, populated an array which was used for the positions of the nodes. Using the forcelayout from the javascript library D3.
What I want to do now, if the nodes meet a certain criteria, for example, name starts with L, move them out to the outline of a bigger circle. I have made a simple diagram to explain.
I wish to be able to move from [X2,Y2] to [X3,Y3]. I labelled [X1,Y1] as I am sure you would need this to work out the vector from x1y2 to x2,y2 wish would then be used to calculate the movement along that vector, but I'm unsure how to do this movement
I don't know if the problem is still active, but I'll answer anyway. Since the Problem has a cylindrical symmetry it is best to use polar coordinates. So x,y become r,phi whereas r = sqrt(x^2+y^2) and phi=arctan(y/x). If you want to move a point X(r,phi) in the radial direction by lets say r' you do it by simple adding it to the existing radius. Thus X'=X(r+r',phi)
Here's the way I solved it. I had a variable moveOutso I could toggle between the original node position and the one I move to. So depending on the value of moveOut I alter the scale of movement away from center.
var thisNode = circleViewNode.filter(function(d){
//console.log(d)
return d.origin != 'EquivalenceSets' && d.hasRelationship != true;
});
thisNode.each(function(d){
thisNodeSize = d.thisRadius;
});
if(!moveOut){
thisScale = innerModelRadius - thisNodeSize*1.5;
moveOut = true;
} else {
thisScale = innerModelItemRadius + (outerModelItemRadius - innerModelItemRadius)/2;
moveOut = false;
}
thisNode.each(function(d){
//console.log(d);
var centerOfCircle = [width/2,height/2]; //get center
//var centerOfCircle = [arcCenter.x, arcCenter.y];
var thisPosition = [d.x, d.y]; //get position of current node
//thisVector = [center[0]-thisPosition[0], center[1]-thisPosition[1]],
var thisVector = [thisPosition[0] - centerOfCircle[0], thisPosition[1] - centerOfCircle[1]];
var thisVectorX = thisVector[0];
var thisVectorY = thisVector[1];
var xSquared = Math.pow(thisVector[0],2);
var ySquared = Math.pow(thisVector[1],2);
var normalVector = Math.sqrt(xSquared + ySquared); //using pythagoras theorum to work out length from node pos to center
//console.log(d);
thisVectorX= thisVectorX/normalVector;
thisVectorY= thisVectorY/normalVector;
// d.x = centerOfCircle[0]+(thisVectorX*thisScale);// + -38.5;
// d.y = centerOfCircle[1]+(thisVectorY*thisScale);// + -20;
d.x = centerOfCircle[0]+(thisVectorX*thisScale); //thisScale gives the ability to move back to original pos
d.y = centerOfCircle[1]+(thisVectorY*thisScale);
//}
})
.transition().duration(1000)
.attr("transform", function(d)
{
//console.log(d.hasRelationship);
//console.log(d.y);
return "translate(" + d.x + "," + d.y + ")"; //transition nodes
});

Implementing complex Processing pushMatrix/popMatrix logic with d3js

I want to reimplement the following Processing sketch with d3js.
Recursion Thing
This wonderful sketch recursively builds up the graph, using a complex pushMatrix, popMatrix hierarchy.
How could this be implemented in d3.js as we there always work on the DOM immediately when appending a shape or transformation. But in a logic like in this sketch the appending part seems to have to be held back for the respective popMatrix to come. It feels like I have to implement my own transform and shape stack to temporary remember the transformation and shapes to be added until the popMatrix comes but that seems so not d3.js.
Any suggestion highly appreciated
ps:
i dont want to use processing.js as i want to work with svg, not canvas.
Interesting problem! Here's my take on it: http://jsfiddle.net/Y48BL/
This is more a proof of concept; I didn't do all the different colours and such. Nevertheless, it demonstrates the general approach. The general idea is to use g elements instead of the matrices that processing uses. Both are used for local transformations of the coordinate system; in the case of the g elements by setting transform accordingly. New gs (matrices) are created inside the recursive function and then passed on to the next level of the recursion. This would correspond to pushMatrix(). Coming back up, we continue to use the original g, corresponding to popMatrix().
The translation of the drawing of the circles and lines is fairly straightforward -- I find the D3 code much easier to read.
So I came up with this helper "class" to get this done, maybe a bit of a overkill but I will have more use cases for this.
var TransformStack = (function () {
function TransformStack() {
this.stack = [];
}
TransformStack.prototype.getCurrentElement = function () {
return this.stack[this.stack.length - 1];
};
TransformStack.prototype.setCurrentElement = function (element) {
this.stack[this.stack.length - 1] = element;
};
TransformStack.prototype.push = function (transformElement) {
this.stack.push(transformElement);
};
TransformStack.prototype.pushAndTransform = function (transformAttr) {
this.push(this.getCurrentElement().append("g").attr("transform", transformAttr));
};
TransformStack.prototype.transform = function (transformAttr) {
this.setCurrentElement(this.getCurrentElement().append("g").attr("transform", transformAttr));
};
TransformStack.prototype.pop = function () {
return this.stack.pop();
};
return TransformStack;
})();
Basically a stack to push/pop g elements which replaces the matrices approach in processing as Lars already pointed out. With this the main routine looks something like
var svg = d3.select("body").append("svg").attr("width", width).attr("height", height)
.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + gScale + ")");
var tstack = new TransformStack();
tstack.push(svg);
doIt(nRecursions);
function doIt(n) {
// Circle
tstack.getCurrentElement()
.append("circle")
.attr("r", theSize)
.style("fill", "#fe6b0c")
.style("stroke", "0")
.style("stroke-width", "2")
.style("opacity", 0.3);
if (n != nRecursions) {
for (var i = 0; i < 4; i++) {
tstack.getCurrentElement().append("line")
.style("stroke", "red")
.style("opacity", 0.16)
.attr("x1", Math.random() * 4 - 2)
.attr("x2", Math.random() * 4 - 2)
.attr("y1", theSize / 2.0 + Math.random() * 4 - 2)
.attr("y2", distance - (theSize * theScale / 2.0) - 8.0 + Math.random() * 4 - 2);
}
}
var rot = 0;
tstack.pushAndTransform("scale(" + theScale + ")");
for (var i = 0; i < n; i++) {
if (n > 0) {
tstack.pushAndTransform("translate(0," + distance + ")");
doIt(n - 1);
tstack.pop();
rot = 360 / n;
tstack.transform('rotate(' + rot + ')');
}
}
tstack.pop();
}
}
Just wanted to share this, maybe of some use for some. The main point was given by Lars already.

Categories

Resources