javascriptmathd3.jssvg-animate

How can I simulate firefox animation behaviour on chrome when using calcMode="linear"?


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


Solution

  • 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>