【d3.js实战】2020东京奥运会奖牌数据可视化

2020东京奥运会奖牌数据可视化

很早我写了一个关2020东京奥运会奖牌数据可视化的页面,使用了d3.js库进行数据的处理和展示。这篇实战将详细讲解。

题目要求:

  1. 输出部分数据集:将给定的数据集数据进行输出(到页面中),输出的时间为:2021 年 7 月 25 日奖牌榜 前 10 名、2021 年 7 月 27 日和 2021 年 8 月 2 日奖牌榜前 10 名。
  2. 动态显示美国、中国、日本和澳大利亚的金牌折线图。
    1. 筛选出中国、日本、美国、澳大利亚的金牌数据,绘制折线图;
    2. 如样例 GIF 所示,动态地将各个国家的金牌数据折线图进行可视化。
    3. 每个数据节点添加交互,放大显示本日的各国奖牌数据,金牌、银牌、铜牌(样例 中只显示金牌数据)。

读取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;
});
});
  1. 读取CSV文件:"d3.csv("./2021东京奥运会奖牌数据.csv")" 这行代码使用D3库读取名为 "2021东京奥运会奖牌数据.csv" 的文件。

  2. 提取日期数据:通过遍历数据集,将每个对象的 "日期" 属性提取出来,存储在 "dataArray" 数组中。

  3. 去重:使用 Set 对象对 "dataArray" 数组进行去重操作,得到 "dateArray" 数组。

  4. 按日期排序:使用自定义的比较函数对 "dateArray" 数组进行排序,按照日期从早到晚的顺序排列。

  5. 筛选国家数据:根据给定的国家列表('中国', '美国', '日本', '澳大利亚'),从原始数据集中筛选出这些国家的奖牌数据,存储在 "countryData" 数组中。

  6. 构建数据结构:创建一个二维数组 "lines",用于存储每个国家的奖牌数据。遍历 "countryData" 数组,将每个国家的奖牌数据添加到对应的 "lines" 子数组中。

  7. 补全缺失数据:遍历 "lines" 数组,对于每个国家的子数组,检查是否包含当天的数据。如果没有,则添加一条新的记录,其中日期为当前遍历的日期,其他国家信息与第一个国家的相同,金牌、银牌和铜牌数量都为0。

  8. 排序:对每个国家的子数组按照日期进行排序。

  9. 累计金牌:遍历每个国家的子数组,计算累计金牌数,并将结果存储在每个对象的 "累计金牌" 属性中。

最后,代码输出了处理后的 "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>标签插入对应的国旗图片。其他列则直接显示对应的值。

相关推荐
东方翱翔4 分钟前
CSS的三种基本选择器
前端·css
Fan_web26 分钟前
JavaScript高级——闭包应用-自定义js模块
开发语言·前端·javascript·css·html
yanglamei196234 分钟前
基于GIKT深度知识追踪模型的习题推荐系统源代码+数据库+使用说明,后端采用flask,前端采用vue
前端·数据库·flask
千穹凌帝35 分钟前
SpinalHDL之结构(二)
开发语言·前端·fpga开发
dot.Net安全矩阵1 小时前
.NET内网实战:通过命令行解密Web.config
前端·学习·安全·web安全·矩阵·.net
Hellc0071 小时前
MacOS升级ruby版本
前端·macos·ruby
前端西瓜哥1 小时前
贝塞尔曲线算法:求贝塞尔曲线和直线的交点
前端·算法
又写了一天BUG1 小时前
npm install安装缓慢及npm更换源
前端·npm·node.js
cc蒲公英1 小时前
Vue2+vue-office/excel 实现在线加载Excel文件预览
前端·vue.js·excel
Java开发追求者1 小时前
在CSS中换行word-break: break-word和 word-break: break-all区别
前端·css·word