How to set a specific duration to interpolate along a path one point at time? - d3.js

I'm trying to figure out the best way to interpolate a circle along a path as Mike Bostock does in this example: http://bl.ocks.org/mbostock/1705868. However, instead of setting one transition value as he does, I'd like to be able to set a unique duration for each point-to-point interpolation; e.g., transition the circle from node[0] to node [1] over x milliseconds, transition from node [1] to node [2] over y milliseconds, etc. Is there a way to do this without splitting the path up into a bunch of smaller separate paths and transitioning along them consecutively? The limiting factor seems to be path.getTotalLength() - is there a way to get the length of only the subset of a path?
transition();
function transition() {
circle.transition()
.duration(10000)
.attrTween("transform", translateAlong(path.node()))
.each("end", transition);
}
// Returns an attrTween for translating along the specified path element.
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 + ")";
};
};
}

There's in a fact a way but it's way too ugly (because it needs an initial brute force computation), the solution involves the following:
First of all you need an array with the transition times between nodes, in my example is times, for example the first element 3000 corresponds to the time in ms to get from [480,200] to [580,400]
compute the sum of the transition times (needed for the duration of the overall transition)
compute the linear time in ms to reach each one of the points that made this path, this is actually tricky when the path between two points is not a line e.g. a curve, in my example I compute those times by brute force which makes it ugly, it'd be awesome if there was a method that computed the path length needed to get to some point lying on the path itself, unfortunately such a method doesn't exist as far as I know
Finally once you know the linear times you have to compute the correct time as if it followed the list of the numbers in the times array e.g.
Let's say that the linear time to get to the first point is 50ms and we're currently on the time t < 50ms, we have to map this value which is between [0ms, 50ms] to somewhere in the range [0ms, 3000ms] which is given by the formula 3000 * (t ms - 0ms) / (50ms - 0ms)
var points = [
[480, 200],
[580, 400],
[680, 100],
[780, 300],
[180, 300],
[280, 100],
[380, 400]
];
var times = [3000, 100, 5000, 100, 3000, 100, 1000]
var totalTime = times.reduce(function (a, b) {return a + b}, 0)
var svg = d3.select("body").append("svg")
.attr("width", 960)
.attr("height", 500);
var path = svg.append("path")
.data([points])
.attr("d", d3.svg.line()
.tension(0) // Catmull–Rom
.interpolate("cardinal-closed"));
svg.selectAll(".point")
.data(points)
.enter().append("circle")
.attr("r", 4)
.attr("transform", function(d) { return "translate(" + d + ")"; });
var circle = svg.append("circle")
.attr("r", 13)
.attr("transform", "translate(" + points[0] + ")");
function transition() {
circle.transition()
.duration(totalTime)
.ease('linear')
.attrTween("transform", translateAlong(path.node()))
.each("end", transition);
}
// initial computation, linear time needed to reach a point
var timeToReachPoint = []
var pathLength = path.node().getTotalLength();
var pointIndex = 0
for (var t = 0; pointIndex < points.length && t <= 1; t += 0.0001) {
var data = points[pointIndex]
var point = path.node().getPointAtLength(t * pathLength)
// if the distance to the point[i] is approximately less than 1 unit
// make `t` the linear time needed to get to that point
if (Math.sqrt(Math.pow(data[0] - point.x, 2) + Math.pow(data[1] - point.y, 2)) < 1) {
timeToReachPoint.push(t);
pointIndex += 1
}
}
timeToReachPoint.push(1)
function translateAlong(path) {
return function(d, i, a) {
return function(t) {
// TODO: optimize
var timeElapsed = t * totalTime
var acc = 0
for (var it = 0; acc + times[it] < timeElapsed; it += 1) {
acc += times[it]
}
var previousTime = timeToReachPoint[it]
var diffWithNext = timeToReachPoint[it + 1] - timeToReachPoint[it]
// range mapping
var placeInDiff = diffWithNext * ((timeElapsed - acc) / times[it])
var p = path.getPointAtLength((previousTime + placeInDiff) * pathLength)
return "translate(" + p.x + "," + p.y + ")"
}
}
}
transition();
path {
fill: none;
stroke: #000;
stroke-width: 3px;
}
circle {
fill: steelblue;
stroke: #fff;
stroke-width: 3px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Related

nested donuts, partial sum of the values in the 2nd dimension array

Is this possible? The reason I am asking the question is first I did the concentric donuts with 2 datasets and the slices size did not match related data it was each proportionate but slightly smaller on the inner ring and I want the slices to match inner and outer. So I read you need nested dataset.
I need the pie slices of the first 2 values of apples to match the first 2 slices of the inner and outer donuts. Then I need the total value of the remaining apples to be one slice and it needs to match the same individual pie slices of the rest of the first array. So the client just wants to compare the summed values or see it as only 3 slices compared to the 5 slices.
I used the working apples and oranges JSfiddle to start with from the internet: https://jsfiddle.net/vgq0z5aL/
I modified it here to use the dataset that will work with my problem but couldn't get it to work. Something wrong with the dataset I think?
My Example: https://jsfiddle.net/aumnxjc8/
How can I fix the dataset so it works?
var dataset = {
apples: [13245, 28479, 1000, 1000, 3000],
apples2: [dataset[0][0], dataset[0][1], sumofapples],
};
var sumofapples = dataset[0][3]+ dataset[0][4]+dataset[0][5];
var width = d3.select('#duration').node().offsetWidth,
height = 300,
cwidth = 33;
var colorO = ['#1352A4', '#2478E5', '#5D9CEC', '#A4C7F4', '#DBE8FB'];
var colorA = ['#58A53B', '#83C969', '#A8D996'];
var pie = d3.layout.pie()
.sort(null);
var arc = d3.svg.arc();
var svg = d3.select("#duration svg")
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
console.log(dataset);
var gs = svg.selectAll("g").data(d3.values(dataset)).enter().append("g");
var path = gs.selectAll("path")
.data(function(d, i) { return pie(d); })
.enter().append("path")
.attr("fill", function(d, i, j) {
if (j == 0) {
return colorO[i];
} else {
return colorA[i];
}
})
.attr("d", function(d, i, j) {
if (j == 0) {
return arc.innerRadius(75 + cwidth * j - 17).outerRadius(cwidth * (j + 2.9))(d);
} else {
return arc.innerRadius(75 + cwidth * j - 5).outerRadius(cwidth * (j + 2.5))(d);
}
});
Try:
const apples = [13245, 28479, 11111, 11000, 3876];
const apples2 = [apples[0], apples[1],
apples.slice(2).reduce((sum,item) => sum + item, 0)];
const dataset = { apples, apples2 };
You can see the result in a fiddle

Rotate every arc of pie chart 180 (like sun) with D3 JS. How to calculate translate parameters

I am working on pie chart with d3 js. I want to rotate every arc of my pie chart 180. I know that I am unable to explain completely show here is my fiddle link.
[fiddle]: https://jsfiddle.net/dsLonquL/
How can i get dynamic parameters for translate() function.
Basically you need to work out the centre point of the edge of each arc. I used this example for help : How to get coordinates of slices along the edge of a pie chart?
This works okay, but I needed to rotate the points to get them in the correct positions. As it is in radians the rotation is the following :
var rotationInRadians = 1.5708 * 1.5;
Now using the example before I used the data for the paths, so the start and end angle and got the center points like so :
var thisAngle = (d.startAngle + rotationInRadians + (d.endAngle + rotationInRadians - d.startAngle + rotationInRadians) / 2);
var x = centreOfPie[0] + radius * 2 * Math.cos(thisAngle)
var y = centreOfPie[1] + radius * 2 * Math.sin(thisAngle)
I created a function to show circles at these points to clarify :
function drawCircle(points, colour) {
svg.append('circle')
.attr('cx', points[0])
.attr('cy', points[1])
.attr('r', 5)
.attr('fill', colour);
}
Called it inside the current function like so :
drawCircle([x, y], color(d.data.label))
And then translated and rotated accordingly :
return 'translate(' + (x) + ',' + y + ') rotate(180)';
I added a transition so you can see it working. Here is the final fiddle :
https://jsfiddle.net/thatOneGuy/dsLonquL/7/
EDIT
In your comments you say you want the biggest segment to be kept in the middle. So we need to run through the segments and get the biggest. I have also taken care of duplicates, i.e if two or more segments are the same size.
Here is the added code :
var biggestSegment = {
angle: 0,
index: []
};
path.each(function(d, i) {
var thisAngle = (d.endAngle - d.startAngle).toFixed(6);//i had to round them as the numbers after around the 7th or 8th decimal point tend to differ tet theyre suppose to be the same value
if (i == 0) {
biggestSegment.angle = thisAngle
} else {
if (biggestSegment.angle < thisAngle) {
biggestSegment.angle = thisAngle;
biggestSegment.index = [i];
} else if (biggestSegment.angle == thisAngle) {
console.log('push')
biggestSegment.index.push(i);
}
}
})
Now this goes through each path checks if its bigger than the current value, if it is overwrite the biggest value and make note of the index. If its the same, add index to index array.
Now when translating the paths, you need to check the current index against the index array above to see if it needs rotating. Like so :
if (biggestSegment.index.indexOf(i) > -1) {
return 'translate(' + (centreOfPie[0]) + ',' + (centreOfPie[1]) + ')' // rotate(180)';
} else {
return 'translate(' + (x) + ',' + y + ') rotate(180)';
}
Updated fiddle : https://jsfiddle.net/thatOneGuy/dsLonquL/8/
I have editted 3 values to be different to the rest. Go ahead and change these, see what you think :)
This is a pure middle school geometry job.
CASE 1: The vertex of each sector rotation is on the outer line of the circle
fiddle
// ... previous code there
.attr('fill', function(d, i) {
return color(d.data.label);
})
.attr("transform", function(d, i) {
var a = (d.endAngle + d.startAngle) / 2, // angle of vertex
dx = 2 * radius * Math.sin(a), // shift/translate is two times of the vertex coordinate
dy = - 2 * radius * Math.cos(a); // the same
return ("translate(" + dx + " " + dy + ") rotate(180)"); // output
});
CASE 2: The vertex on the center of the chord
fiddle
// ... previous code there
.attr('fill', function(d, i) {
return color(d.data.label);
})
.attr("transform", function(d, i) {
var dx = radius * (Math.sin(d.endAngle) + Math.sin(d.startAngle)), // shift/translation as coordinate of vertex
dy = - radius * (Math.cos(d.endAngle) + Math.cos(d.startAngle)); // the same for Y
return ("translate(" + dx + " " + dy + ") rotate(180)"); // output
});

