D3 从入门到实战 - 5

1,505 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 28 天,点击查看活动详情

前因

上一节中,我们了解到在 D3 中是如何根据动态数据进行动态更新,主要是通过 Data - Join 来处理相关的数据更新,这一小节咱们就一起通过一个实例来加强对 Data - Join 的使用和理解吧!

散点图

散点图,顾名思义就是由一些散乱的点组成的图表,这些点在哪个位置,是由其X值和Y值确定的。所以也叫做XY散点图。

我们要实现一个湖北2020年的疫情散点图,需要有一个基础的画布,跟前面一节的示例一样,我们首先创建一个画布,代码如下:

<svg width="800" height="600" id="svg" class="svgs" style="background-color: #ffffff;"></svg>

当画布已经准备好,我们就需要设置相关的基础属性了,还是跟前面一节的代码类似,如下:

const svg = d3.select('#svg');
const width = +svg.attr('width');
const height = +svg.attr('height');
const margin = {top: 100, right: 120, bottom: 100, left: 120};
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;

以上代码在前面一节已经讲解过相关原理,这里就不做相关的赘述了。

我们还需要添加横坐标与纵坐标的标题,以及它们的比例尺,代码如下:

let xScale, yScale;
const xAxisLabel = '累计确诊人数(对数)';
const yAxisLabel = '新增人数(对数)';

当然,我们还需要将湖北各个城市用不同的颜色来标示出来,代码如下:

const color = {
    "武汉":"#ff1c12",
    "黄石": "#de5991",
    "十堰": "#759AA0",
    "荆州": "#E69D87",
    "宜昌": "#be3259",
    "襄阳": "#EA7E53",
    "鄂州": "#EEDD78",
    "荆门": "#9359b1",
    "孝感": "#47c0d4",
    "黄冈": "#F49F42",
    "咸宁": "#AA312C",
    "恩施州": "#B35E45",
    "随州": "#4B8E6F",
    "仙桃": "#ff8603",
    "天门": "#ffde1d",
    "潜江": "#1e9d95",
    "神农架": "#7289AB"
}

基础的准备工作都已经做完了,下面我们就开始正式的编码。

因为我们要实现的是一个可视化的散点图,因此我们需要先获取数据,在前面的示例中,我们的数据是在前端界面写死的,这当然是不行的。在 D3 中我们一般是通过 d3.csv(path) 来读取一个远程的数据,之所有用 csv 这样的数据格式,是因为它本质上是纯文本,区别于 excle 的格式,但是它却能在 excle 中打开。

接下来我们就先读取一个 csv 文件,动态的获取数据,代码如下:

d3.csv("./static/data/hubeinxt.csv").then(data => {
    // ... data
});

我们通过 d3.csv 读取一个 csv 文件,返回的是一个 Promise,其中的 data 就是读取出来的数据,原始的 csv 文件数据如下图所示:

image.png

读取到的 data 是一个数组对象,其中就包括上图中的所有值,而这里我们需要筛选一下相关的字段来获取我们需要的数据,代码如下:

data = data.filter((d) => d["地区"] !== "总计");

data.forEach((d) => {
    d["确诊人数"] = +d["确诊人数"];
    d["新增确诊"] = +d["新增确诊"];
    if (d["新增确诊"] < 0) {
        d["新增确诊"] = 0;
    }
});

通过筛选,我们获取到一个新的数组,其中的确诊人数新增确诊转换成了数字类型,并且判断当新增确诊的数据小于0时,就直接设置为0。得到基础的数据后,我们还需要组装成我们能够使用的数据,代码如下:

allDates = Array.from(new Set(data.map((d) => d["日期"])));

allDates = allDates.sort((a: number, b: number) => {
  return new Date(a) - new Date(b);
});

sequantial = [];
allDates.forEach(() => {
  sequantial.push([]);
});
data.forEach((d) => {
  sequantial[allDates.indexOf(d["日期"])].push(d);
});

通过一系列的操作,我们最终得到了我们的坐标轴数据,接下来我们就需要将这些数据渲染在画布上了,因此我们需要有一个画布的初始方法,代码如下:

const renderInit = (data: Record<string, any>[]) => {
  xScale = d3
    .scaleLinear()
    .domain([d3.min(data, xValue), d3.max(data, xValue)])
    .range([0, iWidth])
    .nice();

  yScale = d3
    .scaleLinear()
    .domain(d3.extent(data, yValue).reverse())
    .range([0, iHeight])
    .nice();

  const g = svg
    .append("g")
    .attr("transform", `translate(${margin.left}, ${margin.top})`)
    .attr("id", "svg");

  const yAxis = d3.axisLeft(yScale).tickSize(-iWidth).tickPadding(10);

  const xAxis = d3.axisBottom(xScale).tickSize(-iHeight).tickPadding(10);

  let yAxisGroup = g.append("g").call(yAxis).attr("id", "yaxis");
  yAxisGroup
    .append("text")
    .attr("font-size", "2em")
    .attr("transform", "rotate(-90)")
    .attr("x", -iHeight / 2)
    .attr("y", -50)
    .attr("fill", "#333333")
    .text(yAxisLabel)
    .attr("text-anchor", "middle");
  yAxisGroup.selectAll(".domain").remove();

  let xAxisGroup = g
    .append("g")
    .call(xAxis)
    .attr("transform", `translate(0, ${iHeight})`)
    .attr("id", "xaxis");
  xAxisGroup
    .append("text")
    .attr("font-size", "2em")
    .attr("y", 60)
    .attr("x", iWidth / 2)
    .attr("fill", "#333333")
    .text(xAxisLabel);
  xAxisGroup.selectAll(".domain").remove();
};

跟前面一节的操作很类似,都是比较简单的操作。然后我们将数据渲染到画布上,最终的坐标轴如下图所示:

image.png

具体的效果可以狠戳这里

坐标轴已经准备好了,接下来还需要讲坐标轴中的散列点渲染到画布中,这个基本的散列图就完成了,我们还需要实现一个更新散列点的函数,代码如下:

const renderUpdate = (seq) => {
    const g = d3.select("#svg");

    let circleUpdates = g.selectAll("circle").data(seq, (d) => d["地区"]);

    let circleEnter = circleUpdates
        .enter()
        .append("circle")
        .attr("cx", (d) => xScale(xValue(d)))
        .attr("cy", (d) => yScale(yValue(d)))
        .attr("r", 10)
        .attr("fill", (d) => color[d["地区"]])
        .attr("opacity", 0.8);

    circleUpdates
        .merge(circleEnter)
        .transition()
        .ease(d3.easeLinear)
        .duration(aduration)
        .attr("cx", (d) => xScale(xValue(d)))
        .attr("cy", (d) => yScale(yValue(d)));
};

最终实现的效果可以狠戳这里

最后

这一小节我们主要是为了加强对上一节中 Data - Join 的学习和理解,并实现了一个基础的散列图示例,其中需要注意的就是 csv 数据的读取和处理。我们一起加油吧!

未完待续...

最后,如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,谢谢大家

往期回顾

D3 从入门到实战 - 1

D3 从入门到实战 - 2

D3 从入门到实战 - 3

D3 从入门到实战 - 4