从带线的数据转换日期和时间

Date and time transition from data with line

我使用 D3 V5 创建了这个图表。另外,我在 fiddle 上附上了示例数据,您可以 view by clicking here.

我已经包含了 tick 函数代码块,它为 x 和 y 尺度附加了新的域,并且 line/data 在向左滑动的路径上:

当 tick 函数执行时,线条会重新构建,使其看起来像是在弹跳。
重建线路的时候怎么能流畅,一点弹跳都没有?

var tr = d3
  .transition()
  .duration(obj.tick.duration)
  .ease(d3.easeLinear);

function tick() {
  return setInterval(function() {
    var newData = [];
    var tickFunction = obj.tick.fnTickData;
    if (tickFunction !== undefined && typeof tickFunction === "function") {
      newData = tickFunction();
      for (var i = 0; i < newData.length; i++) {
        obj.data.push(newData[i]);
      }
    }

    if (newData.length > 0) {
      var newMaxDate, newMinDate, newDomainX;
      if (isKeyXDate) {
        newMaxDate = new Date(
          Math.max.apply(
            null,
            obj.data.map(function(e) {
              return new Date(e[obj.dataKeys.keyX]);
            })
          )
        );
        newMinDate = new Date(
          Math.min.apply(
            null,
            obj.data.map(function(e) {
              return new Date(e[obj.dataKeys.keyX]);
            })
          )
        );
        newDomainX = [newMinDate, newMaxDate];
      } else {
        newDomainX = [
          d3.min(obj.data, function(d) {
            return d[obj.dataKeys.keyX];
          }),
          d3.max(obj.data, function(d) {
            return d[obj.dataKeys.keyX];
          })
        ];
      }

      // update the domains
      //x.domain([newMinDate, newMaxDate]);
      if (obj.tick.updateXDomain) {
        newDomainX = obj.tick.updateXDomain;
      }
      x.domain(newDomainX);
      if (obj.tick.updateYDomain) {
        y.domain(obj.tick.updateYDomain);
      }

      path.attr("transform", null);

      // slide the line left
      if (obj.area.allowArea) {
        areaPath.attr("transform", null);
        areaPath
          .transition()
          .transition(tr)
          .attr("d", area);
      }
      path
        .transition()
        .transition(tr)
        .attr("d", line);
      svg
        .selectAll(".x")
        .transition()
        .transition(tr)
        .call(x.axis);
      svg
        .selectAll(".y")
        .transition()
        .transition(tr)
        .call(y.axis);

      // pop the old data point off the front
      obj.data.shift();
    }
  }, obj.tick.tickDelay);
}
this.interval = tick();

当您转换 d 属性时,bounce 实际上是预期的结果,它只是一个字符串。

这里有几种解决方案。无需过多重构您的代码,一个简单的方法是使用 Mike Bostock 在 bl.ocks: https://bl.ocks.org/mbostock/3916621 中编写的 pathTween 函数。在这里,我对其进行了一些更改,以便您可以像这样传递数据:

path.transition()
    .transition(tr)
    .attrTween("d", function(d) {
        var self = this;
        var thisd = line(d);
        return pathTween(thisd, 1, self)()
    })

这是分叉的插件:https://plnkr.co/edit/aAqpdSb9JozwHsErpqa9?p=preview

正如 Gerardo 指出的那样,转换路径的 d 属性不会很好地工作,除非您修改该方法。如果只是更新路径的 d 属性,则会出现这种 wiggle/bouncing 的简单示例:

pᴏɪɴᴛsᴛʀᴀɴSɪᴛɪᴏɴɪɴɢSsssssssssssssssssssSᴄʀᴇᴇɴsɪᴛɪᴏɴɪɴɢsɪᴛɪᴏɴɪɴɢsɪᴛɪᴏɴɪɴɢsᴇᴛsᴇᴛsᴇᴛsᴇᴛsᴇᴛsᴇᴛx。

