D3.js 介绍与入门

D3.js(全称:Data-Driven Documents)数据驱动文档是一个基于数据驱动 DOM 的 JS 库。

相比EChart、G2…之类的封装好的图标库,D3就像一个Jquery。
封装了很多函数供开发者使用。

为什么总觉得D3难

  1. 不常用就会忘记。当我们面对一个简单图表时永远首选会使用EChart一类封装好的图表库。
  2. D3的案例很多,但是D3的API变化也较多。网上会有很多个人写的案例,根据网友写的案例去学习,经常性会有示例代码无法运行的问题。
  3. D3.js的教程很多、但好的教程相对较少。D3的官方教程是以核心概念为主。

今天这次分享最主要想推荐一本书: fullstack d3.js。

fullstack d3.js 是我目前觉得最适合入门的教程。整本书是循序渐进的教学方式,并且总结了D3绘图的7个步骤,非常推荐大家完整的阅读一遍。

认识D3的组成

D3现在已经拆成了单独的模块,可以单独引用。

我们可以简单的把d3的模块按照功能做个简单分类

  • 获取数据:d3-dsv、d3-fetch
  • 操作数据:d3-array、d3-random、d3-collection
  • 操作DOM:d3-selection
  • 绘制SVG图形:d3-path、d3-polygon、d3-shape
  • 比例尺:d3-scale
  • 处理颜色:d3-color、d3-hsv、d3-interpolate、d3-scale-chromatic
  • 处理时间:d3-time-format、d3-time、d3-timer
  • 动画:d3-interpolate、d3-transition、d3-ease、d3-timer
  • 地图:d3-geo
  • 特定的可视化图形:d3-quadtree、d3-force、d3-hierarchy、d3-brush、d3-chord、d3-axis、d3-voronoi、d3-contour
  • 交互:d3-drag、d3-zoom、d3-dispatch

作者对d3.js的介绍与分类

绘制任何图表的步骤

我们每次制作图表时都需要采取一般步骤

  1. 获取数据

    查看数据结构并声明如何获取我们需要的值

  2. 设置图表尺寸

    声明图表的参数(宽高之类的)

  3. 绘制画布

    渲染图表区域

  4. 创建比例尺

    为图表中的每个数据到物理像素创建比例尺

  5. 绘制数据

    渲染数据元素

  6. 绘制其他部分

    绘制坐标轴、标签和图例等等

  7. 设置交互

添加事件监听、交互
在这里插入图片描述

我们要做一个散点图

这里我们用一年的天气数据的json作为我们的数据来源。
根据天气数据的湿度露点(结露的温度)
用散点图展示一个这一年每天湿度露点的关系

Access data 获取数据

获取数据比较简单,d3提供了各种获取数据的函数如d3.json()之类的。

湿度和露点我们要分别做我们X和Y的数据

const dataset = await d3.json("./data/my_weather_data.json")

const xAccessor = d => d.dewPoint
const yAccessor = d => d.humidity

Create chart dimensions 设置图表尺寸

我们需要定义图表的尺寸。通常,散点图为正方形,X轴的宽度与Y轴的高度相同。

要制作正方形图表,我们希望高度与宽度相同。
我们直接使用窗口的高度或宽度乘以0.9,给窗口留0.1的空白。

 // 2. Create chart dimensions

  const width = d3.min([
    window.innerWidth * 0.9,
    window.innerHeight * 0.9,
  ])

为什么一定要明确图表的尺寸?
在Web开发时,我们经常让元素去自适应大小。
在d3做图时明确图表尺寸对我们有更重要的原因

  • 如果是SVG元素自适应的缩放可能会导致不一致

  • 我们需要知道图表的宽度和高度,以便计算比例尺输出

  • 能更好地控制图表元素的大小

  • wrapper 是整个 SVG 元素,包含轴、数据元素和图例

  • bounds 位于 wrapper 内,仅包含数据元素

