【问题标题】:D3.js - DOM reorders after sort but chart does not updateD3.js - 排序后 DOM 重新排序,但图表未更新
【发布时间】:2021-10-06 03:01:45
【问题描述】:

我是 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 中的变化?

【问题讨论】:

    标签: javascript html svg d3.js


    【解决方案1】:

    我是否缺少一些东西来重新渲染图表以反映 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) => d3.ascending(a.playcount, 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) => d3.ascending(a.playcount, 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", 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(() => {
      x.domain(chartData.sort((a,b)=>a.playcount-b.playcount).map(d=>d.track))
    
      svg.selectAll('rect')
        .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>

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

    【讨论】:

    • 出色的答案;这对我帮助很大,非常感谢!
    猜你喜欢
    • 1970-01-01
    • 2015-09-16
    • 2014-01-14
    • 2012-12-15
    • 2020-08-18
    • 1970-01-01
    • 2016-08-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多