D3.js - DOM 在排序后重新排序但图表不更新

我完全是 D3 的初学者,并已开始绘制从 API 中检索到的一些数据,例如:

chartData = [
{ track: "A Kind of Magic",playcount: 2683110 },{ track: "Another One Bites the Dust",playcount: 6425611 },{ track: "Bohemian Rhapsody",playcount: 10167011 },{ track: "Crazy Little Thing Called Love",playcount: 4360128},{ track: "Don't Stop Me Now",playcount: 7762976 },{ track: "flash",playcount: 1248561 }];

根据一些在线示例,我呈现了条形图:

var margin = {
  top: 50,right: 40,bottom: 80,left: 100
},width = 1200 - margin.left - margin.right,height = 500 - margin.top - margin.bottom;

var svg = d3.select('#queenChart')
  .attr('width',width + margin.left + margin.right)
  .attr('height',height + margin.top + margin.bottom)
  .append('g').attr('transform',`translate(${margin.left},${margin.top})`);

var x = d3.scaleBand()
  .domain(chartData.map(d => d.track))
  .range([0,width])
  .padding(0.1);

svg.append('g')
  .attr('transform',`translate(0,${height})`)
  .call(d3.axisBottom(x))
  .selectAll('text')
  .attr('transform','translate(-10,0) rotate(-45)')
  .style('text-anchor','end');

var y = d3.scaleLinear()
  .domain([0,d3.max(chartData,d => { return d.playcount; })])
  .range([height,0]);

svg.append('g')
  .call(d3.axisLeft(y));

svg.selectAll('rect')
  .data(chartData)
  .enter()
  .append('rect')
  .style('fill','steelblue')
  .attr('x',d => x(d.track))
  .attr('y',d => y(d.playcount))
  .attr('height',d => { return height - y(d.playcount); })
  .attr('width',x.bandwidth ); 

当我尝试对图表进行排序时出现问题:

setTimeout(() => {
  svg.selectAll('rect')
    .sort((a,b) => d3.ascending(a.playcount,b.playcount))
    .transition()
    .duration(500)
    .attr('x',d => x(d.track));
},2500);

2.5 秒后,我可以看到 DOM 中的所有 <rect> 元素确实已正确排序,但图表 svg 本身完全没有变化。是否有什么我遗漏的东西可以让图表重新渲染以反映 DOM 中的变化?

zcg345 回答:D3.js - DOM 在排序后重新排序但图表不更新

有什么我遗漏的东西可以让图表重新渲染以反映 DOM 中的变化吗?

您重新渲染了图表,它确实反映了 DOM 中的变化,但是 DOM 的顺序对于图表元素在 SVG 中的放置位置无关紧要,因为 x 和 y属性由元素的属性指定。顺序不影响元素的 x 或 y 属性。

顺序仅与重叠元素有关:被另一个元素覆盖并共享同一父元素的元素将首先附加

让我们看看你的代码:

svg.selectAll('rect')
  .sort((a,b) => d3.ascending(a.playcount,b.playcount))
  .attr('x',d => x(d.track));

您对 DOM 元素进行排序,但根据绑定数据将它们放置在 x 轴上。每个元素的绑定数据没有改变(即使它们在 DOM 中的顺序发生了变化),所以每个条形的 x 位置与原来相同。

选项 1

我从这个选项开始是因为它更接近你所拥有的,但我更喜欢选项 2

一种选择是在对元素进行排序后重新计算比例。使用排序元素的绑定数据,我们可以重新计算比例的域,因为我们根据比例定位数据,这将对条进行排序:

  svg.selectAll('rect')
    .sort((a,b.playcount))
    .call(function(rects) {
      x.domain(rects.data().map(d => d.track));
    })
    .attr('x',d => x(d.track));

这里使用 .call 可以让我们访问已排序的元素数据:rects.data() 并将这些值按顺序映射到域,覆盖之前的域。现在我们只需要再次根据 d.track 进行定位。

当然我们也需要更新轴,所以我在下面添加了一些代码:

chartData = [
{ track: "A Kind of Magic",playcount: 2683110 },{ track: "Another One Bites the Dust",playcount: 6425611 },{ track: "Bohemian Rhapsody",playcount: 10167011 },{ track: "Crazy Little Thing Called Love",playcount: 4360128},{ track: "Don't Stop Me Now",playcount: 7762976 },{ track: "Flash",playcount: 1248561 }];

var margin = {
  top: 50,right: 40,bottom: 80,left: 100
},width = 1200 - margin.left - margin.right,height = 500 - margin.top - margin.bottom;

var svg = d3.select('body')
  .append('svg')
  .attr('width',width + margin.left + margin.right)
  .attr('height',height + margin.top + margin.bottom)
  .append('g').attr('transform',`translate(${margin.left},${margin.top})`);

var x = d3.scaleBand()
  .domain(chartData.map(d => d.track))
  .range([0,width])
  .padding(0.1);

// store g in variable
var xAxis = svg.append('g')
  .attr('transform',`translate(0,${height})`)
  .call(d3.axisBottom(x));
 
// break method chaining so we don't store a selection of text elements
xAxis
  .selectAll('text')
  .attr('transform','translate(-10,0) rotate(-45)')
  .style('text-anchor','end');

var y = d3.scaleLinear()
  .domain([0,d3.max(chartData,d => { return d.playcount; })])
  .range([height,0]);

svg.append('g')
  .call(d3.axisLeft(y));

svg.selectAll('rect')
  .data(chartData)
  .enter()
  .append('rect')
  .style('fill','steelblue')
  .attr('x',d => x(d.track))
  .attr('y',d => y(d.playcount))
  .attr('height',d => { return height - y(d.playcount); })
  .attr('width',x.bandwidth );
  
setTimeout(() => {
  svg.selectAll('rect')
    .sort((a,b.playcount))
    .call(function(rects) {
      x.domain(rects.data().map(d => d.track));
    })
    .transition()
    .duration(500)
    .attr('x',d => x(d.track));
    
   // transition the x axis to reflect the new data:
   xAxis.transition().call(d3.axisBottom(x));
    
    
},2500);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

选项 2

另一种不使用 selection.sort() 的选项是对实际数据进行排序,然后将其直接用作秤的域:

 x.domain(chartData.sort((a,b)=>a.playcount-b.playcount).map(d=>d.track))

 svg.selectAll('rect')
    .attr('x',d => x(d.track));

当然,我们也需要更新 x 轴,但这可能看起来像:

chartData = [
{ track: "A Kind of Magic",x.bandwidth );
  
setTimeout(() => {
  x.domain(chartData.sort((a,b)=>a.playcount-b.playcount).map(d=>d.track))

  svg.selectAll('rect')
    .transition()
    .duration(500)
    .attr('x',2500);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

最终,任何解决方案都应涉及重新排序数据:通过重新排序数据或元素并使用结果更新 x 尺度的域。这是必需的,因为您根据元素的数据放置元素,并且 DOM 顺序不会影响它们在 x 轴上的位置。

本文链接:https://www.f2er.com/9108.html

大家都在问