Search code examples
d3.jspathjquery-animategeojsonvoronoi

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>

Solution

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