d3.js - apply exit after append 模拟动画

d3.js - apply exit after append to simulate a animation

我想像动画一样在条形图上绘制矩形。我只需要一次绘制一个矩形(可能保持 0.5 秒)然后将其移除并绘制另一个矩形。

目前所有的矩形都会在屏幕上绘制!我尝试使用退出模式但不起作用!

barchart_hist()
function barchart_hist() {
  var m = [10, 80, 25, 80]
  var w = 600 - m[1] - m[3]
  var h = 400 - m[0] - m[2]

  var y_data = [2,1,5,6,2,3,0]

  var graph = d3.select('body').append("svg")
  .attr("width", w + m[1] + m[3])
  .attr("height", h + m[0] + m[2])
  .append("svg:g")
  .attr("transform", "translate(" + m[3] + "," + m[0] + ")")

  var x = d3.scaleBand()
  .domain(d3.range(y_data.length))
  .range([0, w])
  .padding(0.05);

  var y = d3.scaleLinear()
  .domain([0, d3.max(y_data)])
  .range([h,0])

  var xAxis = d3.axisBottom(x)
  .ticks(y_data.length)

  var yAxisLeft = d3.axisLeft(y)
  .ticks(d3.max(y_data))

  graph.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + (h) + ")")
    .call(xAxis);

  graph.append("g")
    .attr("class", "y axis")
    .attr("transform", "translate(-25,0)")
    .call(yAxisLeft);

  graph.selectAll("bar")
    .data(y_data)
    .enter().append("rect")
    .style("fill", "none")
    .attr('stroke','black')
    .attr("x", (d,i) => x(i))
    .attr("y", (d,i) => y(d))
    .attr("width", x.bandwidth())
    .attr("height", (d,i) => h - y(d));
 
  var rdata = [[0,1],[3,4],[2,4],[5,6],[4,6],[1,6]]
  
  var box = graph.selectAll('box')
  .data(rdata)
  
  var boxenter = box.enter()
  .append('rect')
  .attr('stroke','red')
  .attr('stroke-width',2)
  .attr('fill','none')
  .attr('x',d => {
    return x(d[0])})
  .attr('y',d => y(y_data[d[0]]))
  .attr('width',d => {
    return x(d[1])-x(d[0])})
  .attr('height',d => {
    return y(0) - y(y_data[d[0]])
  })  
  
  box.exit().transition().duration(500).remove()
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>

似乎有两个关键问题:

Enter/Update/Exit

当 DOM 中的选定元素在数据数组中没有对应项时,退出选择仅包含元素。在您的情况下,我们有一个带有绑定数据的空选择:

  // an empty selection (no elements with tag box exist):
  var box = graph.selectAll('box')
     .data(rdata) // bind data to the selection

然后我们使用输入选择box.enter()创建元素,使数据数组中的每一项都作为DOM中的对应元素。由于 DOM 中没有匹配的元素,我们为 DOM 中的每个项目创建一个新元素:

var boxenter = box.enter()
  .append('rect')
  ...

框出口选择,box.exit() 包含数据数组中不存在相应项目的任何元素。 selection.exit() 除了 return 选择 之外什么都不做。由于没有预先存在的元素,因此退出选择中不能包含任何元素。所以这个:

box.exit().transition().duration(500).remove()

不执行任何操作,因为退出选择为空。

供参考,任何未包含在退出选择中的现有元素都在更新选择中 (box)。

如果我们想对新输入的框做一些事情,我们可以使用 boxenter 选择。

不使用带.data()的键函数,只有enter/exit选择之一可以包含元素:我们有太多元素(退出选择包含元素)或没有足够(输入选择包含元素)。

过渡

您使用 selection.transition().duration(500).remove() 将删除一个选择,但不会过渡任何内容。转换用于随时间改变 attribute/style/property。您尚未指定要转换的内容 属性。 selection.transition().duration(500).remove() 将在 500 毫秒后简单地从 DOM 中删除选择。

相反,您需要转换一个 attribute/style/property,例如:

 selection.transition() // return a transition (not a selection).
 .attr('y',d => y(0))
 .attr('height',0)  
 .duration(10000)
 .remove();