bounds 周围的要留边距为图表的其他元素(轴、图例)分配空间,同时允许图表区域根据可用空间动态调整大小。
在这里插入图片描述

 // 2. Create chart dimensions

  const width = d3.min([
    window.innerWidth * 0.9,
    window.innerHeight * 0.9,
  ])

  let dimensions = {
    
    
    width: width,
    height: width,
    margin: {
    
    
      top: 10,
      right: 10,
      bottom: 50,
      left: 50,
    },
  }

  dimensions.boundedWidth = dimensions.width
    - dimensions.margin.left
    - dimensions.margin.right
  dimensions.boundedHeight = dimensions.height
    - dimensions.margin.top
    - dimensions.margin.bottom

Draw canvas 绘制画布

找到一个现有的DOM元素(#wrapper),添加一个<svg>进去

然后我们使用 attr 来设置 <svg> 的尺寸。

Note that these sizes are the size of the “outside” of our plot. Everything we draw next will be within this <svg>.

const wrapper = d3.select("#wrapper")
  .append("svg")
  .attr("width", dimensions.width)
  .attr("height", dimensions.height)

在上面,我们创建了一个 元素,使用transform CSS属性将其向右和向下移动,来当我们的边距

const bounds = wrapper.append("g")
  .style("transform", `translate(${
      
      
    dimensions.margin.left
  }px, ${
      
      
    dimensions.margin.top
  }px)`)

Create scales 比例尺

在绘制数据之前,我们需要思考如何将数字从数据域转换到像素域。

让我们从X轴开始。我们想根据露点来决定每天的点的水平位置

为了找到这个位置,我们使用了d3 scale object,它可以帮助我们将数据映射到像素。

让我们创建一个刻度,它将采用露点(温度),并告诉我们一个点需要向右移动多远。

这将是线性标度,因为输入(露点)和输出(像素)将是线性增加的数字。

const xScale = d3.scaleLinear()

比例尺的概念

我们需要告诉我们的比例尺:

  • 需要处理哪些输入(域)
  • 我们想要返回的输出(范围)

举个简单的例子,假设数据集中的温度范围为
0到100度。在这种情况下,将温度转换为像素很容易:温度为50
度映射到50个像素,因为范围和域都是[0,100]。
但我们的数据和像素输出之间的关系很少如此简单。
比例尺就可以帮我们完成数据的等比转换。比例尺是D3的亮点之一。

在这里插入图片描述

确定范围

为了创建比例,, 我们需要选择要处理的最小值和最大值

D3有一个辅助函数,我们可以在这里使用: d3.extent() 接受两个参数.(extent:范围)。直接获取最大值和最小值

从数据点提取度量值的访问器函数。如果没有
如果指定,则默认为恒等函数d=>d。

  1. 数组
  2. 从数据点提取度量值的访问器函数、默认为恒等函数d=>d。
const xScale = d3.scaleLinear()
  .domain(d3.extent(dataset, xAccessor))
  .range([0, dimensions.boundedWidth])

这个比例尺生成的结果是[-7.22, 73.83]。我们的x轴最左侧代表-7.22最右代表73.83

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PDfkJD7P-1687759984798)(./images/d3/在这里插入图片描述
.png)]

虽然能用,但如果第一个和最后一个刻度线是整数,则更容易读取坐标轴。
D3 Scales有一个.nice()方法,该方法将对我们的Scale域进行四舍五入,从而为我们的X轴提供更友好的边界。
我们可以通过查看使用.nice()之前和之后的值来查看.nice()如何修改我们的X刻度的定义域。

不带参数调用.domain()将输出刻度的现有域

console.log(xScale.domain()) // [-7.22, 73.83]
xScale.nice()
console.log(xScale.domain()) // [10, 80]
const xScale = d3.scaleLinear()
  .domain(d3.extent(dataset, xAccessor))
  .range([0, dimensions.boundedWidth])
  .nice()

const yScale = d3.scaleLinear()
  .domain(d3.extent(dataset, yAccessor))
  .range([dimensions.boundedHeight, 0])
  .nice()

Draw data 绘制数据

重点来了!绘制散点图的我们需要使用<circle>元素。
在这里插入图片描述
cx: 圆心x坐标
cy: 圆心y坐标
r: 半径

