Interpolating along consecutive paths with D3.js - animation

I'm adapting Mike Bostock's point along path interpolation model to accept an array of n individual paths and interpolate along each consecutively. Being relatively new to D3 the code below shows as far as I've got, which is to run the point interpolation for both paths concurrently. Now I'm a bit stuck over how to restructure this to make the process consecutive (with just one moving object). Really I need to be able to stop between paths to listen for a mouseclick, but I can figure out that code once the structure is there. Most grateful for assistance.
Here's the jsfiddle.
Code for posterity:
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<style>
path {
fill: none;
stroke: #000;
stroke-width: 3px;
}
circle {
stroke: #fff;
stroke-width: 3px;
}
</style>
<script type="text/javascript" src="http://d3js.org/d3.v3.js"></script><script>
var pathdata = [
[[240, 100],
[290, 200],
[340, 50]],
[[340, 50],
[90, 150],
[140, 50],
[190, 200]]
];
var svg = d3.select("body").append("svg")
.attr("width", 960)
.attr("height", 500);
var paths = svg.selectAll("path")
.data(pathdata)
.enter()
.append("path")
.attr("d", d3.svg.line())
.attr("id",function(d, i) { return "path" + i });
// plot path vertices
svg.selectAll(".point")
.data([].concat.apply([], pathdata))
.enter().append("circle")
.attr("r", 5)
.attr("fill", "red")
.attr("transform", function(d) { return "translate(" + d + ")"; });
// interpolate along path0
var circle = svg.append("circle")
.attr("r", 10)
.attr("fill", "steelblue")
.attr("transform", "translate(" + pathdata[0][1] + ")")
.transition()
.duration(4000)
.attrTween("transform", translateAlong(d3.select("#path0")[0][0]));
// interpolate along path1
var circle = svg.append("circle")
.attr("r", 10)
.attr("fill", "steelblue")
.attr("transform", "translate(" + pathdata[1][1] + ")")
.transition()
.duration(4000)
.attrTween("transform", translateAlong(d3.select("#path1")[0][0]));
function translateAlong(path) {
console.log(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 + ")";
};
};
}
</script>
</body>
</html>
I'm also wondering if it might be better to format the input data along one of the following lines?
// 3rd field for path id
var points_alt1 = [
[240, 100, 0],
[290, 200, 0],
[340, 50, 0],
[340, 50, 1],
[90, 150, 1],
[140, 50, 1],
[190, 200, 1]
]
or..
// 3rd field for interpolation end-points
var points_alt2 = [
[240, 100, 0],
[290, 200, 0],
[340, 50, 1],
[340, 50, 0],
[90, 150, 0],
[140, 50, 0],
[190, 200, 1]
]

Create a function that takes as params a d3 selection of paths and an integer index of the path (within the selection) along which you want to animate. This function finds the appropriate path within that selection, starts up a transition of a circle along it, and subscribes to the 'end' event of the transition, at which point it triggers the next animation.
Here's the working fiddle
function animateSinglePath(selection, indexOfAnimated) {
indexOfAnimated = indexOfAnimated || 0;
// Create circle if doesn't already exist
// (achived by binding to single element array)
circle = svg.selectAll('.animated-circle').data([null])
circle.enter()
.append("circle")
.attr("class", "animated-circle")
.attr("r", 10)
.attr("fill", "steelblue")
selection.each(function(d, i) {
// find the path we want to animate
if(i == indexOfAnimated) {
// In this context, the "this" object is the DOM
// element of the path whose index, i equals the
// desired indexOfAnimated
path = d3.select(this)
// For style, make it dashed
path.attr('stroke-dasharray', '5, 5')
// Move circle to start pos and begin animation
console.log('Start animation', i)
circle
.attr("transform", "translate(" + d[0] + ")")
.transition()
.duration(2000)
.attrTween("transform", translateAlong(path.node()))
.each('end', function() {
console.log('End animation', i);
// For style, revert stroke to non-dashed
path.attr('stroke-dasharray', '')
// trigger the next animation by calling this
// function again
animateSinglePath(selection, indexOfAnimated + 1);
});
}
});
}
P.S
I wouldn't restructure the data as you proposed, because you need to have distinct SVG <path> elements – one for each "chapter" in the sequence. Having a distinct array for each of path, as you do now, is what enables you to create these <path>s via data() binding.
Depending on what you're trying to achieve, you may even want to further nest each path array, wrapping it with an object {}, to hold meta data about the path:
var pathData = [
{
name: "Crossing the Niemen",
points: [ [240, 100], [290, 200], [340, 50] ]
},
{
name: "March on Vilnius",
points: [ [340, 50], [90, 150], [140, 50], [190, 200] ]
},
{
name: "March on Moscow",
points: [ [190, 200], [70, 180], [30, 30], [350, 160] ]
}
];