Can one specify a custom force function for a force-directed layout?

I want to experiment with an alternative family force functions for force-directed graph layouts.
For each node n_i, I can define a "force function" f_i such that
f_i ( n_i ) is identically zero; and
f_i ( n_j ), where n_i != n_j, is the force on node n_i that is due to some other node n_j.
The net force on node n_i should then be the vector sum of the forces f_i ( n_j ), where n_j ranges over all other nodes1.
Is there some way to tell d3.js to use these custom force functions in the layout algorithm?
[The documentation for d3.js's force-directed layout describes various ways in which its built-in force function can be tweaked, but I have not been able to find a way to specify an entirely different force function altogether, i.e. a force function that cannot be achieved by tweaking the parameters of the built-in force function.]
1IOW, no other/additional forces should act on node n_i besides those computed from its force function f_i.
Yes you can. Credit goes to Shan Carter and his bl.ocks example
let margin = {
top: 100,
right: 100,
bottom: 100,
left: 100
};
let width = 960,
height = 500,
padding = 1.5, // separation between same-color circles
clusterPadding = 6, // separation between different-color circles
maxRadius = 12;
let n = 200, // total number of nodes
m = 10, // number of distinct clusters
z = d3.scaleOrdinal(d3.schemeCategory20),
clusters = new Array(m);
let svg = d3.select('body')
.append('svg')
.attr('height', height)
.attr('width', width)
.append('g').attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
let nodes = d3.range(200).map(() => {
let i = Math.floor(Math.random() * m),
radius = Math.sqrt((i + 1) / m * -Math.log(Math.random())) * maxRadius,
d = {
cluster: i,
r: radius
};
if (!clusters[i] || (radius > clusters[i].r)) clusters[i] = d;
return d;
});
let circles = svg.append('g')
.datum(nodes)
.selectAll('.circle')
.data(d => d)
.enter().append('circle')
.attr('r', (d) => d.r)
.attr('fill', (d) => z(d.cluster))
.attr('stroke', 'black')
.attr('stroke-width', 1);
let simulation = d3.forceSimulation(nodes)
.velocityDecay(0.2)
.force("x", d3.forceX().strength(.0005))
.force("y", d3.forceY().strength(.0005))
.force("collide", collide) // <<-------- CUSTOM FORCE
.force("cluster", clustering)//<<------- CUSTOM FORCE
.on("tick", ticked);
function ticked() {
circles
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y);
}
// Custom 'clustering' force implementation.
function clustering(alpha) {
nodes.forEach(function(d) {
var cluster = clusters[d.cluster];
if (cluster === d) return;
var x = d.x - cluster.x,
y = d.y - cluster.y,
l = Math.sqrt(x * x + y * y),
r = d.r + cluster.r;
if (l !== r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
cluster.x += x;
cluster.y += y;
}
});
}
// Custom 'collide' force implementation.
function collide(alpha) {
var quadtree = d3.quadtree()
.x((d) => d.x)
.y((d) => d.y)
.addAll(nodes);
nodes.forEach(function(d) {
var r = d.r + maxRadius + Math.max(padding, clusterPadding),
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.data && (quad.data !== d)) {
var x = d.x - quad.data.x,
y = d.y - quad.data.y,
l = Math.sqrt(x * x + y * y),
r = d.r + quad.data.r + (d.cluster === quad.data.cluster ? padding : clusterPadding);
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.data.x += x;
quad.data.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
});
}
<!doctype html>
<meta charset="utf-8">
<body>
<script src="//d3js.org/d3.v4.min.js"></script>
Also here is a more in-depth look at the subject.
To achieve this, you'll need to create your own custom layout. There's no tutorial for this that I'm aware of, but the source code for the existing force layout should be a good starting point as, by the sound of it, the structure of your custom layout would be very similar to that.