bounds.append("circle")
  .attr("cx", dimensions.boundedWidth / 2)
  .attr("cy", dimensions.boundedHeight / 2)
  .attr("r", 5)

data.forEach(d => {
    
    
  bounds
    .append("circle")
    .attr("cx", xScale(xAccessor(d)))
    .attr("cy", yScale(yAccessor(d)))
    .attr("r", 5)
})

在这里插入图片描述
这种画点的方法虽然能跑,但有几个问题

  • 嵌套多了,这使我们的代码更难理解。
  • 函数调用两次,我们最终将绘制两组。

大家期望的最好的结果肯定是根据数据来渲染<circle>

Data joins

忘掉上边代码哈。

我们用要开始用D3的选择器,选择所有的<circle>元素

const dots = bounds.selectAll("circle")

这一点和Jquery的选择器就不一样了。我们直接执行bounds.selectAll("circle")的时候,画布上还没有任何元素。

这里就需要我们转换到D3的思路上来。

D3选择的选择器,它知道数据对应的哪些元素已经存在。如果我们已经绘制了数据的一部分,该选择器将知道已经绘制了哪些点,以及需要添加哪些点。

我们用.data()方法把数据传递给选择的对象。

const dots = bounds.selectAll("circle")
  .data(dataset)

当我们调用.data()时,我们将所选元素与数据点数组连接在一起。
返回的选择将包含现有元素、需要添加的新元素和需要删除的旧元素

我们将以三种方式查看对选择对象的这些更改:
我们的选择对象被更新以包含现有DOM元素和数据点之间的任何重叠。
添加了一个_enter键,用于列出尚未呈现元素的任何数据点。
添加了_exit键,用于列出已呈现但不在所提供的数据集中的任何数据点。
在这里插入图片描述

可以在控制台看一下

let dots = bounds.selectAll("circle")
console.log(dots)
dots = dots.data(dataset)
console.log(dots)

当前选定的DOM元素位于_groups键下。在我们将数据集加入之前,只包含一个空数组。

在这里插入图片描述

但是,下一个选择对象看起来不同。我们有两个新键:_enter和_exit,并且我们的_groups数组有一个具有365个元素的数组

在这里插入图片描述

看_enter键。如果我们展开数组并查看其中一个值,我们可以看到一个具有数据属性的对象。
在这里插入图片描述
如果我们展开__data__,将看到我们的数据点
我们可以看到 _enter 中的每个值都对应于数据集中的一个值.
_exit值是一个空数组—如果我们要删除现有元素,我们能在这里看到。

为了对新元素进行操作,我们可以使用enter方法创建一个仅包含这些元素的D3 selection 对象。
在这里插入图片描述
为每个数据点附加一个<circle>。我们可以使用.append()方法,D3将为每个数据点创建一个元素。
这里我们也直接给圆设置一下x,y坐标和半径

const dots = bounds.selectAll("circle")
    .data(dataset)
  .enter().append("circle")
    .attr("cx", d => xScale(xAccessor(d)))
    .attr("cy", d => yScale(yAccessor(d)))
    .attr("r", 5)
    .attr("fill", "cornflowerblue")

Data join exercise 数据连接练习

下面是一个简单的示例,可以更直观地了解数据连接概念。

function drawDots(dataset, color) {
    
    
  const dots = bounds.selectAll("circle").data(dataset)

  dots
    .enter().append("circle")
    .attr("cx", d => xScale(xAccessor(d)))
    .attr("cy", d => yScale(yAccessor(d)))
    .attr("r", 5)
    .attr("fill", color)
}

drawDots(dataset.slice(0, 200), "darkgrey")

一秒钟后,让我们使用整个数据集再次调用该函数,这次使用蓝色。
setTimeout(() => {
    
    
  drawDots(dataset, "cornflowerblue")
}, 1000)

第一次执行
在这里插入图片描述

在这里插入图片描述

如果单纯从函数调用来说,第二次调用时应该把所有的圆圈全部设置成了蓝色。但是我们能看到灰色的并没有变蓝。

分析一下:第二次调用时,365个<circle>已经有200个存在了。所以_enter的补分是剩下的165个点,这165个点被设置成了蓝色。

