D3.js折线图在使用常规更新模式时使用了错误的键



我使用D3.js创建使用一般更新模式的折线图。我有两种不同类型的数据。第一种类型使用缩写的月份键,另一种类型使用月份的日期。

我遇到的问题是行不能正确地从一种数据类型转换到另一种数据类型。我读过一些文档,它指出,当更新d3更新使用每个元素的索引行。但是你可以通过定义D3在更新图表时应该观察哪个变量来改变这一点。

因此,为了完成这一点,我在数据函数中声明D3应该使用数据数组中的key变量来检查两个数据点是否相同。但是在下面的代码片段示例中,您可以看到更新不能正常工作。而不是从底部加载完整的新行。它将第一行转换为第二行,但它们显然有不同的键。

我已经更新了代码:

问题没有得到正确的解释。我想更新这条线,在这条线上的每个点都应该插值到下一个点。在底部的片段是工作的。如果它从第一个数组切换到第二个数组,其中所有键都是相同的。该行应该像代码片段中那样做,只是插入。

但是,如果我输入一个带有所有新键的全新数据(如代码片段中的第三个数组),它应该显示从底部插入的行(就像第一次加载应用程序时输入行一样)图表,而不是从其先前的位置插入。这是因为在项目中,我使用的线也由点(圆)组成,当使用新数组时,这些也从底部过渡。

this.area = this.area
.data([data], d => d.key)