How to repeat rotation using d3

I'm trying to figure out how to repeat a transition. I' m using world tour with my own tsv file. The tsv file s much smaller which ends the world tour quickly.
How can I repeat the rotation so its starts at beginning?
//Globe rotating
(function transition() {
d3.transition()
.duration(1500)
.each("start", function() {
title.text(countries[i = (i + 1) % n].name);
})
.style("color", "lightgreen")
.style("text-anchor", "middle")
.tween("rotate", function() {
var p = d3.geo.centroid(countries[i]),
r = d3.interpolate(projection.rotate(), [-p[0], -p[1]]);
return function(t) {
projection.rotate(r(t));
c.clearRect(0, 0, width, height); //clear the canvas for redrawing
c.fillStyle = "black", c.beginPath(), path(land), c.fill();
c.fillStyle = "lightgreen", c.beginPath(), path(countries[i]), c.fill();
c.strokeStyle = "green", c.lineWidth = .5, c.beginPath(), path(borders), c.stroke();
c.strokeStyle = "#000", c.lineWidth = 2, c.beginPath(), path(globe), c.stroke();
};
})
.transition()
.each("end", transition);
})();
}
One option would be to reset i to zero when it exceeds the number of countries in your list. Something like this:
.each("start", function() {
i = (i + 1) % n;
if(i >= names.length)
i = 0;
title.text(countries[i].name);
})
Edit: After looking at the World Tour example code, a simpler solution would be redefine n to be the length of your data (instead of the number of countries on the map):
n = names.length; // instead of countries.length
Then you can leave the rest of the code as is. The modulo in this expression - i = (i + 1) % n - will reset to zero once you reach the end of your list.