Related

D3 transition along segments of path and pause at coordinate values

I would like to be able to click on a circle (coordinate points); bring the marker to the position of the circle and pause at the position of the circle and then resume again along the path.
In addition I would like to activate a circle when marker is paused on them - they are clicked (or their Voronoi cell is clicked). My intention is to have an on click function to an href for the circle coordinates eventually.
I think I need to pass the index of the path coordinates into the translateAlong function instead of the time variables but can't work out how to do this.
I’m not sure if the Voronoi cells are necessary - I tried to add this thinking I could pause my transition and activate my circles with the Voronoi cells. In any case I can’t activate the circle with the Voronoi cell.
I was helped considerably recently on Stackoverflow d3 on click on circle pause and resume transition of marker along line
and I am hoping for assistance again
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>basic_animateBetweenCircles</title>
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
path {
stroke: #848484;
fill: none;
}
circle {
fill: steelblue;
stroke: steelblue;
stroke-width: 3px;
}
.line {
fill: none;
stroke: #FE642E;
stroke-width: 4;
stroke-dasharray: 4px, 8px;
}
.point{
fill:#DF013A;
}
</style>
</head>
<body>
<script>
var width = 960,
height = 500;
var data = [
[480, 200],
[580, 400],
[680, 100],
[780, 300],
[180, 300],
[280, 100],
[380, 400]
];
//check index of path data
for (var i = 0; i < data.length; i++) {
var coordindex = i + " " + data[i];
console.log("Coordindex: " + coordindex);
//return coordindex;
};
var duration = 20000;
var line = d3.line()
.x(function(d) {return (d)[0];})
.y(function(d) {return (d)[1];});
var voronoi = d3.voronoi()
.extent([[0, 0], [width, height]]);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
//path to animate - marker transitions along this path
var path = svg.append("path")
.data([data])
.attr("d", line)
.attr('class', 'line')
.attr("d", function(d) {
return line(d)
});
//voronoi
var voronoiPath = svg.append("g")
.selectAll("path")
.data(voronoi.polygons(data))
.enter().append("path")
.attr("d", polygon)
.on("touchmove mousemove", function() {
d3.select(this)
.style("fill", "purple");
});
//Want to activate circles when marker paused on them / in voronoi cell - intention is to have on click to href
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("class", "point")
.attr("r", 10)
.attr("transform", function(d) { return "translate(" + d + ")"; })
.on('click', function(d, i) {
d3.select(this)
.style("fill", "green");
if (d3.active(this)) {
marker.transition();
setTimeout(function() {
pauseValues.lastTime = pauseValues.currentTime;
//console.log(pauseValues);
}, 100);
} else {
transition();
}
});
var pauseValues = {
lastTime: 0,
currentTime: 0
};
//marker to transition along path
var marker = svg.append("circle")
.attr("r", 19)
.attr("transform", "translate(" + (data[0]) + ")")
.on('click', function(d, i) {
if (d3.active(this)) {
marker.transition();
setTimeout(function() {
pauseValues.lastTime = pauseValues.currentTime;
//console.log(pauseValues);
}, 100);
} else {
transition();
}
});
function transition() {
marker.transition()
.duration(duration - (duration * pauseValues.lastTime))
.attrTween("transform", translateAlong(path.node()))
.on("end", function() {
pauseValues = {
lastTime: 0,
currentTime: 0
};
transition()
});
}
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
t += pauseValues.lastTime;
var p = path.getPointAtLength(t * l);
pauseValues.currentTime = t;
return "translate(" + p.x + "," + p.y + ")";
};
};
}
function polygon(d) {
return "M" + d.join("L") + "Z";
}
</script>
</body>
If you want to pause at points, I would not run one transition across the entire path. Instead, I would break it up into N transitions, moving from point to point. Before starting the circle on it's next leg, you can pause it for a time. To do this, I would just transition along each line segment with a little algebra:
// copy our data
transData = data.slice();
function transition() {
marker.transition()
.ease(d3.easeLinear)
.duration(duration)
.attrTween("transform", function(){
// get our two points
// slope between them
// and intercetp
var p0 = transData.shift(),
p1 = transData[0];
m = (p0[1] - p1[1]) / (p0[0] - p1[0]),
b = p0[1] - (m * p0[0]),
i = d3.interpolateNumber(p0[0], p1[0]);
// move the point along the line
return function(t){
var x = i(t),
y = m*x + b;
return "translate(" + x + "," + y + ")";
}
})
// one line segment is complete
.on("end", function(){
// if no more movements, stop
if (transData.length <= 1) return;
iter++;
// determine if this is a "pause"
setTimeout(transition, pausePoints.indexOf(iter) !== -1 ? pauseTime : 0);
});
Running code, click a dot to start you can pause a multiple points:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>basic_animateBetweenCircles</title>
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
path {
stroke: #848484;
fill: none;
}
circle {
fill: steelblue;
stroke: steelblue;
stroke-width: 3px;
}
.line {
fill: none;
stroke: #FE642E;
stroke-width: 4;
stroke-dasharray: 4px, 8px;
}
.point {
fill: #DF013A;
}
</style>
</head>
<body>
<script>
var width = 960,
height = 500;
var data = [
[480, 200],
[580, 400],
[680, 100],
[780, 300],
[180, 300],
[280, 100],
[380, 400]
];
var duration = 20000/data.length,
pauseTime = 2000;
var line = d3.line()
.x(function(d) {
return (d)[0];
})
.y(function(d) {
return (d)[1];
});
var voronoi = d3.voronoi()
.extent([
[0, 0],
[width, height]
]);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
//path to animate - marker transitions along this path
var path = svg.append("path")
.data([data])
.attr("d", line)
.attr('class', 'line')
.attr("d", function(d) {
return line(d)
});
//voronoi
var voronoiPath = svg.append("g")
.selectAll("path")
.data(voronoi.polygons(data))
.enter().append("path")
.attr("d", polygon);
//Want to activate circles when marker paused on them / in voronoi cell - intention is to have on click to href
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("class", "point")
.attr("r", 10)
.attr("transform", function(d) {
return "translate(" + d + ")";
})
.on('click', function(d, i) {
d3.select(this)
.style("fill", "green");
pausePoints.push(i);
if (pausePoints.length === 1)
transition();
});
//marker to transition along path
var marker = svg.append("circle")
.attr("r", 19)
.attr("transform", "translate(" + (data[0]) + ")");
var pausePoints = [],
iter = 0,
transData = data.slice();
function transition() {
marker.transition()
.ease(d3.easeLinear)
.duration(duration)
.attrTween("transform", function(){
var p0 = transData.shift(),
p1 = transData[0];
m = (p0[1] - p1[1]) / (p0[0] - p1[0]),
b = p0[1] - (m * p0[0]),
i = d3.interpolateNumber(p0[0], p1[0]);
return function(t){
var x = i(t),
y = m*x + b;
return "translate(" + x + "," + y + ")";
}
})
.on("end", function(){
if (transData.length <= 1) return;
iter++;
setTimeout(transition, pausePoints.indexOf(iter) !== -1 ? pauseTime : 0);
});
}
function polygon(d) {
return "M" + d.join("L") + "Z";
}
</script>
</body>

Multidimensional array for D3

The following code works for D3 V3, but not D3 V4, how to rectify it?
<!DOCTYPE html>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.8/d3.min.js" type="text/JavaScript"></script>
<!--script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.9/d3.js" type="text/JavaScript"></script-->
<body></body>
<script>
var dataset = [[1,3,3,5,6,7],[3,5,8,3,2,6],[9,0,6,3,6,3],[3,4,4,5,6,8],[3,4,5,2,1,8]];
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 500);
svg.append("g")
.selectAll("g")
.data(dataset)
.enter()
.append("g") //removing
.selectAll("text") // these
.data( function(d,i,j) { return d; } ) //lines
.enter() //text displays normally
.append("text")
.text( function(d,i,j) { return d; } )
.attr("x", function(d,i,j) { console.log(j);return (i * 20) + 40; })
.attr("y", function(d,i,j) { return (j * 20) + 40; })
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
</script>
The explanation here is simple, and it was the subject of this answer of mine: the third argument has changed from D3 v3 to D3 v4.
This is the problematic line:
.attr("y", function(d,i,j) { return (j * 20) + 40; })
//the 3rd argum. -------^ ^--- using the 3rd argument
In D3 v3, the third argument is the index of the parent group. However, in v4, the third argument is the current group. The changelog is clear:
The arguments passed to callback functions has changed slightly in 4.0 to be more consistent. The standard arguments are the element’s datum (d), the element’s index (i), and the element’s group (nodes), with this as the element.
But there is a way to achieve what you want in v4! The same changelog says:
The slight exception to this convention is selection.data, which is evaluated for each group rather than each element; it is passed the group’s parent datum (d), the group index (i), and the selection’s parents (parents), with this as the group’s parent. (emphasis mine)
Thus, we can use the data() method to get the group's index.
Here, I'm using a local variable...
var local = d3.local();
... to get the index inside data():
.data( function(d,i) {
local.set(this, i);
return d;
})
Then, using it to set the y position:
.attr("y", function(d) {
return (local.get(this) * 20) + 40;
})
Here is your code with that change:
var dataset = [
[1, 3, 3, 5, 6, 7],
[3, 5, 8, 3, 2, 6],
[9, 0, 6, 3, 6, 3],
[3, 4, 4, 5, 6, 8],
[3, 4, 5, 2, 1, 8]
];
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 500);
var local = d3.local();
svg.append("g")
.selectAll("g")
.data(dataset)
.enter()
.append("g")
.selectAll("text")
.data(function(d, i) {
local.set(this, i)
return d;
})
.enter()
.append("text")
.text(function(d, i, j) {
return d;
})
.attr("x", function(d, i, j) {
return (i * 20) + 40;
})
.attr("y", function(d) {
return (local.get(this) * 20) + 40;
})
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
<script src="https://d3js.org/d3.v4.min.js"></script>