请注意,转换与选择具有相似的方法 - 但每个方法都有不适用于另一个的方法。这里 .attr()、.duration() 和 .remove() 对转换和选择都是通用的,但是 .duration(),例如仅用于转换。

我们可以将过渡链接到输入选择,因为我们想在添加后立即将其删除。

以上是实际操作:

barchart_hist()
function barchart_hist() {
  var m = [10, 80, 25, 80]
  var w = 600 - m[1] - m[3]
  var h = 400 - m[0] - m[2]

  var y_data = [2,1,5,6,2,3,0]

  var graph = d3.select('body').append("svg")
  .attr("width", w + m[1] + m[3])
  .attr("height", h + m[0] + m[2])
  .append("svg:g")
  .attr("transform", "translate(" + m[3] + "," + m[0] + ")")

  var x = d3.scaleBand()
  .domain(d3.range(y_data.length))
  .range([0, w])
  .padding(0.05);

  var y = d3.scaleLinear()
  .domain([0, d3.max(y_data)])
  .range([h,0])

  var xAxis = d3.axisBottom(x)
  .ticks(y_data.length)

  var yAxisLeft = d3.axisLeft(y)
  .ticks(d3.max(y_data))

  graph.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + (h) + ")")
    .call(xAxis);

  graph.append("g")
    .attr("class", "y axis")
    .attr("transform", "translate(-25,0)")
    .call(yAxisLeft);

  graph.selectAll("bar")
    .data(y_data)
    .enter().append("rect")
    .style("fill", "none")
    .attr('stroke','black')
    .attr("x", (d,i) => x(i))
    .attr("y", (d,i) => y(d))
    .attr("width", x.bandwidth())
    .attr("height", (d,i) => h - y(d));
 
  var rdata = [[0,1],[3,4],[2,4],[5,6],[4,6],[1,6]]
  
  var box = graph.selectAll('box')
  .data(rdata)
  
  var boxenter = box.enter()
  .append('rect')
  .attr('stroke','red')
  .attr('stroke-width',2)
  .attr('fill','none')
  .attr('x',d => {
    return x(d[0])})
  .attr('y',d => y(y_data[d[0]]))
  .attr('width',d => {
    return x(d[1])-x(d[0])})
  .attr('height',d => {
    return y(0) - y(y_data[d[0]])
  })
  .transition() 
     .attr('y',d => y(0))
     .attr('height',0)  
    .duration(10000)
    .remove();
  
 
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>

连续转换

好的,现在我们可以看看将以上内容放在一起进行连续转换。如果每个系列中的元素数量相同,这将是最简单的:我们可以只使用一个数据数组,该数组具有表示两个数组中的值的属性,并从一个数组转换到另一个数组,同时也只输入一次项目。如果需要,我还可以扩展该方法。

但是,由于您有不同大小的数组(或者您可能有动态数据),我们需要一种不同的方法。下面对两个数组使用标准化的数据结构,以便我们可以对每个数组使用相同的更新函数。

我修改了第一个数据数组中的最后一个元素,因此它在用于代码演示的图表中可见(所以你知道它不是错误地丢失)

// The data:
var data1 = [[0,2],[1,1],[2,5],[3,6],[4,2],[5,3],[6,1]];
var data2 = [[0,1],[3,4],[2,4],[5,6],[4,6],[1,6]]


// The set up:
var m = [10, 80, 25, 80]
var w = 600 - m[1] - m[3]
var h = 300 - m[0] - m[2]
  
var graph = d3.select('body').append("svg")
  .attr("width", w + m[1] + m[3])
  .attr("height", h + m[0] + m[2])
  .append("svg:g")
  .attr("transform", "translate(" + m[3] + "," + m[0] + ")")
  
var xAxis = graph.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + (h) + ")")

var yAxis = graph.append("g")
    .attr("class", "y axis")
    .attr("transform", "translate(-25,0)")

// (For demonstration):
var i = 0;
var datasets = [data1, data2];

