Search code examples
animationd3.jslinegeometryclip-path

Clip path for animated line and circle in d3 js


How to clip animated line and circle along with the path?

For this example, I have tried to clip the circle and points also.

I have added the following code:

svg.append("defs")
            .append("clipPath")
            .attr("id", "clip")
            .append("rect")
            .attr("width", 960/2)
            .attr("height", 500/2);

And added the following part where the circle and path is defined

.attr('clip-path', 'url(#clip)')

The clipping is working fine for path, but it works differently for the circle and line.

Demo1 and Demo2 is here.

How can I add the clip-path, that it shows the circle & line only on the visible/clipped part of the path?

N.B. Need to apply clipping for multiple circles and lines moving along multiple path.


Solution

  • It is because you are using a transform on the circle: the circle's clip path is also transformed, meaning that it is relative the circle. As the circle moves, the clip path moves. And as the circle is centered at 0,0 relative to its transform (the transform changes as the circle moves, not the centering attributes), the clip path cuts it into a quarter circle (as it passes through 0,0 as well).

    One solution is to use cx and cy attributes to position the circles. A quicker option is to append path, line, circle and points to a g and clip the g, clipping all the children in the process:

    var g = svg.append("g")
      .attr("clip-path", "url(#clip)")
    
    var path = g.append("path")
        .data([points])
        .attr("d", d3.svg.line()
        .tension(0) // Catmull–Rom
        .interpolate("cardinal-closed"));
    
     g.selectAll(".point")
        .data(points)
      .enter()
        .append("circle")
        .attr("r", 4)
        .attr("transform", function(d) { return "translate(" + d + ")"; });
    
    circle = g.append("circle")
        .attr("r", 13)
        .attr("transform", "translate(" + points[0] + ")");
    
    line = g.append("line")
        .attr("y1", -50)
        .attr("y2", 50)
        .attr("class", "x-hover-line hover-line")
        .attr("transform", "translate(" + points[0] + ")");
    

    Updated code:

    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);
    
    
    
    svg.append("defs")
                .append("clipPath")
                .attr("id", "clip")
                .append("rect")
                .attr("width", 960/2)
                .attr("height", 500/2);
                
       
    
    var g = svg.append("g")
      .attr("clip-path", "url(#clip)")
      
      
    var path = g.append("path")
        .data([points])
        .attr("d", d3.svg.line()
        .tension(0) // Catmull–Rom
        .interpolate("cardinal-closed"));
      
    
    g.selectAll(".point")
        .data(points)
      .enter()
        .append("circle")
        .attr("r", 4)
        .attr("transform", function(d) { return "translate(" + d + ")"; });
    
    circle = g.append("circle")
        .attr("r", 13)
       
        .attr("transform", "translate(" + points[0] + ")");
    
    line = g.append("line")
        .attr("y1", -50)
        .attr("y2", 50)
        .attr("class", "x-hover-line hover-line")
        .attr("transform", "translate(" + points[0] + ")");
    
    
    
    transition();
    
    function transition() {
      circle.transition()
          .duration(10000)
          .attrTween("transform", translateAlong(path.node()))
          .each("end", transition);
    
      line.transition()
          .duration(10000)
          .attrTween("transform", translateAlong(path.node()))
          .each("end", transition);
    }
    
    // Returns an attrTween for translating along the specified path element.
    function translateAlong(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 + ")";
        };
      };
    }
    path {
      fill: none;
      stroke: #000;
      stroke-width: 3px;
    }
    
    circle {
      fill: steelblue;
      stroke: #fff;
      stroke-width: 3px;
    }
    
    .hover-line {
        /*stroke: #6F257F;*/
        stroke: #140917;
        stroke-width: 2.5px;
        /*stroke-dasharray: 3,3;*/
    }
    
    .hover-text {
        font-size: 22px;
        font-weight: bold;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.8/d3.min.js"></script>

    Or see updated fiddle.

    If you don't want to clip one of those, you can append them as you are (or use a different parent g). See this fiddle, the line will be visible everywhere.