D3.js - DOM 排序后重新排序但图表未更新
D3.js - DOM reorders after sort but chart does not update
我是 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?
中的更改的内容
Is there something I'm missing that gets the chart to rerender to reflect the changes in the 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 轴上的位置。
我是 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?
Is there something I'm missing that gets the chart to rerender to reflect the changes in the 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 轴上的位置。