// Update function:
function update(data) {
  // set up scales:
  var x = d3.scaleBand()
   .domain(d3.range(data.length))
   .range([0, w])
   .padding(0.05); 
  
  var y = d3.scaleLinear()
  .domain([0, d3.max(data, d=>d[1])])
  .range([h,0])

  // set up axis:
  xAxis.transition().call(d3.axisBottom(x));
  yAxis.transition().call(d3.axisLeft(y));

  // Enter then remove:
  graph.selectAll(".bar")
    .data(data)
    .enter()
    .append("rect")
    .style("fill", "none")
    .attr('stroke','black')
    .attr("x", d=>x(d[0]))
    .attr("width", x.bandwidth())
    // Start new rects with zero height:
    .attr("y", y(0))
    .attr("height", 0)
    // Transition up: 
    .transition()
    .attr("y", d=>y(d[1]))
    .attr("height", d=>y(0)-y(d[1]))
    // Transition down:
    .transition()
    .delay(1000) // wait a bit first
    .attr("y", y(0))
    .attr("height", 0)
    .remove()
    .end()
    // get next dataset, and repeat:
    .then(()=>update(datasets[++i%2]));
    // example to get one more data set and stop:
    //.then(function() {
    //  if (data === data1) update(data2)
    //  else console.log("end");
    //});
   

   
}

update(data1);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>

注意使用 transition.end() returns 承诺一旦所有转换结束,而使用 transition.on("end", fn) 将触发在每个元素的过渡端。

您已声明要在使用新数据更新之前删除现有元素。 SO 上有很多问题询问如何删除所有现有元素然后创建新元素,此模式省略了更新和退出选择并且通常是非规范的。可能是您的第二个数据数组项不代表第一个数据数组中表示的任何实体 - 在这种情况下,这种方法很好。但是,这种方法通常会限制您的功能。如果您想用新数据更新现有项目,我们可以改变方法以充分利用更新现有数据点(这可能会讲述一个有趣的故事,具体取决于您的数据及其代表的内容):

// The data:
var data1 = [[0,2],[1,1],[2,5],[3,6],[4,2],[5,3],[6,1]];
var data2 = [[0,1],[3,4],[2,4],[5,6],[4,6],[1,6]]


// The set up:
var m = [10, 80, 25, 80]
var w = 600 - m[1] - m[3]
var h = 300 - m[0] - m[2]
  
var graph = d3.select('body').append("svg")
  .attr("width", w + m[1] + m[3])
  .attr("height", h + m[0] + m[2])
  .append("svg:g")
  .attr("transform", "translate(" + m[3] + "," + m[0] + ")")
  
var xAxis = graph.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + (h) + ")")

var yAxis = graph.append("g")
    .attr("class", "y axis")
    .attr("transform", "translate(-25,0)")

// (For demonstration):
var i = 0;
var datasets = [data1, data2];

// Update function:
function update(data) {
  // set up scales:
  var x = d3.scaleBand()
   .domain(d3.range(data.length))
   .range([0, w])
   .padding(0.05); 
  
  var y = d3.scaleLinear()
  .domain([0, d3.max(data, d=>d[1])])
  .range([h,0])

  // set up axis:
  xAxis.transition().delay(500).call(d3.axisBottom(x));
  yAxis.transition().delay(500).call(d3.axisLeft(y));

  // Enter then remove:
  var selection = graph.selectAll(".bar")
    .data(data);
    
  var enterTransition = selection.enter()
    .append("rect")
    .attr("class","bar")
    .style("fill", "steelblue")
    .attr("opacity",0)
    .attr('stroke','black')
    .attr('stroke-width',1)
    .attr("x", d=>x(d[0]))
    .attr("width", x.bandwidth())
    // Start new rects with zero height:
    .attr("y", y(0))
    .attr("height", 0)
    // Transition up and remove color: 
    .transition()
    .delay(500)
    .duration(1000)
    .style("fill","white")
    .attr("opacity",1)
    .attr("y", d=>y(d[1]))
    .attr("height", d=>y(0)-y(d[1]))
    .end()

     
  var updateTransition = selection.transition()
    .delay(500)
    .duration(1000)
    .attr("y", d=>y(d[1]))
    .attr("height", d=>y(0)-y(d[1]))   
    .attr("x", d=>x(d[0]))
    .attr("width", x.bandwidth()) 
    .end()
    
  var exitTransition = selection.exit()
    .transition()
    .duration(500)
    .attr("opacity",0)
    .style("fill","crimson")
    .remove()
    .end()
    
    
    Promise.allSettled([enterTransition,updateTransition,exitTransition])
       .then(()=>update(datasets[++i%2]));
   
   
}

