赞
踩
D3.js(全称:Data-Driven Documents)数据驱动文档是一个基于数据驱动 DOM 的 JS 库。
相比EChart、G2…之类的封装好的图标库,D3就像一个Jquery。
封装了很多函数供开发者使用。
今天这次分享最主要想推荐一本书: fullstack d3.js。
fullstack d3.js 是我目前觉得最适合入门的教程。整本书是循序渐进的教学方式,并且总结了D3绘图的7个步骤,非常推荐大家完整的阅读一遍。
D3现在已经拆成了单独的模块,可以单独引用。
我们可以简单的把d3的模块按照功能做个简单分类
我们每次制作图表时都需要采取一般步骤
获取数据
查看数据结构并声明如何获取我们需要的值
设置图表尺寸
声明图表的参数(宽高之类的)
绘制画布
渲染图表区域
创建比例尺
为图表中的每个数据到物理像素创建比例尺
绘制数据
渲染数据元素
绘制其他部分
绘制坐标轴、标签和图例等等
设置交互
添加事件监听、交互
这里我们用一年的天气数据的json作为我们的数据来源。
根据天气数据的湿度
和露点
(结露的温度)
用散点图展示一个这一年每天湿度
和露点
的关系
获取数据比较简单,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
我们需要定义图表的尺寸。通常,散点图为正方形,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
找到一个现有的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)
在上面,我们创建了一个
const bounds = wrapper.append("g")
.style("transform", `translate(${
dimensions.margin.left
}px, ${
dimensions.margin.top
}px)`)
在绘制数据之前,我们需要思考如何将数字从数据域转换到像素域。
让我们从X轴开始。我们想根据露点来决定每天的点的水平位置。
为了找到这个位置,我们使用了d3 scale object,它可以帮助我们将数据映射到像素。
让我们创建一个刻度,它将采用露点(温度),并告诉我们一个点需要向右移动多远。
这将是线性标度,因为输入(露点)和输出(像素)将是线性增加的数字。
const xScale = d3.scaleLinear()
我们需要告诉我们的比例尺:
举个简单的例子,假设数据集中的温度范围为
0到100度。在这种情况下,将温度转换为像素很容易:温度为50
度映射到50个像素,因为范围和域都是[0,100]。
但我们的数据和像素输出之间的关系很少如此简单。
比例尺就可以帮我们完成数据的等比转换。比例尺是D3的亮点之一。
为了创建比例,, 我们需要选择要处理的最小值和最大值。
D3有一个辅助函数,我们可以在这里使用: d3.extent()
接受两个参数.(extent:范围)。直接获取最大值和最小值
从数据点提取度量值的访问器函数。如果没有
如果指定,则默认为恒等函数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()
重点来了!绘制散点图的我们需要使用<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>
忘掉上边代码哈。
我们用要开始用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")
下面是一个简单的示例,可以更直观地了解数据连接概念。
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()
之类的基础方法还是要了解的。
主要内容绘制完毕了,我们现在要绘制一下坐标轴
我们可以用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 (°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")
散点图最直观的是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")
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。