Get location along a path or line upon click in d3

As stated in the title, I was wondering how to catch the location along a path or line of a mouse click - is it simply using the mousedown event and then calculating the d3.event.pageX, d3.event.pageY as it matches the line? Is there a better way?
Use d3.mouse function.
d3.mouse(container)
Returns the x and y coordinates of the current d3.event, relative to
the specified container. The container may be an HTML or SVG container
element, such as an svg:g or svg:svg. The coordinates are returned as
a two-element array [x, y].
Reference: https://github.com/mbostock/d3/wiki/Selections#d3_mouse
Example: (Try clicking on the path)
var points = [
[480, 200],
[580, 400],
[680, 100],
[780, 300],
[180, 300],
[280, 100],
[380, 400]
];
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"));
var circle = svg.append("circle")
.attr("r", 5)
.style("fill", "red")
.attr("cx", points[0][0])
.attr("cy", points[0][1]);
path.on("click", function() {
circle.attr("cx", d3.mouse(this)[0])
.attr("cy", d3.mouse(this)[1]);
});
path {
fill: none;
stroke: black;
stroke-width: 5;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

stacking axes horizontally on top of each other

Thisis an example by Mike Bostock of a "simple" hive graph (as he refers to it in this article ). It has three "axis" created by this code
svg.selectAll(".axis")
.data(d3.range(3))
.enter().append("line")
.attr("class", "axis")
.attr("transform", function(d) { return "rotate(" + degrees(angle(d)) + ")"; })
.attr("x1", radius.range()[0])
.attr("x2", radius.range()[1]);
As you can see from the first link, the three "axes" form a circle, which seems to be accomplished by the rotation in the "transform" of the code above and use of these angle and degrees functions
var angle = d3.scale.ordinal().domain(d3.range(4)).rangePoints([0, 2 * Math.PI]),
function degrees(radians) {
return radians / Math.PI * 180 - 90;
}
Question: if there were only two "axes", how would it be possible (using a "translate") to stack the "axes" on top of each other (i.e. as two horizontal lines parallel to each other?
In my attempt to do this, I tried to remove the rotation of the "axis" and then to space them vertically. To stop the rotation,I removed the call to "degrees" like this
.attr("transform", function(d) { return "rotate(" + angle(d) + ")"; })
and I also set the range of the angles to be 0,0
d3.scale.ordinal().domain(["one", "two"]).range([0,0]);
then , to space the axes, I included a "translate" like this
.attr("transform", function(d) {return "translate(" + width /2 + "," + height/d + ")"});
The result is that there is one visible horizontal axis, and it seems the other one exists but is only detectable when I run the mouse over it ( and the nodes and lines haven't been repositioned)
Not sure if this is what you are after but two "axis" stacked vertically can be achieved with:
var angle = d3.scale.ordinal()
.domain(d3.range(3)) //<-- only calculate angles for 2 [-90, 90]
.rangePoints([0, 2 * Math.PI]),
...
svg.selectAll(".axis")
.data(d3.range(2)) //<-- 2 lines
EDITS
What are you are describing is not really a hive plot and attempting to re-purpose the layout is probably more trouble then it's worth. If you just want linked points on a line, here's an off-the-cuff implementation:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.link {
fill: none;
stroke-width: 1.5px;
}
.axis, .node {
stroke: #000;
stroke-width: 1.5px;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="d3.hive.min.js"></script>
<script>
var width = 500,
height = 500;
var lineSep = 200,
lineLen = 400,
color = d3.scale.category10().domain(d3.range(20)),
margin = [50,50];
var nodes = [
{x: 0, y: .1},
{x: 0, y: .9},
{x: 0, y: .2},
{x: 1, y: .3},
{x: 1, y: .1},
{x: 1, y: .8},
{x: 1, y: .4},
{x: 1, y: .6},
{x: 1, y: .2},
{x: 1, y: .7},
{x: 1, y: .8}
];
var links = [
{source: nodes[0], target: nodes[3]},
{source: nodes[1], target: nodes[3]},
{source: nodes[2], target: nodes[4]},
{source: nodes[2], target: nodes[9]},
{source: nodes[3], target: nodes[0]},
{source: nodes[4], target: nodes[0]},
{source: nodes[5], target: nodes[1]}
];
var nodeNest = d3.nest().key(function(d){ return d.x }).entries(nodes);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + margin[0] + "," + margin[1] + ")");
var axis = svg.selectAll(".axis")
.data(nodeNest);
var g = axis
.enter().append("g")
.attr("class", "axis")
.attr("transform", function(d,i) {
var t = "translate(0," + (i * lineSep) + ")";
return t;
})
.append("line")
.attr("x1", 0)
.attr("x2", lineLen);
axis.selectAll(".node")
.data(function(d){
d.values.forEach(function(q){
q.len = d.values.length;
})
return d.values;
})
.enter().append("circle")
.attr("class", "node")
.attr("cx", function(d, i, j) {
d.cx = ((i + 0.5) * (lineLen / d.len));
d.cy = (j * lineSep);
return d.cx;
})
.attr("r", 5)
.style("fill", function(d) { return color(d.x); });
svg.selectAll(".link")
.data(links)
.enter().append("path")
.attr("class", "link")
.attr("d", function(d){
console.log(d);
var p = "";
p += "M" + d.source.cx + "," + d.source.cy;
p += "Q" + "0," + ((margin[1] / 2) + (lineSep/2));
p += " " + d.target.cx + "," + d.target.cy;
return p;
})
.style("stroke", function(d) {
return color(d.source.x);
});
function degrees(radians) {
return radians / Math.PI * 180 - 90;
}
</script>

d3.js. How to animate throughout all data set from start to end?

I draw a circle and want to run it transition from first to the last point of data set. But can't understand how to do it. Code available here. How can i do it? What is the best practice for this kind of animation?
var data = [[{
x: 10,
y: 10,
r: 10,
color: "red"
}, {
x: 70,
y: 70,
r: 15,
color: "green"
}, {
x: 130,
y: 130,
r: 20,
color: "blue"
}]];
function setUp() {
this.attr("cx", function(d, i) {
return d[i].x;
}).attr("cy", function(d, i) {
return d[i].y;
}).attr("r", function(d, i) {
return d[i].r;
}).attr("fill", function(d, i) {
return d[i].color;
});
}
var canvas = d3.select("body")
.append("svg")
.attr("width", 300)
.attr("height", 300);
canvas.append("rect")
.attr("width", 300)
.attr("height", 300)
.attr("fill", "lightblue");
var circles = canvas.selectAll("circle")
.data(data)
.enter()
.append("circle")
.call(setUp);
Are you looking to do something like this?
var data = [[{
x: 10,
y: 10,
r: 10,
color: "red"
}], [{
x: 70,
y: 70,
r: 15,
color: "green"
}], [{
x: 130,
y: 130,
r: 20,
color: "blue"
}]];
...
var circles = canvas.selectAll("circle")
.data(data[0]);
circles
.enter()
.append("circle")
.call(setUp);
circles
.data(data[1])
.transition()
.duration(2000)
.call(setUp)
.each("end",function(){
circles
.data(data[2])
.transition()
.duration(2000)
.call(setUp);
});
Edits For Comment
If you have a variable number of points, this is a great place to use a recursive function:
// first point
var circles = canvas.selectAll("circle")
.data([data[0]]);
circles
.enter()
.append("circle")
.call(setUp);
// rest of points...
var pnt = 1;
// kick off recursion
doTransition();
function doTransition(){
circles
.data([data[pnt]])
.transition()
.duration(2000)
.call(setUp)
.each("end",function(){
pnt++;
if (pnt >= data.length){
return;
}
doTransition();
});
}
Updated example.

Resources