2020东京奥运会奖牌数据可视化
很早我写了一个关2020东京奥运会奖牌数据可视化的页面,使用了d3.js库进行数据的处理和展示。这篇实战将详细讲解。
题目要求:
- 输出部分数据集:将给定的数据集数据进行输出(到页面中),输出的时间为:2021 年 7 月 25 日奖牌榜 前 10 名、2021 年 7 月 27 日和 2021 年 8 月 2 日奖牌榜前 10 名。
- 动态显示美国、中国、日本和澳大利亚的金牌折线图。
- 筛选出中国、日本、美国、澳大利亚的金牌数据,绘制折线图;
- 如样例 GIF 所示,动态地将各个国家的金牌数据折线图进行可视化。
- 每个数据节点添加交互,放大显示本日的各国奖牌数据,金牌、银牌、铜牌(样例 中只显示金牌数据)。
读取CSV文件
js
d3.csv("./2021东京奥运会奖牌数据.csv").then(function (data) {})
解析数据
js
// 寻找所有的日期
var dataArray = data.map(function (d) {
return d.日期;
});
// 去重
var dateArray = [...new Set(dataArray)];
// 按日期排序
dateArray.sort(function (a, b) {
return new Date(a) - new Date(b);
});
console.log('dateArray :>> ', dateArray);
// '中国', '美国', '日本', '澳大利亚' 4个国家的数据
var countrys = ['中国', '美国', '日本', '澳大利亚'];
var countryData = data.filter(function (d) {
return countrys.includes(d.国家);
});
var lines = [[], [], [], []];
countryData.map(function (d) {
var index = countrys.indexOf(d.国家);
lines[index].push(d);
});
// 如果某个国家某天没有数据,就补0
lines.map(function (d) {
dateArray.map(function (date) {
var index = d.findIndex(function (item) {
return item.日期 == date;
});
if (index == -1) {
d.push({
日期: date,
国家: d[0].国家,
国家编码: d[0].国家编码,
金牌: 0,
银牌: 0,
铜牌: 0,
总计: 0,
国旗: d[0].国旗
});
}
});
});
// 排序
lines.forEach(function (item) {
item.sort(function (a, b) {
return new Date(a.日期) - new Date(b.日期);
});
});
// 累计金牌
lines.forEach(function (item) {
var sum = 0;
item.forEach(function (d) {
sum += +d.金牌;
d.累计金牌 = sum;
});
});
-
读取CSV文件:"d3.csv("./2021东京奥运会奖牌数据.csv")" 这行代码使用D3库读取名为 "2021东京奥运会奖牌数据.csv" 的文件。
-
提取日期数据:通过遍历数据集,将每个对象的 "日期" 属性提取出来,存储在 "dataArray" 数组中。
-
去重:使用 Set 对象对 "dataArray" 数组进行去重操作,得到 "dateArray" 数组。
-
按日期排序:使用自定义的比较函数对 "dateArray" 数组进行排序,按照日期从早到晚的顺序排列。
-
筛选国家数据:根据给定的国家列表('中国', '美国', '日本', '澳大利亚'),从原始数据集中筛选出这些国家的奖牌数据,存储在 "countryData" 数组中。
-
构建数据结构:创建一个二维数组 "lines",用于存储每个国家的奖牌数据。遍历 "countryData" 数组,将每个国家的奖牌数据添加到对应的 "lines" 子数组中。
-
补全缺失数据:遍历 "lines" 数组,对于每个国家的子数组,检查是否包含当天的数据。如果没有,则添加一条新的记录,其中日期为当前遍历的日期,其他国家信息与第一个国家的相同,金牌、银牌和铜牌数量都为0。
-
排序:对每个国家的子数组按照日期进行排序。
-
累计金牌:遍历每个国家的子数组,计算累计金牌数,并将结果存储在每个对象的 "累计金牌" 属性中。
最后,代码输出了处理后的 "lines" 数组,其中包含了每个国家的奖牌数据,按照日期排序,并计算了累计金牌数。
可视化数据
js
var w = 1200, h = 600; // 设置图表的宽度和高度
let padding = { l: 50, r: 80, t: 60, b: 50 }; // 设置图表的内边距
var div = d3.select("body").append("div") // 在body中添加一个div元素作为容器
.style("width", w + "px")
.style("height", h + "px")
.style("background", "linear-gradient(to bottom, #BB6688, #4499DD)")
.style('margin', '24px auto');
var svg = div.append("svg") // 在容器中添加一个svg元素作为绘图区域
.attr("width", w)
.attr("height", h);
// 添加标题和坐标轴标签
svg.append('text')
.attr('x', w / 2)
.attr('y', padding.t / 2 + 10)
.attr('text-anchor', 'middle')
.attr('font-size', 22)
.attr('font-weight', 'bold')
.attr('fill', 'red')
.text('中国 VS 美国 VS 日本 VS 澳大利亚金牌走势图');
svg.append('text')
.attr('x', padding.l)
.attr('y', padding.t / 2 + 10)
.attr('text-anchor', 'middle')
.attr('font-size', 16)
.attr('fill', 'yellow')
.attr('font-weight', 'bold')
.text('金牌/枚');
// 定义颜色数组
var colors = ["#AF425D", "#354A5B", "#6F9EA6", "#C8716E"];
// 创建图例
var legend = svg.append("g")
.attr("transform", `translate(${w - padding.r - 30},${h - padding.b - 100})`);
legend.selectAll("rect")
.data(countrys)
.enter()
.append("rect")
.attr("x", 0)
.attr("y", (d, i) => i * 20)
legend.selectAll("text")
.data(countrys)
.enter()
.append("text")
.attr("x", 30)
.attr("y", (d, i) => i * 20 + 10)
.text((d) => d);
// 定义比例尺
// 创建x轴和y轴
var xAxis = d3.axisBottom(scaleX);
var yAxis = d3.axisLeft(scaleY).ticks(5);
// 添加x轴和y轴到图表中
svg.append("g")
.attr("transform", `translate(${padding.l},${h - padding.b})`)
.call(xAxis);
svg.append("g")
.attr("transform", `translate(${padding.l},${padding.t})`)
.call(yAxis);
// 添加白色虚线
svg.append("g")
.attr("transform", `translate(${padding.l},${padding.t})`)
.selectAll("line")
.data([10, 20, 30, 40])
.enter()
.append("line")
.attr("x1", 0)
.attr("y1", (d) => scaleY(d))
添加交互
js
var tips = div.append("div")
.style("position", "absolute")
.style("background", "rgba(0,0,0,0.5)")
.style("color", "white")
.style("padding", "10px")
.style("border-radius", "5px")
.style("display", "none");
var lineG = svg.append("g").attr("class", "lineG")
.attr("transform", `translate(${padding.l},${padding.t})`);
var line = d3.line()
.x((d) => scaleX(d.日期))
.y((d) => scaleY(d.累计金牌))
.curve(d3.curveMonotoneX)
// 动态绘制折线图
function draw(index) {
var _lines = lines.map(d => d.slice(0, index + 1));
lineG.selectAll('*').remove();
lineG.selectAll('path')
// point
lineG.selectAll('circle')
.data(_lines)
.enter()
.append('g')
.selectAll('circle')
.data(d => d)
.enter()
.append('circle')
.attr('cx', (d) => scaleX(d.日期))
.attr('cy', (d) => scaleY(d.累计金牌))
.attr('r', 5)
.attr("stroke-width", 3)
.attr("fill", 'white')
.on('mouseover', function (d) {
d3.select(this).attr('r', 10);
tips.style('display', 'block')
.style('left', d3.event.pageX + 10 + 'px')
.style('top', d3.event.pageY + 10 + 'px')
.html(`
<div>日期:${d.日期}</div>
<div>金牌:${d.金牌}</div>
<div>银牌:${d.银牌}</div>
<div>铜牌:${d.铜牌}</div>
<div>总计:${d.总计}</div>
`)
})
.on('mouseout', function (d) {
d3.select(this).attr('r', 5);
tips.style('display', 'none');
})
// text
lineG.selectAll('text')
.data(_lines)
.enter()
.append('g')
.selectAll('text')
.data(d => d)
.enter()
.append('text')
.attr('x', (d) => scaleX(d.日期))
.attr('y', (d) => scaleY(d.累计金牌))
.attr('dx', 10)
.attr('dy', 5)
.attr('fill', (d, i) => {
var _j = countrys.indexOf(d.国家);
return colors[_j];
})
.text((d) => d.累计金牌)
lineG.selectAll('image')
.data(_lines)
.enter()
.append('g')
.selectAll('image')
.data(d => d)
.enter()
.append('image').filter((d, i, it) => {
return i === it.length - 1;
})
}
创建了一个名为tips的div元素,用于显示鼠标悬停时的信息。然后,创建了一个名为lineG的SVG元素,并定义了一个名为line的折线生成器。接下来,它定义了一个名为draw的函数,该函数接受一个索引参数,并根据该索引绘制折线图。
在draw函数中,它首先根据索引值获取数据,并清空lineG元素。然后,它使用d3.line()方法创建一个折线生成器,并设置x和y轴的比例尺。接着,它遍历数据,为每个数据点创建一个路径元素,并设置相应的属性。同时,它还为每个数据点添加了鼠标悬停事件,以显示提示信息。
此外,还为每个数据点添加了圆形元素作为标记,并设置了相应的属性。当鼠标悬停在圆形元素上时,它会放大圆形元素的大小;当鼠标离开圆形元素时,它会恢复圆形元素的原始大小。最后,它还为每个数据点添加了文本元素,以显示累计金牌数。
最后,它使用setInterval()方法定时调用draw函数,以动态更新折线图。当索引值达到dateArray的长度时,它会清除定时器。
绘制表格
js
d3.csv("./2021东京奥运会奖牌数据.csv").then(function (data) {
// 选取2021/7/25 数据的前10名
var dataD1 = data.filter((item) => {
return item.日期 == '2021/7/25'
}).sort((a, b) => {
return b.总计 - a.总计
}).slice(0, 10);
// 选取2021/7/27 数据的前10名
var dataD2 = data.filter((item) => {
return item.日期 == '2021/7/27'
}).sort((a, b) => {
return b.总计 - a.总计
}).slice(0, 10);
// 选取2021/8/2 数据的前10名
var dataD3 = data.filter((item) => {
return item.日期 == '2021/8/2'
}).sort((a, b) => {
return b.总计 - a.总计
}).slice(0, 10);
// 添加排名
dataD1.forEach((item, index) => {
item.排名 = index + 1
})
dataD2.forEach((item, index) => {
item.排名 = index + 1
})
dataD3.forEach((item, index) => {
item.排名 = index + 1
})
draw(dataD1, '2021/7/25 奖牌榜排行榜');
draw(dataD2, '2021/7/27 奖牌榜排行榜');
draw(dataD3, '2021/8/2 奖牌榜排行榜');
function draw(tableData, info) {
d3.select("body").append('h4').text(info)
.style("text-align", "center")
let table = d3.select("body").append("table")
.style("margin", "auto")
.style("border-collapse", "collapse")
.style("border", "1px black solid")
.style("width", "80%")
const title = [
{
name: '排名',
width: 10,
},
{
name: '国家',
width: 10,
},
{
name: '国家编码',
width: 10,
},
{
name: '金牌',
width: 10,
},
{
name: '银牌',
width: 10,
},
{
name: '铜牌',
width: 10,
},
{
name: '总计',
width: 10,
},
{
name: '国旗',
width: 10,
},
]
var thead = table.append("thead");
var tbody = table.append("tbody");
thead.append("tr")
.selectAll("th")
.data(title)
.enter()
.append("th")
.text(function (d) {
return d.name;
})
.style("border", "1px black solid")
.style("padding", "5px")
.style("width", function (d) {
return d.width + "%";
})
.style("background-color", "lightgray");
var rows = tbody.selectAll("tr")
.data(tableData)
.enter()
.append("tr")
.style("border", "1px black solid")
.style("text-align", "center")
var cells = rows.selectAll("td")
.data(function (row) {
return title.map(function (column) {
return {
column: column.name,
value: row[column.name]
};
});
})
.enter()
.append("td")
.html(d => {
if (d.column == '国旗') {
return `<img src="${d.value}" width="50" />`
} else {
return d.value
}
})
.style("border", "1px black solid")
}
});
使用D3.js读取CSV文件,并对其中的数据进行处理和可视化。首先,它筛选出特定日期(如2021/7/25、2021/7/27和2021/8/2)的数据,并按照总计奖牌数降序排列,取前10名。然后,为每个数据点添加排名属性。最后,调用draw函数绘制三个不同日期的奖牌榜排行榜。
在draw函数中,首先创建一个表格,并设置表头和样式。接着,根据传入的tableData数据,生成表格的每一行和每一列。对于国旗列,使用<img>
标签插入对应的国旗图片。其他列则直接显示对应的值。