d3.js: distance point to svg:path

Is there an (efficient) method to (a) calculate the shortest distance between a fixed point and a svg:path element in d3.js and (b) determine the point on the path which belongs to this distance?
In the general case, I don´t think so. An SVG path is a complex element. For instance, if the path is a Bezier curve, the control points may be off the represented line, and the represented shape may be off the bounding box of the control points.
I think that if you have a set of points that you use to generate the path, you may use this points to compute the distance from this points to a given point and get the minimum distance. In the MDN SVG Path Tutorial you can find some examples of complex shapes and how they are made.
Although my calculus answer is still valid, you could just do everything in this bl.ocks example:
var points = [[474,276],[586,393],[378,388],[338,323],[341,138],[547,252],[589,148],[346,227],[365,108],[562,62]];
var width = 960,
height = 500;
var line = d3.svg.line()
.interpolate("cardinal");
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var path = svg.append("path")
.datum(points)
.attr("d", line);
var line = svg.append("line");
var circle = svg.append("circle")
.attr("cx", -10)
.attr("cy", -10)
.attr("r", 3.5);
svg.append("rect")
.attr("width", width)
.attr("height", height)
.on("mousemove", mousemoved);
function mousemoved() {
var m = d3.mouse(this),
p = closestPoint(path.node(), m);
line.attr("x1", p[0]).attr("y1", p[1]).attr("x2", m[0]).attr("y2", m[1]);
circle.attr("cx", p[0]).attr("cy", p[1]);
}
function closestPoint(pathNode, point) {
var pathLength = pathNode.getTotalLength(),
precision = pathLength / pathNode.pathSegList.numberOfItems * .125,
best,
bestLength,
bestDistance = Infinity;
// linear scan for coarse approximation
for (var scan, scanLength = 0, scanDistance; scanLength <= pathLength; scanLength += precision) {
if ((scanDistance = distance2(scan = pathNode.getPointAtLength(scanLength))) < bestDistance) {
best = scan, bestLength = scanLength, bestDistance = scanDistance;
}
}
// binary search for precise estimate
precision *= .5;
while (precision > .5) {
var before,
after,
beforeLength,
afterLength,
beforeDistance,
afterDistance;
if ((beforeLength = bestLength - precision) >= 0 && (beforeDistance = distance2(before = pathNode.getPointAtLength(beforeLength))) < bestDistance) {
best = before, bestLength = beforeLength, bestDistance = beforeDistance;
} else if ((afterLength = bestLength + precision) <= pathLength && (afterDistance = distance2(after = pathNode.getPointAtLength(afterLength))) < bestDistance) {
best = after, bestLength = afterLength, bestDistance = afterDistance;
} else {
precision *= .5;
}
}
best = [best.x, best.y];
best.distance = Math.sqrt(bestDistance);
return best;
function distance2(p) {
var dx = p.x - point[0],
dy = p.y - point[1];
return dx * dx + dy * dy;
}
}
path {
fill: none;
stroke: #000;
stroke-width: 1.5px;
}
line {
fill: none;
stroke: red;
stroke-width: 1.5px;
}
circle {
fill: red;
}
rect {
fill: none;
cursor: crosshair;
pointer-events: all;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
And I spent all that time in the previous answer writing up pretty LaTeX!
I'm not aware of a d3-specific solution to this. But if your path can be represented as a segment of a function, there is hope with a little calculus.
Start with the line length equation .
Plug in your point to x1 and y1.
Replace the remaining y with the function representing your path.
Simplify, then calculate the derivative .
Set to 0 and solve. Hopefully one x value will be within the bounds of your path endpoints. Apply this x to your path function and you have your point on the path.
A more visual example. There are plenty of considerations to make this happen in JavaScript: what is my function? What is the fastest way to take a derivative of the above? These are specific to your situation.

Resources