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