new Vue({
el: "#app",
data() {
return {
index: 0,
data: [
[{
key: "Jan",
value: 5787
},
{
key: "Feb",
value: 6387
},
{
key: "Mrt",
value: 7375
},
{
key: "Apr",
value: 6220
},
{
key: "Mei",
value: 6214
},
{
key: "Jun",
value: 5205
},
{
key: "Jul",
value: 5025
},
{
key: "Aug",
value: 4267
},
{
key: "Sep",
value: 6901
},
{
key: "Okt",
value: 5800
},
{
key: "Nov",
value: 7414
},
{
key: "Dec",
value: 6547
}
],
[{
key: "Jan",
value: 4859
},
{
key: "Feb",
value: 5674
},
{
key: "Mrt",
value: 6474
},
{
key: "Apr",
value: 7464
},
{
key: "Mei",
value: 6454
},
{
key: "Jun",
value: 5205
},
{
key: "Jul",
value: 6644
},
{
key: "Aug",
value: 5343
},
{
key: "Sep",
value: 5363
},
{
key: "Okt",
value: 5800
},
{
key: "Nov",
value: 4545
},
{
key: "Dec",
value: 5454
}
],
[{
"key": 1,
"value": 4431
},
{
"key": 2,
"value": 5027
},
{
"key": 3,
"value": 4586
},
{
"key": 4,
"value": 7342
},
{
"key": 5,
"value": 6724
},
{
"key": 6,
"value": 6070
},
{
"key": 7,
"value": 5137
},
{
"key": 8,
"value": 5871
},
{
"key": 9,
"value": 6997
},
{
"key": 10,
"value": 6481
},
{
"key": 11,
"value": 5194
},
{
"key": 12,
"value": 4428
},
{
"key": 13,
"value": 4790
},
{
"key": 14,
"value": 5825
},
{
"key": 15,
"value": 4709
},
{
"key": 16,
"value": 6867
},
{
"key": 17,
"value": 5555
},
{
"key": 18,
"value": 4451
},
{
"key": 19,
"value": 7137
},
{
"key": 20,
"value": 5353
},
{
"key": 21,
"value": 5048
},
{
"key": 22,
"value": 5169
},
{
"key": 23,
"value": 6650
},
{
"key": 24,
"value": 5918
},
{
"key": 25,
"value": 5679
},
{
"key": 26,
"value": 5546
},
{
"key": 27,
"value": 6899
},
{
"key": 28,
"value": 5541
},
{
"key": 29,
"value": 7193
},
{
"key": 30,
"value": 5006
},
{
"key": 31,
"value": 6580
}
]
]
}
},
mounted() {
// set the dimensions and margins of the graph
var margin = {
top: 20,
right: 20,
bottom: 30,
left: 30
},
width = 500 - margin.left - margin.right;
this.height = 200 - margin.top - margin.bottom;
// append the svg obgect to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
this.svg = d3
.select("#my_dataviz")
.append("svg")
.attr(
"viewBox",
`0 0 ${width + margin.left + margin.right} ${this.height +
margin.top +
margin.bottom}`
)
.attr("preserveAspectRatio", "xMinYMin")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// set the ranges
this.xScale = d3
.scalePoint()
.range([0, width])
.domain(
this.data.map(function(d) {
return d.key;
})
)
.padding(0.5);
this.yScale = d3.scaleLinear().rangeRound([this.height, 0]);
this.yScale.domain([0, 7000]);
// Draw Axis
this.xAxis = d3.axisBottom(this.xScale);
this.xAxisDraw = this.svg
.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0, ${this.height})`);
this.yAxis = d3
.axisLeft(this.yScale)
.tickValues([0, 7000])
.tickFormat(d => {
if (d > 1000) {
d = Math.round(d / 1000);
d = d + "K";
}
return d;
});
this.yAxisDraw = this.svg.append("g").attr("class", "y axis");
this.update(this.data[this.index]);
},
methods: {
swapData() {
if (this.index === 2) this.index = 0;
else this.index++;
this.update(this.data[this.index]);
},
update(data) {
// Update scales.
this.xScale.domain(data.map(d => d.key));
this.yScale.domain([0, 7000]);
// Set up transition.
const dur = 1000;
const t = d3.transition().duration(dur);
// Update line.
this.line = this.svg.selectAll(".line")
this.line = this.line
.data([data], d => d.key)
.join(
enter => {
enter
.append("path")
.attr("class", "line")
.attr("fill", "none")
.attr("stroke", "#206BF3")
.attr("stroke-width", 4)
.attr(
"d",
d3
.line()
.x(d => {
return this.xScale(d.key);
})
.y(() => {
return this.yScale(0);
})
)
.transition(t)
.attr(
"d",
d3
.line()
.x(d => {
return this.xScale(d.key);
})
.y(d => {
return this.yScale(d.value);
})
);
},
update => {
update.transition(t).attr(
"d",
d3
.line()
.x(d => {
return this.xScale(d.key);
})
.y(d => {
return this.yScale(d.value);
})
);
},
exit => exit.remove()
);
// Update Axes.
this.yAxis.tickValues([0, 7000]);
if (data.length > 12) {
this.xAxis.tickValues(
data.map((d, i) => {
if (i % 3 === 0) return d.key;
else return 0;
})
);
} else {
this.xAxis.tickValues(
data.map(d => {
return d.key;
})
);
}
this.yAxis.tickValues([0, 7000]);
this.xAxisDraw.transition(t).call(this.xAxis.scale(this.xScale));
this.yAxisDraw.transition(t).call(this.yAxis.scale(this.yScale));
}
}
})
<div id="app">
<button @click="swapData">Swap</button>
<div id="my_dataviz" class="flex justify-center"></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://d3js.org/d3.v6.js"></script>

[UPDATE:根据注释,当新数据中的键集不同时,代码更新为从底部开始的新行]

这是一个有助于更好地理解问题的贡献,以及一个可能的答案。

key元素有一些误用。当你定义一行的键时,d3知道一行绑定到那个键。在这种情况下,您的密钥绑定到路径。当你添加

this.line = this.line
.data([data], d => d.key)

d3将选择绑定到[data],并将恰好生成一个元素([data])。长度= 1)对于这些元素,d = data,因此d.key = null。这就是为什么您不添加多行,因为您的路径总是得到key = null的原因。

所以,在第一次一切都按计划进行时,你开始的路径为零,然后通过过渡将其移动到最终位置。

该路径具有d3生成的d属性。格式为M x1 y1 L x2 y2 L x3 y3 ... L x12 y 12的行。第一次正好12分

当您交换数据时,d3将检查键(再次为空),并将此视为更新。

因此,它将用新数据将当前路径插入到新路径中。这里的问题是没有键来绑定值。因为你现在有31个点,它将插入前12个点(这是你看到移动的部分),并添加剩余的点(13到31)。当然,最后这些点没有过渡,因为它们不存在。

对于您的情况,一个可能的解决方案是使用自定义插值器(您可以构建)并使用attrTween来进行插值。

幸运的是,有人已经建了一个:https://unpkg.com/d3-interpolate-path/build/d3-interpolate-path.min.js

所以这是一个有效的解决方案

new Vue({
el: "#app",
data() {
return {
index: 0,
data: [
[{
key: "Jan",
value: 5787
},
{
key: "Feb",
value: 6387
},
{
key: "Mrt",
value: 7375
},
{
key: "Apr",
value: 6220
},
{
key: "Mei",
value: 6214
},
{
key: "Jun",
value: 5205
},
{
key: "Jul",
value: 5025
},
{
key: "Aug",
value: 4267
},
{
key: "Sep",
value: 6901
},
{
key: "Okt",
value: 5800
},
{
key: "Nov",
value: 7414
},
{
key: "Dec",
value: 6547
}
],
[{
"key": 1,
"value": 4431
},
{
"key": 2,
"value": 5027
},
{
"key": 3,
"value": 4586
},
{
"key": 4,
"value": 7342
},
{
"key": 5,
"value": 6724
},
{
"key": 6,
"value": 6070
},
{
"key": 7,
"value": 5137
},
{
"key": 8,
"value": 5871
},
{
"key": 9,
"value": 6997
},
{
"key": 10,
"value": 6481
},
{
"key": 11,
"value": 5194
},
{
"key": 12,
"value": 4428
},
{
"key": 13,
"value": 4790
},
{
"key": 14,
"value": 5825
},
{
"key": 15,
"value": 4709
},
{
"key": 16,
"value": 6867
},
{
"key": 17,
"value": 5555
},
{
"key": 18,
"value": 4451
},
{
"key": 19,
"value": 7137
},
{
"key": 20,
"value": 5353
},
{
"key": 21,
"value": 5048
},
{
"key": 22,
"value": 5169
},
{
"key": 23,
"value": 6650
},
{
"key": 24,
"value": 5918
},
{
"key": 25,
"value": 5679
},
{
"key": 26,
"value": 5546
},
{
"key": 27,
"value": 6899
},
{
"key": 28,
"value": 5541
},
{
"key": 29,
"value": 7193
},
{
"key": 30,
"value": 5006
},
{
"key": 31,
"value": 6580
}
]
]
}
},
mounted() {
// set the dimensions and margins of the graph
var margin = {
top: 20,
right: 20,
bottom: 30,
left: 30
},
width = 500 - margin.left - margin.right;
this.height = 200 - margin.top - margin.bottom;
// append the svg obgect to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
this.svg = d3
.select("#my_dataviz")
.append("svg")
.attr(
"viewBox",
`0 0 ${width + margin.left + margin.right} ${this.height +
margin.top +
margin.bottom}`
)
.attr("preserveAspectRatio", "xMinYMin")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// set the ranges
this.xScale = d3
.scalePoint()
.range([0, width])
.domain(
this.data.map(function(d) {
return d.key;
})
)
.padding(0.5);
this.yScale = d3.scaleLinear().rangeRound([this.height, 0]);
this.yScale.domain([0, 7000]);
// Draw Axis
this.xAxis = d3.axisBottom(this.xScale);
this.xAxisDraw = this.svg
.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0, ${this.height})`);
this.yAxis = d3
.axisLeft(this.yScale)
.tickValues([0, 7000])
.tickFormat(d => {
if (d > 1000) {
d = Math.round(d / 1000);
d = d + "K";
}
return d;
});
this.yAxisDraw = this.svg.append("g").attr("class", "y axis");
this.update(this.data[this.index]);
},
methods: {
swapData() {
if (this.index === 0) this.index = 1;
else this.index = 0;
this.update(this.data[this.index]);
},
update(data) {
// Update scales.
this.xScale.domain(data.map(d => d.key));
this.yScale.domain([0, 7000]);
// Set up transition.
const dur = 1000;
const t = d3.transition().duration(dur);
const line = d3
.line()
.x(d => {
return this.xScale(d.key);
})
.y((d) => {
return this.yScale(d.value);
});
// Update line.
this.line = this.svg.selectAll(".line")
this.line = this.line
.data([data], d => d.reduce((key, elem) => key + '_' + elem.key, ''))
.join(
enter => {
enter
.append("path")
.attr("class", "line")
.attr("fill", "none")
.attr("stroke", "#206BF3")
.attr("stroke-width", 4)
.attr(
"d",
d3
.line()
.x(d => {
return this.xScale(d.key);
})
.y(() => {
return this.yScale(0);
})
)
.transition(t)
.attr(
"d", (d) => line(d)
);
},
update => {
update
.transition(t)
.attrTween('d', function(d) { 
var previous = d3.select(this).attr('d');
var current = line(d);
return d3.interpolatePath(previous, current); 
});
},
exit => exit.remove()
);
// Update Axes.
this.yAxis.tickValues([0, 7000]);
if (data.length > 12) {
this.xAxis.tickValues(
data.map((d, i) => {
if (i % 3 === 0) return d.key;
else return 0;
})
);
} else {
this.xAxis.tickValues(
data.map(d => {
return d.key;
})
);
}
this.yAxis.tickValues([0, 7000]);
this.xAxisDraw.transition(t).call(this.xAxis.scale(this.xScale));
this.yAxisDraw.transition(t).call(this.yAxis.scale(this.yScale));
}
}
})
<div id="app">
<button @click="swapData">Swap</button>
<div id="my_dataviz" class="flex justify-center"></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://d3js.org/d3.v6.js"></script>
<script src="https://unpkg.com/d3-interpolate-path/build/d3-interpolate-path.min.js"></script>

我还没有直接回答你的问题(对不起!),因为这可能是一个更好的解决方案。可以在具有不同点数的线之间进行插值,这可能会提供更好的体验?

有一个d3-interpolate-path插件,可以处理路径中存在的不同数量的点,但仍然可以通过在行中插入占位符点来创建一个相当平滑的动画。

有一个很好的解释它是如何工作的,以及一些例子https://bocoup.com/blog/improving-d3-path-animation .

如果你真的想每次都从零开始动画,那么你需要检查键是否匹配最后一组键。

  1. 创建d3本地存储

    const keyStore = d3.local();

  2. 从上次渲染中获取键(元素想要成为你的行)

    const oldKeys = keyStore.get(element);

  3. 确定键是否匹配:

    const newKeys = data.map(d => d.key);
    // arraysEqual - https://stackoverflow.com/a/16436975/21061
    const keysMatch = arraysEqual(oldKeys, newKeys);
    
  4. 更改keysMatch上的插值(参见前面的三元制):

    update.transition(t)
    .attrTween('d', function(d) { 
    var previous = keysMatch ? d3.select(this).attr('d') : 0;
    var current = line(d);
    return d3.interpolatePath(previous, current); 
    });
    

最新更新