如果我们想要设置所有圆的颜色
D3 selection 有一个merge()方法,该方法将当前选择与另一个选择合并。
在这种情况下,我们可以将新的enter 选择与原始的dots选择组合在一起。然后更新的时候就会更新所有的点。

function drawDots(data, color) {
    
    
  const dots = bounds.selectAll("circle").data(dataset)

  dots
    .enter().append("circle")
    .merge(dots) // 合并到一起更新
      .attr("cx", d => xScale(xAccessor(d)))
      .attr("cy", d => yScale(yAccessor(d)))
      .attr("r", 5)
      .attr("fill", color)
}

.join()

.join() 是一个.enter(), .append(), .merge()…(还有些我们没用到的)的快捷方式

function drawDots(data, color) {
    
    
  const dots = bounds.selectAll("circle").data(dataset)

  dots.join("circle")
    .attr("cx", d => xScale(xAccessor(d)))
    .attr("cy", d => yScale(yAccessor(d)))
    .attr("r", 5)
    .attr("fill", color)
}

drawDots(data.slice(0, 200), "darkgrey")

setTimeout(() => {
    
    
  drawDots(data, "cornflowerblue")
}, 1000)

.join() 函数能让我们更方便的使用D3
但是.enter(), .append(), .merge() 之类的基础方法还是要了解的。

Draw peripherals 绘制次要内容

主要内容绘制完毕了,我们现在要绘制一下坐标轴

我们可以用d3.axisBottom(),用来生成x轴

轴生成器需要知道

  • domain获取X刻度
  • range获取尺寸

const xAxisGenerator = d3.axisBottom().scale(xScale)

// const xAxis = bounds.append("g")
// xAxisGenerator(xAxis)
// 这样也可以生效,但是会导致链式调用断掉

const xAxis = bounds.append("g")
  .call(xAxisGenerator)
  .style("transform", `translateY(${
      
      dimensions.boundedHeight}px)`)

然后我们标注一下x轴是什么

const xAxisLabel = xAxis.append("text")
  .attr("x", dimensions.boundedWidth / 2)
  .attr("y", dimensions.margin.bottom - 10)
  .attr("fill", "black")
  .style("font-size", "1.4em")
  .html("Dew point (&deg;F)")

在这里插入图片描述

Y轴类似,但是略微不同
我们可以用ticks设置刻度。

const yAxisGenerator = d3.axisLeft()
  .scale(yScale)
  .ticks(4)

const yAxisLabel = yAxis.append("text")
  .attr("x", -dimensions.boundedHeight / 2)
  .attr("y", -dimensions.margin.left + 10)
  .attr("fill", "black")
  .style("font-size", "1.4em")
  .text("Relative humidity") // // 相对湿度
  .style("transform", "rotate(-90deg)")
  .style("text-anchor", "middle")

在这里插入图片描述

adding a color scale 添加颜色比例尺

散点图最直观的是x,y两个维度,不过我们可以通过颜色或者大小添加更多的维度

我们的数据里有cloudCover数值,我们可以通过添加颜色来显示云量是如何根据湿度和露点变化的。

const colorAccessor = d => d.cloudCover

// 刻度还可以将数字转换为颜色—我们只需要将域替换为一系列颜色
const colorScale = d3.scaleLinear()
    .domain(d3.extent(dataset, colorAccessor)) 
    .range(["skyblue", "darkslategrey"])


// 回到第五步,把颜色给替换掉

const dots = bounds.selectAll("circle")
  .data(dataset)
  .enter().append("circle")
    .attr("cx", d => xScale(xAccessor(d)))
    .attr("cy", d => yScale(yAccessor(d)))
    .attr("r", 4)
    // .attr("fill", "cornflowerblue")
    .attr("fill", d => colorScale(colorAccessor(d)))
    .attr("tabindex", "0")
    

在这里插入图片描述

相关链接

英文官网教程

印记中文翻译的文档

github api列表

fullstack d3作者博客,有很多可视化相关文章非常值得一看

fullstack d3教程网址

猜你喜欢

转载自blog.csdn.net/kang_k/article/details/131396429
今日推荐