前言
上一篇文章「安利一些不错的D3.js资源 - 牛衣古柳 2021.06.29」的反响还不错,记得有新群友说是主管推给她文章才加过来的,也是很神奇。
一眨眼又一个月没更新了。其实一直有想写简单的 D3.js
入门文章/教程的打算,但总想着要写就写的全面细致些、有趣些、够通俗易懂些,甚至如果能对标 Daniel Shiffman
在 Processing、P5.js
等方面的输出,能真的让更多人更顺滑地入门 D3.js
可视化就好了。相关阅读:伴随 P5.js 入坑创意编程 - 牛衣古柳 2019.06.28
理想很丰满,现实很骨感。古柳自身水平不够就不提了,至今没积累多少案例可以支撑实现上面的目标,还经常因为一段时间没接触 D3.js
就忘个精光,再次拿起来用自己都磕磕绊绊,更何谈输出教程呢?
说起来也很沮丧,古柳一直觉得自己提供不了什么可视化相关有价值的内容
,如果连写入门教程的事也无法实现的话,真的不知道还能做些什么。
但一直拖着不行动也不行,仍然心有不甘。纵使无法一上来就输出较系统全面、够通俗易懂的教程,很多地方可能无法达到心中的目标,但姑且先行动起来,看看到底能写出什么样的内容再说。优化迭代等有所输出后再进行也来得及
。
因而就有了这篇文章,有了这个系列里的第一篇文章,至于本系列能写多少,到底会写成什么样,古柳也完全心里没数,就让时间来说明一切吧,另外虽然是奔着初学者也能轻松看懂的目标去的,但真的大家看完觉得有什么感受,古柳也不清楚,所以希望大家多多反馈,后续文章能改进的也继续改进
。
本系列配套代码和用到的数据都会开源到这个仓库,欢迎大家 Star,其他有任何问题可以群里交流:https://github.com/DesertsX/d3-tutorial
正文
基本代码结构
首先,介绍下代码结构,id为"chart"的div元素
将用于放后面添加的 SVG
画布;引入下载到本地的 D3.js
库(v5.9版本);JS 部分就是本次代码的重点,且都在 drawChart()
函数里实现。另外 CSS 样式主要是为后续画布能全屏撑满不留空白所用。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>D3.js 教程</title>
<style> * {
margin: 0;
padding: 0;
}
html,
body {
overflow: hidden;
}</style>
</head>
<body>
<div id="chart"></div>
<!-- 可以下载到本地也可以引用线上版本 -->
<script src="./d3.js"></script>
<!-- <script src="https://cdn.bootcdn.net/ajax/libs/d3/5.9.7/d3.js"></script> -->
<script> function drawChart() {
// code
}
drawChart()</script>
</body>
添加 SVG 画布
以下 JS 代码都是在 drawChart()
的。
用 D3.js
进行可视化,可以用矢量图的 SVG,也可以用标量图、像素的canvas,因为古柳 SVG 用的多些,这里就以此为例。
可视化画图过程简单说来就是把数据映射成视觉元素,再以特定方式布局到画布上
。其中视觉元素可以是散点图里的圆圈,柱形图、直方图里的矩形,折线图里的线条等等;布局核心是要知道每个元素的x/y坐标,可以是自己计算出来,也可以是 D3.js
自带的许多布局函数生成的。
接下来以矩形为例,带大家看看 D3.js
的一些用法。
首先需要一个 SVG 画布来放置后续的视觉元素,其实还会放标题/坐标轴/图例等等,这里可能还用不到,以后会介绍。
通过 d3.select()
选中 id 为 chart 的 div 元素,这里的#
就是表示id
,如果是class
就是.
,很简单的 CSS 选择器用法;
接着通过 append
添加 svg
元素,然后设置其的宽高和背景色,这里为了演示方便,设置成浏览器网页窗口高度的全部和宽度的一半,大家也可以撑满网页窗口,或者用固定大小如 900*600
等,视自己需求而定即可。
const width = window.innerWidth
const height = window.innerHeight
const svg = d3.select('#chart')
.append('svg')
.attr('width', width / 2)
.attr('height', height)
.style('background', '#FEF5E5')
其中 window.innerWidth
和 window.innerHeight
就是网页窗口在某一大小打开时的宽高,即图中红框部分,并且可以看到画布占了一半大小。
画布设置好后,就可以往里面添加视觉元素了,就像很多工具/软件都自带一些基本图形元素一样,SVG 也有 circle/rect/ellipse/polygon/line/path/text
等常用元素,并且每个元素可以设置相应属性,如位置、宽高、半径、颜色、描边、透明度等等(图片取自 fullstack d3),后续会逐渐介绍,都不复杂。
现在我们要在画布里画一个矩形/rect
,同样用 append
加上元素名即可,然后设置 x/y 位置坐标(矩形左上角的坐标,而不是中心点的坐标)、矩形宽高(数字均为像素值,如100就是100px)和颜色即可。
需要注意的是:直角坐标系原点在网页窗口左上角,水平向右是x轴正轴,垂直向下是y轴正轴。
代码语言:javascript复制svg.append('rect')
.attr('x', 30)
.attr('y', 50)
.attr('width', 50)
.attr('height', 100)
.attr('fill', '#00AEA6')
对应浏览器里生成的 HTML 的内容如下。
代码语言:javascript复制<svg width="518.5" height="680" style="background: rgb(254, 245, 229);">
<rect x="30" y="50" width="50" height="100" fill="#00AEA6"></rect>
</svg>
假如矩形画在画布边缘,超出画布部分是不可见的
。所以如果数据多了,就需要换行显示,后面会演示如何处理。
svg.append('rect')
.attr('x', width / 2 - 25)
.attr('y', 50)
.attr('width', 50)
.attr('height', 100)
.attr('fill', '#EB5C36')
上面演示了如何添加一个元素,但更多时候我们需要根据数据集来添加多个元素,那该如何操作呢?
可能有人想到可以遍历循环数据来添加元素......其实倒也不是完全不行。
构造简单数据
这里用 d3.range(20)
简单构造个包含0-19数字的数组——[0, 1, 2, ..., 19]
——作为演示的数据集;
const dataset = d3.range(20)
console.log(dataset) // [0, 1, 2, ..., 19]
const colors = ['#00AEA6', '#DB0047', '#F28F00', '#EB5C36', '#242959', '#2965A7']
并且准备了6种颜色,模拟可视化时将某一类别型属性映射成不同颜色的情况。配色取自于此图,很好看有没有,可是古柳静心挑选的!
因为颜色数据也是数组,而取数组里某项元素可以通过索引来进行,比如取第一个颜色就是 colors[0]
,索引从0开始到数组长度减1结束,即 colors.length - 1
,对应颜色是 colors[colors.length - 1]
,都是比较基础的 JS。
遍历数据来添加元素
接着遍历数据来添加元素就可以这样实现,当然用 for 循环也可以,这里简单着来,采用 forEach
遍历每项元素,d
依次是0-19每个数字,如果一行排列,可以间隔 70px
排开,d * 70
相当于就是等差数列;由于会超出画布所以无法显示全部。
dataset.forEach(d => {
svg.append('rect')
.attr('x', 20 d * 70)
.attr('y', 20)
.attr('width', 50)
.attr('height', 100)
.attr('fill', colors[d % colors.length])
})
其中每个矩形颜色是用数字对颜色数组长度取余数后作为索引值,然后从颜色数组里取色。数值的取整取余
是很好用的操作,后续也会常常出现,下面是具体取余的一些例子。
0 % 6 => 0 => colors[0]
1 % 6 => 1 => colors[1]
2 % 6 => 2 => colors[2]
...
5 % 6 => 5 => colors[5]
6 % 6 => 0 => colors[0]
7 % 6 => 1 => colors[1]
...
19 % 6 => 1 => colors[1]
D3.js 基于数据添加元素的方式
回到空白画布,下面的代码实现了和上面遍历循环一样的效果。
遍历循环数据来添加元素虽然有时候可行,但一般不会这么实现,更一般的、更 D3.js
的方式是用这样一组命令 .selectAll('rect').data(dataset).join('rect')
来基于数据添加元素。
const rects = svg.selectAll('rect')
.data(dataset)
.join('rect')
.attr('x', d => 20 d * 70)
.attr('y', 20)
.attr('width', 50)
.attr('height', 100)
.attr('fill', d => colors[d % colors.length])
想来很多人第一次接触这一方式,都会觉得很奇怪吧?要用数据绘制矩形,需要先 selectAll('rect')
选中所有矩形,可现在明明画布为空,并不存在 rect 元素,仿佛选了个寂寞?后面 .data(dataset)
就是把数据集绑定到选中的元素上;.join('rect')
是实际添加元素的操作。
接着每个元素的属性通过回调函数的方式进行设置,其中 d
就是 dataset
里每一项的数据。固定值的属性可以直接写死,无需函数写法。
这里暂时不做过多解释,其实真实原因是古柳也解释不好,还要牵扯出 enter-update-exit
等一套概念((图片同样取自 fullstack d3)),很多人估计入门时就被这些概念绕晕了,所以目前大家只需要记住这是常规操作、很重要,绑定数据进行绘制元素时会频繁用到,而且记牢这三句即可。
当然大家看网上例子,一定会看到类似下面的写法,其中 .enter().append()
是以前版本 D3.js
的写法,用 .join()
替换即可,少写一句不也挺好;function() {}
也可以用 ES6
的箭头函数 =>
替换,更简洁方便,推荐大家学些基础 JS 后也都像上面那样写。
const rects = svg.selectAll('rect')
.data(dataset)
.enter()
.append('rect')
.attr('x', function (d) {
return 20 d * 70
})
.attr('y', 20)
.attr('width', 50)
.attr('height', 100)
.attr('fill', function (d) {
return colors[d % colors.length]
})
调整布局,换行显示
在上面的例子中,矩形都是一行排列,数据一多就会超出画布,接下来调整下布局,实现换行显示的效果。
x 坐标的计算公式是 20 d * 70
,这里希望每一行的最后一个矩形整体都在画布内,即 x 坐标加上矩形宽度要小于画布宽度
。由此可以计算出一行最多放多少个矩形,以 col_num
命名,注意这里第 n 个元素对于的 d 其实是 n-1,因为 d 是从0开始的,元素确实从第一个元素开始的。
// 公式
20 (col_num - 1) * 70 50 <= witdh / 2
// 等同于
col_num <= witdh / 2 / 70
计算公式如上,因为除法有小数,这里需要向下取个整数,用 Math.floor()
或 parseInt
均可。
const col_num = parseInt(width / 2 / 70)
// const col_num = Math.floor(width / 2 / 70)
console.log(col_num)
算出每列的个数后,就能继续用上文提到的取整取余操作来计算每个元素的x/y坐标,其本质就是需要知道每个元素在哪一行哪一列。
代码语言:javascript复制const dataset = d3.range(50)
const rects = svg.selectAll('rect')
.data(dataset)
.join('rect')
.attr('x', d => 20 d % col_num * 70)
.attr('y', d => 20 Math.floor(d / col_num) * 120)
.attr('width', 50)
.attr('height', 100)
.attr('fill', d => colors[d % colors.length])
比如每一行x坐标等差变化,通过 d % col_num
取余得到元素在每一行里的位置并计算到x坐标上;每一列y坐标等差变化,通过 Math.floor(d / col_num)
取整得到元素在每一列里的位置并计算到y坐标上。这里初学者如果没理清的话可以再梳理下。
需要注意的是上面改了 dataset,生成0-49的50条数据,以方便尽量撑满画布。所以截止目前,通过运用取余取整操作,在画布上较好的绘制出了所有数据。
但如果当数据更多时,超出最大高度又该怎么办呢?
也许可以缩小矩形宽高,然后调节间距一步步搞定。(这里古柳就不调了,主要是引出这个问题)
代码语言:javascript复制const dataset = d3.range(100)
const rects = svg.selectAll('rect')
.data(dataset)
.join('rect')
.attr('x', d => 20 d % col_num * 70)
.attr('y', d => 20 Math.floor(d / col_num) * 120)
.attr('width', 50 / 2)
.attr('height', 100 / 2)
.attr('fill', d => colors[d % colors.length])
但是否能基于数据大小和画布宽度来自动计算出每个rect的宽高和间距,然后自动布局呢?
正好古柳之前啃大西洋手抄本
可视化作品源码时看到了能解决上述问题的实现方式,将在下一篇文章分享给大家,更多 D3.js
内容也将会在下一篇文章继续展开讲解,敬请期待。
相关阅读:迄今复现过最复杂的可视化作品之「大西洋古抄本」(上) - 牛衣古柳 2021.06.17、迄今复现过最复杂的可视化作品之「大西洋古抄本」(下) - 牛衣古柳 2021.06.22