I have two charts, the first one plots two functions, predators and prey. Each containing a circle with an animateMotion that goes along the function path.
The other chart is a phase curve generated by the functions. There is also a circle with an animateMotion that goes along.
Markers on the first graph should have constant speed with respect to X axis. The marker on the second graph should follow along the Y values of the first ones.
On firefox, using calcMode="linear"
makes it work:
firefox linear animation.
On chrome, calcMode="linear"
behaves the same way as calcMode="paced"
, so speed is constant along the curve:
chrome "linear" animation.
Including a somewhat 'minimal' piece of code, I kept the axis on so it's easier to understand what's going on.
<script src="https://d3js.org/d3.v4.js"></script>
<div style="min-width: 100px; max-width: 450px; width:100%">
<div id="prey_predator_chart" style="width:100%;">
</div>
<div id="prey_predator_phase_chart" style="width:100%;">
</div>
</div>
<script>
let prey_predator = {
prey_color: "blue",
predator_color: "green",
phase_curve_color: "red",
draw_graph: function() {
// set the dimensions and margins of the graph
var margin = {top: 0, right: 40, bottom: 40, left: 40},
width = 450 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var total_width = width + margin.left + margin.right;
var total_height = height + margin.top + margin.bottom;
// line graph
var svg_pop = d3.select("#prey_predator_chart")
.append("svg")
.attr("viewBox", "0 0 " + total_width + " " + total_height)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// phase graph
svg_pop_phase = d3.select("#prey_predator_phase_chart")
.append("svg")
.attr("viewBox", "0 0 " + total_width + " " + total_height)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var xDomain = [0,40];
var yDomain = [0,30];
var prey_c = {i_density: 10, growth: 1.1, death: 0.4}
var predator_c = {i_density: 10, growth: 0.1, death: 0.4}
var yScale = d3.scaleLinear().range([height,0]).domain(yDomain);
var xScale = d3.scaleLinear().range([0,width]).domain(xDomain);
var yScale_phase = d3.scaleLinear().range([height,0]).domain(yDomain);
var xScale_phase = d3.scaleLinear().range([0,width]).domain(yDomain);
var eps = 0.0005
var x_space = d3.range(xDomain[0], xDomain[1], eps)
var prey_growth = function(current_prey, current_predator, eps) {
return prey_c.growth*eps*current_prey - prey_c.death*current_prey*eps*current_predator
}
var predator_growth = function(current_prey, current_predator, eps) {
return predator_c.growth*current_prey*eps*current_predator - predator_c.death*eps*current_predator
}
var preys = []
var predators = []
preys = [prey_c.i_density]
predators = [predator_c.i_density]
x_space.forEach((_, i) => {
preys.push(preys[i] + prey_growth(preys[i], predators[i], eps))
predators.push(predators[i] + predator_growth(preys[i], predators[i], eps))
});
var c_preys = d3.line()
.x(function(i) { return xScale(x_space[i]) })
.y(function(i) { return yScale(preys[i]) })
var c_predators = d3.line()
.x(function(i) { return xScale(x_space[i]) })
.y(function(i) { return yScale(predators[i]) })
var c_phase = d3.line()
.x(function(i) {return xScale_phase(preys[i])})
.y(function(i) {return yScale_phase(predators[i])})
predators_curve = svg_pop.append('path')
.attr('stroke', this.predator_color)
.attr('fill', 'none')
.attr('stroke-width', 2).attr('d', c_predators(d3.range(0, x_space.length, 1)));
predators_marker = svg_pop.append('circle')
.attr('r', 3)
.attr('stroke', this.predator_color)
predators_marker.append('animateMotion')
.attr('repeatCount', 'indefinite')
.attr('fill', 'freeze')
.attr('calcMode','linear')
.attr('dur', '10s')
.attr('path', c_predators(d3.range(0, x_space.length, 1)));
preys_curve = svg_pop.append('path')
.attr('stroke', this.prey_color)
.attr('fill', 'none')
.attr('stroke-width', 1).attr('d', c_preys(d3.range(0, x_space.length, 1)));
preys_marker = svg_pop.append('circle')
.attr('r', 3)
.attr('stroke', this.prey_color)
preys_marker.append('animateMotion')
.attr('repeatCount', 'indefinite')
.attr('fill', 'freeze')
.attr('calcMode','linear')
.attr('dur', '10s')
.attr('path', c_preys(d3.range(0, x_space.length, 1)));
phase_curve = svg_pop_phase.append('path')
.attr('stroke', this.phase_curve_color)
.attr('stroke-width', 1)
.attr('fill', 'none').attr('d', c_phase(d3.range(0, x_space.length, 1)));
phase_marker = svg_pop_phase.append('circle')
.attr('r', 3)
.attr('stroke', this.phase_curve_color)
phase_marker.append('animateMotion')
.attr('repeatCount', 'indefinite')
.attr('fill', 'freeze')
.attr('calcMode','linear')
.attr('dur', '10s')
.attr('path', c_phase(d3.range(0, x_space.length, 1)));
bottomAxis = svg_pop.append("g").attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale));
bottomAxis.append("text")
.attr("class", "axis-title")
.attr("y", 25)
.attr("dy", ".71em")
.attr("x", (width+margin.left)/2)
.style("text-anchor", "end")
.attr("fill", "black")
.text("Tiempo");
leftAxis = svg_pop.append("g")
.call(d3.axisLeft(yScale));
leftAxis.append("text")
.attr("class", "axis-title")
.attr("transform", "rotate(-90)")
.attr("y", -30)
.attr("dy", ".71em")
.attr("x", -(height-margin.bottom)/2)
.style("text-anchor", "end")
.attr("fill", "black")
.text("Densidad");
bottomAxis_phase = svg_pop_phase.append("g").attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale_phase));
bottomAxis_phase.append("text")
.attr("class", "axis-title")
.attr("y", 25)
.attr("dy", ".71em")
.attr("x", (width+margin.left)/2)
.style("text-anchor", "end")
.attr("fill", "black")
.text("Densidad presa");
leftAxis_phase = svg_pop_phase.append("g")
.call(d3.axisLeft(yScale_phase));
leftAxis_phase.append("text")
.attr("class", "axis-title")
.attr("transform", "rotate(-90)")
.attr("y", -35)
.attr("dy", ".71em")
.attr("x", -(height-margin.bottom)/2)
.style("text-anchor", "end")
.attr("fill", "black")
.text("Densidad predador");
diag_phase = svg_pop_phase.append('line')
.attr('stroke', 'black')
.attr('stroke-width', 1)
.attr('stroke-dasharray', '5,5')
.attr('x1', xScale_phase(yDomain[0]))
.attr('y1', yScale_phase(yDomain[0]))
.attr('x2', xScale_phase(yDomain[1]))
.attr('y2', yScale_phase(yDomain[1]))
}
}
prey_predator.draw_graph();
</script>
Did a manual synchronization using keyTimes
and keyPoints
, and this does work for the first chart, but I don't know how to calculate the values for the second one.
This also is a bit cumbersome given how firefox just works.
get_curve_points = function(curve, n) {
var points = []
for (var i = 0; i < n; i++) {
points.push(curve.node().getPointAtLength(i * curve.node().getTotalLength() / n))
}
if (points.length > n) {
points.pop()
}
return points;
},
n = 50
points = get_curve_points(preys_curve, n)
xs = points.map(p => p.x)
var min_x = d3.min(xs);
var max_x = d3.max(xs);
xs = xs.map(x => (x - min_x) / (max_x - min_x));
keyTimes = xs.reduce((acc, x) => acc + x + ';', '') + "1"
keyPoints = d3.range(0,1,1/n).reduce((acc, x) => acc + x + ';', '') + "1"
preys_marker.append('animateMotion')
.attr('repeatCount', 'indefinite')
.attr('fill', 'freeze')
.attr('calcMode','linear')
.attr('dur', '10s')
.attr('keyTimes', keyTimes)
.attr('keyPoints', keyPoints)
.attr('path', c_preys(d3.range(0, x_space.length, 1)));
predators_curve = svg_pop.append('path')
.attr('stroke', this.predator_color)
.attr('fill', 'none')
.attr('stroke-width', 2).attr('d', c_predators(d3.range(0, x_space.length, 1)));
predators_marker = svg_pop.append('circle')
.attr('r', 3)
.attr('stroke', this.predator_color)
points = get_curve_points(predators_curve, n)
xs = points.map(p => p.x)
var min_x = d3.min(xs);
var max_x = d3.max(xs);
xs = xs.map(x => (x - min_x) / (max_x - min_x));
keyTimes = xs.reduce((acc, x) => acc + x + ';', '') + "1"
keyPoints = d3.range(0,1,1/n).reduce((acc, x) => acc + x + ';', '') + "1"
predators_marker.append('animateMotion')
.attr('repeatCount', 'indefinite')
.attr('fill', 'freeze')
.attr('calcMode','linear')
.attr('dur', '10s')
.attr('keyTimes', keyTimes)
.attr('keyPoints', keyPoints)
.attr('path', c_predators(d3.range(0, x_space.length, 1)));
phase_curve = svg_pop_phase.append('path')
.attr('stroke', this.phase_curve_color)
.attr('stroke-width', 1)
.attr('fill', 'none').attr('d', c_phase(d3.range(0, x_space.length, 1)));
phase_marker = svg_pop_phase.append('circle')
.attr('r', 3)
.attr('stroke', this.phase_curve_color)
phase_marker.append('animateMotion')
.attr('repeatCount', 'indefinite')
.attr('fill', 'freeze')
.attr('calcMode','linear')
.attr('dur', '10s')
Modified Mark's solution on the comment section to generate the animation based on current time.
This way I can synchronize every graph at the same time by scaling the current time against the x axis.
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
<script src="https://d3js.org/d3.v4.js"></script>
<div style="min-width: 100px; max-width: 450px; width:100%">
<div id="prey_predator_chart" style="width:100%;">
</div>
<div id="prey_predator_phase_chart" style="width:100%;">
</div>
</div>
<script>
let prey_predator = {
prey_color: 'blue',
predator_color: 'green',
phase_curve_color: 'red',
draw_graph: function () {
// set the dimensions and margins of the graph
var margin = { top: 0, right: 40, bottom: 40, left: 40 },
width = 450 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var total_width = width + margin.left + margin.right;
var total_height = height + margin.top + margin.bottom;
// line graph
var svg_pop = d3
.select('#prey_predator_chart')
.append('svg')
.attr('viewBox', '0 0 ' + total_width + ' ' + total_height)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
// phase graph
svg_pop_phase = d3
.select('#prey_predator_phase_chart')
.append('svg')
.attr('viewBox', '0 0 ' + total_width + ' ' + total_height)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
var xDomain = [0, 40];
var yDomain = [0, 30];
var prey_c = { i_density: 10, growth: 1.1, death: 0.4 };
var predator_c = { i_density: 10, growth: 0.1, death: 0.4 };
var yScale = d3.scaleLinear().range([height, 0]).domain(yDomain);
var xScale = d3.scaleLinear().range([0, width]).domain(xDomain);
var yScale_phase = d3.scaleLinear().range([height, 0]).domain(yDomain);
var xScale_phase = d3.scaleLinear().range([0, width]).domain(yDomain);
var eps = 0.0005;
var x_space = d3.range(xDomain[0], xDomain[1], eps);
var prey_growth = function (current_prey, current_predator, eps) {
return (
prey_c.growth * eps * current_prey -
prey_c.death * current_prey * eps * current_predator
);
};
var predator_growth = function (current_prey, current_predator, eps) {
return (
predator_c.growth * current_prey * eps * current_predator -
predator_c.death * eps * current_predator
);
};
var preys = [];
var predators = [];
preys = [prey_c.i_density];
predators = [predator_c.i_density];
x_space.forEach((_, i) => {
preys.push(preys[i] + prey_growth(preys[i], predators[i], eps));
predators.push(
predators[i] + predator_growth(preys[i], predators[i], eps)
);
});
var c_preys = d3
.line()
.x(function (i) {
return xScale(x_space[i]);
})
.y(function (i) {
return yScale(preys[i]);
});
var c_predators = d3
.line()
.x(function (i) {
return xScale(x_space[i]);
})
.y(function (i) {
return yScale(predators[i]);
});
var c_phase = d3
.line()
.x(function (i) {
return xScale_phase(preys[i]);
})
.y(function (i) {
return yScale_phase(predators[i]);
});
predators_curve = svg_pop
.append('path')
.attr('stroke', this.predator_color)
.attr('fill', 'none')
.attr('stroke-width', 2)
.attr('d', c_predators(d3.range(0, x_space.length, 1)));
predators_marker = svg_pop
.append('circle')
.attr('r', 3)
.attr('stroke', this.predator_color);
preys_curve = svg_pop
.append('path')
.attr('stroke', this.prey_color)
.attr('fill', 'none')
.attr('stroke-width', 1)
.attr('d', c_preys(d3.range(0, x_space.length, 1)));
preys_marker = svg_pop
.append('circle')
.attr('r', 3)
.attr('stroke', this.prey_color);
phase_curve = svg_pop_phase
.append('path')
.attr('stroke', this.phase_curve_color)
.attr('stroke-width', 1)
.attr('fill', 'none')
.attr('d', c_phase(d3.range(0, x_space.length, 1)));
phase_marker = svg_pop_phase
.append('circle')
.attr('r', 3)
.attr('stroke', this.phase_curve_color);
const dur = 10000
svg_pop
.transition()
.ease(d3.easeLinear)
.duration(dur)
.tween(null, function () {
return function (t) {
const i = Math.round(t * x_space.length - 1);
const x = xScale(x_space[i]);
predators_marker.attr('cx', x);
predators_marker.attr('cy', yScale(predators[i]));
preys_marker.attr('cx', x);
preys_marker.attr('cy', yScale(preys[i]));
phase_marker.attr('cx', xScale_phase(preys[i]));
phase_marker.attr('cy', yScale_phase(predators[i]));
};
});
bottomAxis = svg_pop
.append('g')
.attr('transform', 'translate(0,' + height + ')')
.call(d3.axisBottom(xScale));
bottomAxis
.append('text')
.attr('class', 'axis-title')
.attr('y', 25)
.attr('dy', '.71em')
.attr('x', (width + margin.left) / 2)
.style('text-anchor', 'end')
.attr('fill', 'black')
.text('Tiempo');
leftAxis = svg_pop.append('g').call(d3.axisLeft(yScale));
leftAxis
.append('text')
.attr('class', 'axis-title')
.attr('transform', 'rotate(-90)')
.attr('y', -30)
.attr('dy', '.71em')
.attr('x', -(height - margin.bottom) / 2)
.style('text-anchor', 'end')
.attr('fill', 'black')
.text('Densidad');
bottomAxis_phase = svg_pop_phase
.append('g')
.attr('transform', 'translate(0,' + height + ')')
.call(d3.axisBottom(xScale_phase));
bottomAxis_phase
.append('text')
.attr('class', 'axis-title')
.attr('y', 25)
.attr('dy', '.71em')
.attr('x', (width + margin.left) / 2)
.style('text-anchor', 'end')
.attr('fill', 'black')
.text('Densidad presa');
leftAxis_phase = svg_pop_phase
.append('g')
.call(d3.axisLeft(yScale_phase));
leftAxis_phase
.append('text')
.attr('class', 'axis-title')
.attr('transform', 'rotate(-90)')
.attr('y', -35)
.attr('dy', '.71em')
.attr('x', -(height - margin.bottom) / 2)
.style('text-anchor', 'end')
.attr('fill', 'black')
.text('Densidad predador');
diag_phase = svg_pop_phase
.append('line')
.attr('stroke', 'black')
.attr('stroke-width', 1)
.attr('stroke-dasharray', '5,5')
.attr('x1', xScale_phase(yDomain[0]))
.attr('y1', yScale_phase(yDomain[0]))
.attr('x2', xScale_phase(yDomain[1]))
.attr('y2', yScale_phase(yDomain[1]));
},
};
prey_predator.draw_graph();
</script>