Mike Bostock 在一篇短文中提到了上述行为 here,下面是重现上述动画的片段:

var n = 10;
var data = d3.range(n).map(function(d) {
  return {x: d, y:Math.random() }
})

var x = d3.scaleLinear()
  .domain(d3.extent(data, function(d) { return d.x; }))
  .range([10,490])
  
var y = d3.scaleLinear()
  .range([290,10]);
  
var line = d3.line()
  .x(function(d) { return x(d.x); })
  .y(function(d) { return y(d.y); })
  
var svg = d3.select("body")
  .append("svg")
  .attr("width",500)
  .attr("height", 400)
  .append("g");
  
var path = svg.append("path")
  .datum(data)
  .attr("d", line);

var points = svg.selectAll("circle")
  .data(data, function(d) { return d.x; })
  .enter()
  .append("circle")
  .attr("cx", function(d) { return x(d.x); })
  .attr("cy", function(d) { return y(d.y); })
  .attr("r", 5);


function tick() {
 
  var transition = d3.transition()
    .duration(1000);
  
  var newPoint = {x:n++, y: Math.random() };
  data.shift()
  data.push(newPoint);
  
  x.domain(d3.extent(data,function(d) { return d.x; }))
  
  points = svg.selectAll("circle").data(data, function(d) { return d.x; })
    
   points.exit()
     .transition(transition)
     .attr("cx", function(d) { return x(d.x); })
     .attr("cy", function(d) { return y(d.y); }) 
     .remove(); 
     
  points.enter().append("circle")
     .attr("cx", function(d) { return x(d.x)+30; })
     .attr("cy", function(d) { return y(d.y); })
     .merge(points)
     .transition(transition)
     .attr("cx", function(d) { return x(d.x); })
     .attr("r", 5);

  path.datum(data)
    .transition(transition)
    .attr("d", line)
    .on("end", tick);
}

tick();

  

   
path {
  fill: none;
  stroke: black;
  stroke-width: 2;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

这个 wiggle/bounce 的一个解决方案是:

  1. 向数据添加一个额外的点,
  2. 用最近添加到数据数组的线重新绘制线
  3. 找出数据的下一个范围
  4. 将行向左移动
  5. 更新比例并转换轴
  6. 删除第一个数据点

我链接到的迈克的文章中也提出了这一点。这是您的代码的基本实现:

我通过在最后一个转换结束时递归调用该函数来避免使用 setInterval 函数:

    function slide() {
     // Stop any ongoing transitions:
     d3.selectAll().interrupt();

      // A transition:
      var transition = d3.transition()
        .duration(2000)
        .ease(d3.easeLinear)

      // 1. add an additional point(s) to the data
      var newData = obj.tick.fnTickData();
      obj.data.push(...newData);

      // 2. redraw the line with the recently added to data array
      path.datum(obj.data)
      areaPath.datum(obj.data)

      // Redraw the graph, without the translate, with less data:
      path.attr("transform","translate(0,0)")
        .attr("d", line)

      areaPath.attr("transform","translate(0,0)")
        .attr("d", area)          

      // 3. find out the next extent of the data                     
      // Assuming data is in chronological order:
      var min = obj.data[newData.length][obj.dataKeys.keyX];
      var max = obj.data[obj.data.length-1][obj.dataKeys.keyX];

      // 4. transition the line to the left
      path.datum(obj.data)
        .transition(transition)
        .attr("transform", "translate("+(-x(new Date(min)))+",0)");
      areaPath.datum(obj.data)
        .transition(transition)
        .attr("transform", "translate("+(-x(new Date(min)))+",0)");

      // 5. update the scale and transition the axis
      x.domain([new Date(min),new Date(max)])

      // Update the xAxis:
      svg.selectAll('.x')
         .transition(transition)
         .call(x.axis)
         .on("end",slide); // Trigger a new transition at the end.

      // 6. remove the first data point(s)  
      obj.data.splice(0,newData.length)
    }
    slide();

这是更新后的 plunkr