【數(shù)據(jù)可視化】D3.js實(shí)現(xiàn)動(dòng)態(tài)氣泡圖
大家好,歡迎來到 Crossin的編程教室 !
數(shù)據(jù)處理及可視化是Python的一大應(yīng)用場(chǎng)景。不過為了實(shí)現(xiàn)更好的動(dòng)態(tài)演示效果,實(shí)際應(yīng)用中常常還需要和js相結(jié)合。
今天我們就來給大家分享一個(gè)用D3.js實(shí)現(xiàn)的動(dòng)態(tài)氣泡圖案例。
本文用到的語(yǔ)言主要 js,不過主要是做一些配置,所以閱讀起來并不困難。另外也建議大家有空可以了解一下基礎(chǔ)的js語(yǔ)法,會(huì)很有幫助。
首先我們來看下 D3.js 的氣泡圖效果:

項(xiàng)目地址:
GitHub地址:
https://github.com/unkleho/d3-render
要想實(shí)現(xiàn)這個(gè)項(xiàng)目的話,首先需要安裝 Node.js 以及 npm,具體的安裝步驟這里贅述,百度一下就有,還是比較簡(jiǎn)單的。
接下來就可以安裝 Vue.js 及 Vue腳手架3.0。
# 安裝Vue.js
npm install vue
# 安裝Vue-cli3腳手架
npm install -g @vue/cli
# 創(chuàng)建名為bubblechart的項(xiàng)目
vue create bubblechart
結(jié)果如下,選擇默認(rèn)模式即可。