update(data1);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>

我为退出选择添加了红色,为进入选择添加了蓝色,并更改了一些不透明度以更好地突出更改

您可以考虑这种方法:当您创建 box 选择时,附加 rect 但不附加其他属性,从而创建与 rects 中的对一样多的不可见 rects =16=]数组.

然后您可以使用 selection.each() to iterate box. Since you know the index argument of each you can use that with selection.filter() 确定要在其上设置样式和坐标的框;否则删除 stroke 属性以使框不可见。在 setTimeout 中执行此操作可为您提供动画过程。

我想说来自@AndrewReid 的关于更新/退出是非规范的遗漏的评论适用于下面的方法,但我觉得它符合要求。

  // prep all the boxes (rect only - will be invisible)
  var box = graph.selectAll('box')
    .data(rdata)
    .enter()
    .append('rect');
    
  // iterate boxes
  box.each((d, i) => {
    setTimeout(() => {
      // otherBoxes are boxes not matching i argument in each
      var otherBoxes = boxes.filter((d, i2) => i2 !== i);
      otherBoxes.attr('stroke', 'none'); 
      // thisbox is the currentbox per the i argument in each
      var thisBox = boxes.filter((d, i2) => i2 === i);
      // your original rendering code
      thisBox
        .attr('stroke','red')
        .attr('stroke-width',2)
        .attr('fill','none')
        .attr('x',d => {
          return x(d[0])})
        .attr('y',d => y(y_data[d[0]]))
        .attr('width',d => {
          return x(d[1])-x(d[0])})
        .attr('height',d => {
          return y(0) - y(y_data[d[0]])
        });

    }, i * 1000); // 1s delay
  });

工作示例:

barchart_hist()
function barchart_hist() {
  var m = [10, 80, 25, 80]
  var w = 600 - m[1] - m[3]
  var h = 200 - m[0] - m[2]

  var y_data = [2,1,5,6,2,3,0]

  var graph = d3.select('body').append("svg")
  .attr("width", w + m[1] + m[3])
  .attr("height", h + m[0] + m[2])
  .append("svg:g")
  .attr("transform", "translate(" + m[3] + "," + m[0] + ")")

  var x = d3.scaleBand()
  .domain(d3.range(y_data.length))
  .range([0, w])
  .padding(0.05);

  var y = d3.scaleLinear()
  .domain([0, d3.max(y_data)])
  .range([h,0])

  var xAxis = d3.axisBottom(x)
  .ticks(y_data.length)

  var yAxisLeft = d3.axisLeft(y)
  .ticks(d3.max(y_data))

  graph.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + (h) + ")")
    .call(xAxis);

  graph.append("g")
    .attr("class", "y axis")
    .attr("transform", "translate(-25,0)")
    .call(yAxisLeft);

  graph.selectAll("bar")
    .data(y_data)
    .enter().append("rect")
    .style("fill", "none")
    .attr('stroke','black')
    .attr("x", (d,i) => x(i))
    .attr("y", (d,i) => y(d))
    .attr("width", x.bandwidth())
    .attr("height", (d,i) => h - y(d));
 
  var rdata = [[0,1],[3,4],[2,4],[5,6],[4,6],[1,6]]
  
  // prep all the boxes (id only)
  var boxes = graph.selectAll('box')
    .data(rdata)
    .enter()
    .append('rect');
    
  // iterate boxes
  boxes.each((d, i) => {
    setTimeout(() => {
      var otherBoxes = boxes.filter((d, i2) => i2 !== i);
      otherBoxes.attr('stroke', 'none'); 
      var thisBox = boxes.filter((d, i2) => i2 === i);
      thisBox
        .attr('stroke','red')
        .attr('stroke-width',2)
        .attr('fill','none')
        .attr('x',d => {
          return x(d[0])})
        .attr('y',d => y(y_data[d[0]]))
        .attr('width',d => {
          return x(d[1])-x(d[0])})
        .attr('height',d => {
          return y(0) - y(y_data[d[0]])
        });

    }, i * 1000);
  });



}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>