[英]D3 General update pattern transition not working on pie chart
我有一个项目,我使用饼图/甜甜圈图来可视化我的数据。 我添加了一般更新模式,以便在我的数据更改/更新时创建平滑过渡。
为了做到这一点,我遵循了一个在饼图上使用一般更新模式的示例: Bl.ocks example 。
我面临的问题是更新数据时图表更新不顺畅。 图表立即从一个 state 交换到下一个。
在这个示例和其他示例中,他们定义了一个arcTween
方法,其中 d3 在先前的角度和新更新数据的角度之间进行插值:
arcTween(a) {
let i = d3.interpolate(this._current, a);
this._current = i(0);
return t => {
return this.arc(i(t));
};
}
我还添加了加入、输入和更新我的数据到饼图的代码。 在这里,我首先创建了绘制饼图的组元素。我还使用“arcTween”方法定义了一个转换,以在状态之间转换。 最后,我还在创建饼图时定义了“this._current”:
this.g = this.svg
.selectAll("doughnut")
.data(data_ready)
.enter()
.append("g");
this.g
.transition()
.duration(1500)
.attrTween("d", this.arcTween);
this.g
.append("path")
.attr("d", d => {
return this.arc(d);
})
.attr("fill", "#206BF3")
.attr("class", "slice")
.attr("stroke", "#2D3546")
.style("stroke-width", "2px")
.each(d => {
this._current = d;
});
这是我的完整代码。 这是用 Vue.js 编写的。 我试图让它在一个片段中工作。 但我无法让它工作。
显示在甜甜圈切片顶部的图像在本地存储:
<template>
<div class="p-5 flex flex-col h-full">
<h2 class="mb-3">{{ title }}</h2>
<div ref="my_dataviz" class="flex justify-center"></div>
<div class="grid grid-cols-2 gap-7 m-7">
<div v-for="item in data" :key="item.key" class="flex">
<img
:src="require('@/assets/img/doughnut/' + item.icon)"
alt=""
class="doughnutIcon mr-4"
/>
<div class="flex flex-col">
<h3>{{ item.key }}</h3>
<p class="opacity-50">
{{ formatNumberValue(item.value) }} {{ unit }}
</p>
<p class="opacity-50">{{ percentageOfTotal(item.value) }} %</p>
</div>
</div>
</div>
</div>
</template>
<script>
import { converter } from "@/shared";
import * as d3 from "d3";
export default {
name: "DoughnutChartItem",
props: {
title: {
type: String,
required: true
},
data: {
type: Array,
required: true
},
height: {
type: Number,
required: true
},
width: {
type: Number,
required: true
},
unit: {
type: String
}
},
data() {
return {
totalAmount: 0,
svg: undefined,
arc: undefined,
radius: undefined,
g: undefined
};
},
created() {
let total = 0;
this.data.forEach(item => {
total += item.value;
});
this.totalAmount = total;
},
mounted() {
// set the dimensions and margins of the graph
var margin = 1;
// The radius of the pieplot is half the width or half the height (smallest one). I subtract a bit of margin.
this.radius = Math.min(this.width, this.height) / 2 - margin;
// append the svg object to the div called 'my_dataviz'
this.svg = d3
.select(this.$refs.my_dataviz)
.append("svg")
.attr("width", this.width)
.attr("height", this.height)
.append("g")
.attr(
"transform",
"translate(" + this.width / 2 + "," + this.height / 2 + ")"
);
// Compute the position of each group on the pie:
this.pie = d3.pie().value(function(d) {
return d[1];
});
// declare an arc generator function
this.arc = d3
.arc()
.outerRadius(100)
.innerRadius(50);
this.setSlicesOnDoughnut(this.data);
this.addImagesToSlices();
},
methods: {
animateSliceOnHover(radius, path, dir) {
switch (dir) {
case 0:
path
.transition()
.duration(500)
.ease(d3.easeBounce)
.attr(
"d",
d3
.arc()
.innerRadius(100)
.outerRadius(50)
);
path.style("fill", "#206BF3");
break;
case 1:
path.transition().attr(
"d",
d3
.arc()
.innerRadius(50)
.outerRadius(110)
);
path.style("fill", "white");
break;
}
},
percentageOfTotal(amount) {
return Math.round((amount / this.totalAmount) * 100);
},
formatNumberValue(amount) {
return converter.formatNumberValue(amount);
},
setSlicesOnDoughnut(data) {
// Build the pie chart: Basically, each part of the pie is a path that we build using the arc function.
var data_ready = this.pie(
data.map(function(d) {
return [d["key"], d["value"], d["icon"], d["hover"]];
})
);
this.g = this.svg
.selectAll("doughnut")
.data(data_ready)
.enter()
.append("g");
this.g
.transition()
.duration(1500)
.attrTween("d", this.arcTween);
this.g
.append("path")
.attr("d", d => {
return this.arc(d);
})
.attr("fill", "#206BF3")
.attr("class", "slice")
.attr("stroke", "#2D3546")
.style("stroke-width", "2px")
.each(d => {
this._current = d;
});
// Add tooltip
d3.selectAll(".slice")
.on("mouseover", this.mouseover)
.on("mousemove", this.mousemove)
.on("mouseout", this.mouseout);
},
addImagesToSlices() {
var image_width = 20;
var image_height = 20;
this.g.selectAll(".logo").remove();
this.g
.append("svg:image")
.attr("transform", d => {
var x = this.arc.centroid(d)[0] - image_width / 2;
var y = this.arc.centroid(d)[1] - image_height / 2;
return "translate(" + x + "," + y + ")";
})
.attr("class", "logo")
.attr("class", function(d) {
return `${d.data[0]}-logo`;
})
.attr("href", function(d) {
return require("@/assets/img/doughnut/" + d.data[2]);
})
.attr("width", image_width)
.attr("height", image_height);
},
mouseover(event, data) {
//Swap doughnut icon to blue icon
d3.selectAll("." + data.data[0] + "-logo").attr("href", d => {
return require("@/assets/img/doughnut/" + d.data[3]);
});
this.animateSliceOnHover(this.radius, d3.select(event.currentTarget), 1);
const tip = d3.select(".tooltip");
tip
.style("left", `${event.clientX + 15}px`)
.style("top", `${event.clientY}px`)
.transition()
.style("opacity", 0.98);
tip.select("h3").html(`${data.data[0]}`);
tip
.select("h4")
.html(`${this.formatNumberValue(data.data[1])} ${this.unit}`);
},
mousemove(event) {
// Move tooltip
d3.select(".tooltip")
.style("left", `${event.clientX + 15}px`)
.style("top", `${event.clientY}px`);
},
mouseout(event, data) {
//Swap doughnut icon to white icon
d3.selectAll("." + data.data[0] + "-logo").attr("href", function(d) {
return require("@/assets/img/doughnut/" + d.data[2]);
});
// Animate slice
var thisPath = d3.select(event.currentTarget);
this.animateSliceOnHover(this.radius, thisPath, 0);
// if (!thisPath.classed("clicked")) {
// this.animateSliceOnHover(this.radius, thisPath, 0);
// }
// Hide tooltip
d3.select(".tooltip")
.transition()
.style("opacity", 0);
},
arcTween(a) {
let i = d3.interpolate(this._current, a);
this._current = i(0);
return t => {
return this.arc(i(t));
};
}
},
watch: {
data() {
this.setSlicesOnDoughnut(this.data);
this.addImagesToSlices();
}
}
};
</script>
我已经缩短了代码并包含在代码片段中:
new Vue({ el: "#app", data() { return { index: 0, data: [ [ { key: "one", value: 123 }, { key: "two", value: 232 }, { key: "three", value: 186 } ], [ { key: "one", value: 145 }, { key: "two", value: 270 }, { key: "three", value: 159 } ], ] } }, mounted() { // set the dimensions and margins of the graph var margin = 1; // The radius of the pieplot is half the width or half the height (smallest one). I subtract a bit of margin. this.radius = Math.min(this.width, this.height) / 2 - margin; this.width = 250; this.height = 250; // append the svg object to the div called 'my_dataviz' this.svg = d3.select("#my_dataviz").append("svg").attr("width", this.width).attr("height", this.height).append("g").attr( "transform", "translate(" + this.width / 2 + "," + this.height / 2 + ")" ); // Compute the position of each group on the pie: this.pie = d3.pie().value(function(d) { return d[1]; }); // declare an arc generator function this.arc = d3.arc().outerRadius(100).innerRadius(50); this.setSlicesOnDoughnut(); },methods: { swapData() { if(this.index === 0) this.index = 1; else this.index = 0; this.setSlicesOnDoughnut(); }, setSlicesOnDoughnut() { // Build the pie chart: Basically, each part of the pie is a path that we build using the arc function. var data_ready = this.pie( this.data[this.index].map(function(d) { return [d["key"], d["value"]]; }) ); console.log(data_ready); // join var arcs = this.svg.selectAll(".arc").data(data_ready); // update arcs.transition().duration(1500).attrTween("d", this.arcTween); // enter arcs.enter().append("path").attr("class", "arc").attr("fill", "#206BF3").attr("stroke", "#2D3546").style("stroke-width", "2px").attr("d", this.arc).each((d, i, n) => { n[i]._current = d; }); } }, arcTween(a) { var i = d3.interpolate(this._current, a); this._current = i(0); return t => { return this.arc(i(t)); }; } })
<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>
该片段有效,但切片更改 position:
new Vue({ el: "#app", data() { return { index: 0, data: [ [{ key: "one", value: 123 }, { key: "two", value: 232 }, { key: "three", value: 186 }, { key: "four", value: 238 } ], [{ key: "one", value: 145 }, { key: "two", value: 270 }, { key: "three", value: 159 }, { key: "four", value: 168 } ], ] } }, mounted() { // set the dimensions and margins of the graph var margin = 1; // The radius of the pieplot is half the width or half the height (smallest one). I subtract a bit of margin. this.radius = Math.min(this.width, this.height) / 2 - margin; this.width = 250; this.height = 250; // append the svg object to the div called 'my_dataviz' this.svg = d3.select("#my_dataviz").append("svg").attr("width", this.width).attr("height", this.height).append("g").attr( "transform", "translate(" + this.width / 2 + "," + this.height / 2 + ")" ); // Compute the position of each group on the pie: this.pie = d3.pie().value(function(d) { return d[1]; }); // declare an arc generator function this.arc = d3.arc().outerRadius(100).innerRadius(50); this.setSlicesOnDoughnut(); }, methods: { swapData() { if (this.index === 0) this.index = 1; else this.index = 0; this.setSlicesOnDoughnut(); }, arcTween(a, j, n) { var i = d3.interpolate(n[j]._current, a); n[j]._current = i(0); return t => { return this.arc(i(t)); }; }, setSlicesOnDoughnut() { // Build the pie chart: Basically, each part of the pie is a path that we build using the arc function. var data_ready = this.pie( this.data[this.index].map(function(d) { return [d["key"], d["value"]]; }) ); //console.log(data_ready); // join var arcs = this.svg.selectAll(".arc").data(data_ready); // update arcs.transition().duration(1500).attrTween("d", this.arcTween); // enter arcs.enter().append("path").attr("class", "arc").attr("fill", "#206BF3").attr("stroke", "#2D3546").style("stroke-width", "2px").attr("d", this.arc).each((d, i, n) => { n[i]._current = d; }); } } })
<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>
两项更改使您的代码段起作用。
第一个是在methods
object 中向上移动arcTween
方法。 由于我不是 Vue 用户,并且从 JS 的角度来看这是没有意义的(对象的属性顺序无关紧要),我将把这部分留给你调查。
第二个变化只是与this
的含义有关。 您将this
混合为method
object(如在this.arc
中)和this
作为 DOM 元素(如在this._current
中)。 就像我在评论中所说的那样,只需使用第三个和第二个 arguments 组合来引用 DOM 元素(在绝大多数 D3 方法中)。
也就是说,这...
arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return t => {
return this.arc(i(t));
};
}
...应该:
arcTween(a, j, n) {
var i = d3.interpolate(n[j]._current, a);
n[j]._current = i(0);
return t => {
return this.arc(i(t));
};
}
这是您进行这两项更改的代码段:
new Vue({ el: "#app", data() { return { index: 0, data: [ [{ key: "one", value: 123 }, { key: "two", value: 232 }, { key: "three", value: 186 } ], [{ key: "one", value: 145 }, { key: "two", value: 270 }, { key: "three", value: 159 } ], ] } }, mounted() { // set the dimensions and margins of the graph var margin = 1; // The radius of the pieplot is half the width or half the height (smallest one). I subtract a bit of margin. this.radius = Math.min(this.width, this.height) / 2 - margin; this.width = 250; this.height = 250; // append the svg object to the div called 'my_dataviz' this.svg = d3.select("#my_dataviz").append("svg").attr("width", this.width).attr("height", this.height).append("g").attr( "transform", "translate(" + this.width / 2 + "," + this.height / 2 + ")" ); // Compute the position of each group on the pie: this.pie = d3.pie().value(function(d) { return d[1]; }); // declare an arc generator function this.arc = d3.arc().outerRadius(100).innerRadius(50); this.setSlicesOnDoughnut(); }, methods: { swapData() { if (this.index === 0) this.index = 1; else this.index = 0; this.setSlicesOnDoughnut(); }, arcTween(a, j, n) { var i = d3.interpolate(n[j]._current, a); n[j]._current = i(0); return t => { return this.arc(i(t)); }; }, setSlicesOnDoughnut() { // Build the pie chart: Basically, each part of the pie is a path that we build using the arc function. var data_ready = this.pie( this.data[this.index].map(function(d) { return [d["key"], d["value"]]; }) ); //console.log(data_ready); // join var arcs = this.svg.selectAll(".arc").data(data_ready); // update arcs.transition().duration(1500).attrTween("d", this.arcTween); // enter arcs.enter().append("path").attr("class", "arc").attr("fill", "#206BF3").attr("stroke", "#2D3546").style("stroke-width", "2px").attr("d", this.arc).each((d, i, n) => { n[i]._current = d; }); } } })
<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>
该代码确实有效,您可以看到小提琴https://jsfiddle.net/1Lk8zqud/1/
如果你真的需要容器,那么你需要有一些考虑:
如果是新容器,请记住它们与最终路径一起添加(无过渡),因此最好降低旧容器以免弄乱过渡。 我没有尝试使用图像。
所以这是新版本
const g = svg.selectAll('g.arcs')
.data(data_ready, function(d) { return d.index; });
// new data -> add arcs
g.enter().append('g')
.attr('class', 'arcs')
.lower()
.append('path')
.attr("fill", "#206BF3")
.attr("class", "slice")
.attr("stroke", "#2D3546")
.style("stroke-width", "2px")
.transition()
.duration(1500)
.attrTween("d", function(a) {
let i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return this.arc(i(t));
};
});
// old data -> transition data on the container g (for using with the images). Don't add arcs
g.raise()
.transition()
.duration(1500)
.attrTween("d", function(a) {
let i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return this.arc(i(t));
};
});
// transition the path inside the old containers g
g.select('path').transition()
.duration(1500)
.attrTween("d", function(a) {
let i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return this.arc(i(t));
};
})
g.exit().lower().transition().duration(1500).remove();```
A working version of this code:
https://jsfiddle.net/rtLbyn52/
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.