Eliminate sudden additions/deletions in D3 line chart transition

回眸只為那壹抹淺笑 提交于 2019-12-06 00:01:08

I faced a similar problem recently, and solved it using a custom interpolator for paths:

// Add path interpolator to d3
d3.interpolators.push(function(a, b) {
  var isPath, isArea, interpolator, ac, bc, an, bn;

  // Create a new array of a given length and fill it with the given value
  function fill(value, length) {
    return d3.range(length)
      .map(function() {
        return value;
      });
  }

  // Extract an array of coordinates from the path string
  function extractCoordinates(path) {
    return path.substr(1, path.length - (isArea ? 2 : 1)).split('L');
  }

  // Create a path from an array of coordinates
  function makePath(coordinates) {
    return 'M' + coordinates.join('L') + (isArea ? 'Z' : '');
  }

  // Buffer the smaller path with coordinates at the same position
  function bufferPath(p1, p2) {
    var d = p2.length - p1.length;

    // Paths created by d3.svg.area() wrap around such that the 'end'
    // of the path is in the middle of the list of coordinates
    if (isArea) {
      return fill(p1[0], d/2).concat(p1, fill(p1[p1.length - 1], d/2));
    } else {
      return fill(p1[0], d).concat(p1);
    }
  }

  // Regex for matching the 'd' attribute of SVG paths
  isPath = /M-?\d*\.?\d*,-?\d*\.?\d*(L-?\d*\.?\d*,-?\d*\.?\d*)*Z?/;

  if (isPath.test(a) && isPath.test(b)) {
    // A path is considered an area if it closes itself, indicated by a trailing 'Z'
    isArea = a[a.length - 1] === 'Z';
    ac = extractCoordinates(a);
    bc = extractCoordinates(b);
    an = ac.length;
    bn = bc.length;

    // Buffer the ending path if it is smaller than the first
    if (an > bn) {
      bc = bufferPath(bc, ac);
    }

    // Or, buffer the starting path if the reverse is true
    if (bn > an) {
      ac = bufferPath(ac, bc);
    }

    // Create an interpolater with the buffered paths (if both paths are of the same length,
    // the function will end up being the default string interpolator)
    interpolator = d3.interpolateString(bn > an ? makePath(ac) : a, an > bn ? makePath(bc) : b);

    // If the ending value changed, make sure the final interpolated value is correct
    return bn > an ? interpolator : function(t) {
      return t === 1 ? b : interpolator(t);
    };
  }
});

Here's what the original gist looks like with the new interpolator: http://bl.ocks.org/4535474.

Its approach is to 'buffer' the smaller dataset's path by inserting zero-length line segments at the beginning. The effect is that new segments expand out of a single point at the start of the line, and unused segments similarly collapse down to a single point.

Transitioning between datasets of different sizes (apparently) isn't a common problem, and doesn't have a universal solution. Because I was visualizing time-series data and transitioning between daily/weekly/monthly intervals, I needed the segments towards the end of the path to maintain visual continuity. I can imagine a case in which you'd want to do the same for the beginning of the path, or perhaps expand/contract the path by uniformly buffering segments throughout. Either way the same approach will work.

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!