由于里面有eslint(編碼規(guī)范)的存在,記得在配置文件package.json中添加下面的代碼。
"rules": {
"no-unused-vars": "off",
"no-undef": "off"
}
要不然會(huì)出現(xiàn)報(bào)錯(cuò),無(wú)法運(yùn)行。
項(xiàng)目創(chuàng)建成功后,修改App.vue文件內(nèi)容如下。
<template>
<div id="app">
<svg id="bubble-chart" width="954" height="450" />
</div>
</template>
<script>
export default {
name: 'App',
components: {
}
}
</script>
<style></style>
保存成功后,打開bubblechart文件夾下的終端,運(yùn)行下面這個(gè)命令。
npm run serve
瀏覽器便會(huì)跳出一個(gè)標(biāo)題為bubblechart的空白網(wǎng)頁(yè)。
安裝一些項(xiàng)目依賴d3,d3-render,d3-selection,d3-transition,axios。
npm install d3@5.16.0 --save-dev
npm install d3-render@0.2.4 --save-dev
npm install d3-selection@1.4.2 --save-dev
npm install d3-transition@2.0.0 --save-dev
npm install axios --save-dev
最好是指定版本,要不然可能會(huì)報(bào)錯(cuò)。
在main.js文件中引用axios,用于請(qǐng)求數(shù)據(jù)。
import axios from 'axios'
Vue.prototype.$axios = axios
在App.vue的script標(biāo)簽中引用d3,d3-render。
import * as d3 from "d3";
import render from "d3-render";
設(shè)置初始數(shù)據(jù),各式各樣的氣泡顏色。
data() {
return {
covidData: null,
countries: null,
colours: {
pink: "#D8352A",
red: "#D8352A",
blue: "#48509E",
green: "#02A371",
yellow: "#F5A623",
hyperGreen: "#19C992",
purple: "#B1B4DA",
orange: "#F6E7AD",
charcoal: "#383838",
}
};
}
獲取各地區(qū)的新冠數(shù)據(jù),兩個(gè)CSV文件放在Public文件夾下,可直接訪問。
methods: {
async getdata() {
//獲取新冠數(shù)據(jù)
await this.$axios.get("data.csv").then((res) => {
this.covidData = d3.csvParse(res.data);
});
//獲取國(guó)家數(shù)據(jù)
await this.$axios.get("countries.csv").then((res) => {
this.countries = d3.csvParse(res.data);
});
//畫圖
this.drawType();
},
}
開始畫圖的操作,先定義一下畫布大小以及各大洲的顏色。
drawType() {
//設(shè)置svg大小
const width = 954;
const height = 450;
//設(shè)置各個(gè)大洲的參數(shù)
const continents = [
{
id: "AF",
name: "Africa",
fill: this.colours.purple,
colour: this.colours.charcoal,
},
{
id: "AS",
name: "Asia",
fill: this.colours.yellow,
colour: this.colours.charcoal,
},
{
id: "EU",
name: "Europe",
fill: this.colours.blue,
colour: this.colours.charcoal,
},
{
id: "NA",
name: "N. America",
fill: this.colours.pink,
},
{
id: "OC",
name: "Oceania",
fill: this.colours.orange,
colour: this.colours.charcoal,
},
{
id: "SA",
name: "S. America",
fill: this.colours.green,
colour: this.colours.charcoal,
},
];
}
定義圓圈組件,其中duration很重要,起到一個(gè)動(dòng)畫過渡的效果。
//定義圓圈組件
const circleComponent = ({ r, cx, cy, fill, duration }) => {
return {
append: "circle",
r,
cx,
cy,
fill,
duration,
};
};
定義文字組件,設(shè)置字體、大小、顏色等。
//定義文字組件
const textComponent = ({
key,
text,
x = 0,
y = 0,
fontWeight = "bold",
fontSize = "12px",
textAnchor = "middle",
fillOpacity = 1,
colour,
r,
duration = 1000,
}) => {
return {
append: "text",
key,
text,
x,
y,
textAnchor,
fontFamily: "sans-serif",
fontWeight,
fontSize,
fillOpacity: { enter: fillOpacity, exit: 0 },
fill: colour,
duration,
style: {
pointerEvents: "none",
},
};
};
數(shù)值轉(zhuǎn)換,對(duì)較大的數(shù)值進(jìn)行處理。
//對(duì)數(shù)值進(jìn)行轉(zhuǎn)換,比如42288變?yōu)?2k
const format = (value) => {
const newValue = d3.format("0.2s")(value);
if (newValue.indexOf("m") > -1) {
return parseInt(newValue.replace("m", "")) / 1000;
}
return newValue;
};
動(dòng)態(tài)變化標(biāo)簽信息,包含名稱及數(shù)值。
//將各地區(qū)名稱長(zhǎng)度和數(shù)值與圓圈大小相比較,實(shí)現(xiàn)信息動(dòng)態(tài)變化
const labelComponent = ({ isoCode, countryName, value, r, colour }) => {
// Don't show any text for radius under 12px
if (r < 12) {
return [];
}
//console.log(r);
const circleWidth = r * 2;
const nameWidth = countryName.length * 10;
const shouldShowIso = nameWidth > circleWidth;
const newCountryName = shouldShowIso ? isoCode : countryName;
const shouldShowValue = r > 18;
let nameFontSize;
if (shouldShowValue) {
nameFontSize = shouldShowIso ? "10px" : "12px";
} else {
nameFontSize = "8px";
}
return [
textComponent({
key: isoCode,
text: newCountryName,
fontSize: nameFontSize,
y: shouldShowValue ? "-0.2em" : "0.3em",
fillOpacity: 1,
colour,
}),
...(shouldShowValue
? [
textComponent({
key: isoCode,
text: format(value),
fontSize: "10px",
y: shouldShowIso ? "0.9em" : "1.0em",
fillOpacity: 0.7,
colour,
}),
]
: []),
];
};
設(shè)置氣泡組件。
//設(shè)置氣泡組件
const bubbleComponent = ({
name,
id,
value,
r,
x,
y,
fill,
colour,
duration = 1000,
}) => {
return {
append: "g",
key: id,
transform: {
enter: `translate(${x + 1},${y + 1})`,
exit: `translate(${width / 2},${height / 2})`,
},
duration,
delay: Math.random() * 300,
children: [
circleComponent({ key: id, r, fill, duration }),
...labelComponent({
key: id,
countryName: name,
isoCode: id,
value,
r,
colour,
duration,
}),
],
};
};
//d3.pack - 創(chuàng)建一個(gè)新的圓形打包圖
//d3.hierarchy - 從給定的層次結(jié)構(gòu)數(shù)據(jù)構(gòu)造一個(gè)根節(jié)點(diǎn)并為各個(gè)節(jié)點(diǎn)指定深度等屬性
const pack = (data) =>
d3
.pack()
.size([width - 2, height - 2])
.padding(2)(d3.hierarchy({ children: data }).sum((d) => d.value));
//生成氣泡圖表
const renderBubbleChart = (selection, data) => {
const root = pack(data);
const renderData = root.leaves().map((d) => {
return bubbleComponent({
id: d.data.id,
name: d.data.name,
value: d.data.value,
r: d.r,
x: d.x,
y: d.y,
fill: d.data.fill,
colour: d.data.colour,
});
});
return render(selection, renderData);
};
const renderBubbleChartContainer = (data) => {
return renderBubbleChart("#bubble-chart", data);
};
//定義新冠數(shù)據(jù)
const covidData_result = this.covidData;
//定義各地區(qū)數(shù)據(jù)
const countries_result = this.countries;
//選擇數(shù)據(jù)類型為所有確診病例數(shù)量
const dataKey = "total_cases";
//定義開始時(shí)間及結(jié)束時(shí)間
const startDate = new Date('2020-01-12')
const endDate = new Date('2020-06-02')
//d3.map - 創(chuàng)建一個(gè)新的空的 map 映射
const dates = d3
.map(this.covidData, (d) => d.date)
.keys()
.map((date) => new Date(date))
.filter((date) => date >= startDate && date <= endDate)
.sort((a, b) => a - b);
//各大洲全選
const selectedContinents = ["AF", "AS", "EU", "NA", "OC", "SA"];
//最小數(shù)值
const minimumPopulation = 0;
//排序
const order = "desc";
//轉(zhuǎn)換日期格式為2020-01-01
const getIsoDate = (date) => {
const IsoDate = new Date(date);
return IsoDate.toISOString().split("T")[0];
};
//獲取最終的數(shù)據(jù)
function getDataBy({
dataKey,
date,
selectedContinents,
order,
minimumPopulation,
}) {
return (
covidData_result
.filter((d) => d)
.filter((d) => d.iso_code !== "OWID_WRL")
// Filter out countries with populations under 1 million
.filter((d) => d.population > parseInt(minimumPopulation))
.filter((d) => {
return d.date === getIsoDate(date);
})
.filter((d) => d[dataKey])
.filter((d) => {
const country = countries_result.find(
(c) => c.iso3 === d.iso_code
);
const continent = continents.find((c, i) => {
if (!country) {
return false;
}
return c.id === country.continentCode;
});
if (!continent) {
return false;
}
return selectedContinents.includes(continent.id);
})
.map((d) => {
const country = countries_result.find(
(c) => c.iso3 === d.iso_code
);
const continent = continents.find(
(c) => c.id === country.continentCode
);
const name = country.shortName || country.name;
return {
name,
id: country.iso3,
value: d[dataKey],
fill: continent.fill,
colour: continent.colour || "white",
};
})
.filter((d) => d.value !== "0.0")
.sort(function (a, b) {
const mod = order === "desc" ? -1 : 1;
return mod * (a.value - b.value);
})
);
}
//延時(shí)執(zhí)行,閉包
for (var i = 0; i < dates.length; i++) {
(function (i) {
setTimeout(function () {
const date = dates[i];
console.log(date);
const data = getDataBy({
dataKey,
date,
selectedContinents,
minimumPopulation,
order,
});
renderBubbleChartContainer(data);
}, 2000 * i);
})(i);
};

npm install
npm run serve
如果文章對(duì)你有幫助,歡迎轉(zhuǎn)發(fā)/點(diǎn)贊/收藏~
_往期文章推薦_
