D3.js(全称:Data-Driven Documents)数据驱动文档是一个基于数据驱动 DOM 的 JS 库。
相比EChart、G2…之类的封装好的图标库,D3就像一个Jquery。
封装了很多函数供开发者使用。
为什么总觉得D3难
- 不常用就会忘记。当我们面对一个简单图表时永远首选会使用EChart一类封装好的图表库。
- D3的案例很多,但是D3的API变化也较多。网上会有很多个人写的案例,根据网友写的案例去学习,经常性会有示例代码无法运行的问题。
- D3.js的教程很多、但好的教程相对较少。D3的官方教程是以核心概念为主。
今天这次分享最主要想推荐一本书: fullstack d3.js。
fullstack d3.js 是我目前觉得最适合入门的教程。整本书是循序渐进的教学方式,并且总结了D3绘图的7个步骤,非常推荐大家完整的阅读一遍。
认识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
绘制任何图表的步骤
我们要做一个散点图
这里我们用一年的天气数据的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做图时明确图表尺寸对我们有更重要的原因
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>
进去
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 比例尺
在绘制数据之前,我们需要思考如何将数字从数据域转换到像素域。
为了找到这个位置,我们使用了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刻度的定义域。
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)
})
这种画点的方法虽然能跑,但有几个问题
Data joins
忘掉上边代码哈。
const dots = bounds.selectAll("circle")
这一点和Jquery的选择器就不一样了。我们直接执行bounds.selectAll("circle")
的时候,画布上还没有任何元素。
D3选择的选择器,它知道数据对应的哪些元素已经存在。如果我们已经绘制了数据的一部分,该选择器将知道已经绘制了哪些点,以及需要添加哪些点。
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 绘制次要内容
轴生成器需要知道
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)`)
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)")
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")
相关链接
fullstack d3作者博客,有很多可视化相关文章非常值得一看
原文地址:https://blog.csdn.net/kang_k/article/details/131396429
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.7code.cn/show_14185.html
如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!