由于想对IMR磁道读写过程进行可视化,以丰富实验内容,最近一段时间在调研并学习d3.js来实现快速的可视化。这篇博客就记录一下学习过程中的一些重点内容,方便编码过程中的查阅以及以后有需要时可以很快捡起来。

D3:Data-Driven Documents

参考资料

  1. https://d3js.org/ (官方网站,包括文档、样例)
  2. https://developer.mozilla.org/zh-CN/docs/Web/SVG/Attribute (SVG属性表)
  3. https://github.com/d3/d3/wiki/Gallery (官方样例的仓库)
  4. https://observablehq.com/@d3/gallery (官方样例的仓库)
  5. https://github.com/xswei/d3js_doc(d3.js资源汇总,包括示例、书籍、API文档等)

运行环境

搭建简易Web开发环境,例如 python Flask + d3.js

重点内容

d3.js介绍

对于d3.js来说,重要的HTML标签主要有两个:

  • <script> : D3.js的编程主要写在这个标签中,为JavaScript脚本
  • <svg> : 对D3来说最重要的标签,是主要操作的对象(画布)

导入D3.js

1
<script src="https://d3js.org/d3.v5.min.js"></script>

也更加建议先下载到本地,再以本地路径来导入。

SVG—可缩放矢量模型

svg是D3.js主要操作对象,是一个容器,用于包含画在上面的各个图元。

svg作为矢量图,不会随着图片的缩放而失真。

可以通过d3.select来获取svg对象:

1
const svg = d3.select('svg');

一些必要的JavaScript语法

熟悉js中函数定义的多种形式:

  • function abc(a){return a+a;}
  • let f = datum => datum.value;
  • const p = function(a,b){return a+b;}
  • let myFunction = (a,b) =>a+b;
  • let f = (d,i) => {console.log(d); return d+i;}

我比较熟悉第三种,即let f = funciton(a,b){…}这种类型。但是其他的类型看到也要明白。

回调函数:

用于实现异步编程,通常会把函数作为变量输入,如

1
2
3
setTimeout(function(){
console.log('hello world!')
},1000);

D3中常用的接口:

  • 模板字符串: let myString = ‘abc-${a}’; (若a为数字10,那么结果为abc-10)

  • 数组:a = [1,2,3]

  • 对象:a = {name: “zzm”, age: 22, lab: ‘cpss’}

  • 数组排序: a.sort(),或者加入回调函数来替代缺省的排序方案:

    1
    2
    3
    a.sort(function(x,y){
    return new Date(x.date)-new Date(y.date);
    })
  • 数组查询:a.find(d=>d.name === ‘Wen-Yang’)

  • 将数字字符串转化成数值:+(‘3.14’)

D3.js还经常读取CSV,JSON等文件,涉及大量对象数组的操作。

D3.js语法基础

使用D3查询SVG

元素/图元有IDClass以及标签。ID是元素的唯一标识符;Class表示人为赋予元素的“类别”,不同元素的class可以相同;标签就是html的标签,使用标签往往难以直接索引到目标元素。

查询API:

1
2
3
4
// 只找一个元素
d3.select(...);
// 批量查询元素
d3.selectAll(...);

若选取元素的ID进行查询则需要在ID前加‘#’,同理,在Class前加’.’标签名前不加符号

:balloon:注意:查询可以层级来进行,例如:

1
d3.selectAll('#secondgroup rect')

这样的语句,会先找到id为secondgroup的标签,然后进一步地,找到该标签下所有rect标签的元素。

svg的属性

常见的属性:

  • id,class(特殊属性,可以用.attr设置)
  • x, y, cx, cy
  • fill, stroke
  • height, width, r
  • transform -> translate, rotate, scale

svg的属性非常多,不认识的可以查阅(https://developer.mozilla.org/zh-CN/docs/Web/SVG/Attribute)

屏幕空间的坐标系为:

0Z73LR_5LQ_P98PBSZZ_L5D.png

左上方是原点,x水平向右,y垂直向下

element.attr(…)

可以使用该方法设置和获取元素的属性,例如:

1
2
3
4
// 设置元素属性:element.attr('attr_name','attr_value')
rect1.attr('y',100);
// 获取元素属性:element.attr('attr_name')
let rect1_height = rect1.attr('y');

.attr()方法返回选择的图元本身,所以经常会使用链式调用的形式来对图元的属性进行操作:

1
selection.attr(…).attr(…).attr(…)

使用D3添加/删除SVG元素

  • 添加:element.append(…)

    1
    const myRect = d3.select('#mainsvg').append('rect').attr('x','100');
  • 删除:element.remove()

    方法会移除整个标签,不过应该小心使用。

    Tip: 在debug的使用可以用’opacity’属性hack出移除的效果,element.attr(‘opacity’,’0’);

数据读取—CSV数据

可以用d3.csv(…)来读取目标路径下的CSV文件,例如:

1
d3.csv('path/….csv').then(data=>{//数据读取后的逻辑})

d3.csv是一个JavaScript异步函数,注意,不能直接获取它的返回值!它返回的是一个Promise对象,要通过.then(data=>{…})的方式来获得读取后的数据。data即为读取后的数据。

D3.js数值计算

  • d3.max(array):返回数组中的最大值
  • d3.min(array):返回数组中的最小值
  • d3.extent(array):同时返回数组中的最小值与最大值,以数组[最小值,最大值]的形式

数组中的内容可以是任意对象,而对象包含多个属性,具体取哪个属性的数值进行比较可以通过回调函数来提示d3的API,例如:

1
2
let a = [{name:"Alice",age:21},{name:'Bob',age:25}]
d3.max(a,d=>d.age)

比例尺

比例尺用于把实际数据空间映射到屏幕(画布)空间,即两个空间的转化。

常用于映射数据以及创建坐标轴。

线性比例尺-Linear

可以用下面的接口来定义一个线性比例尺,其返回值是一个函数,然后可以对比例尺设置定义域以及值域。

1
2
3
4
5
6
let scale = d3.scaleLinear();       // 定义线性比例尺
scale.domain([min_d,max_d]).range([min,max]); // 定义比例尺定义域和值域
// 通常结合d3.max等接口连用
const xScale = d3.scaleLinear()
.domain([0,d3.max(data,d=>d.value)])
.range([0,innerWidth]);

条带比例尺-Band

条带比例尺的定义域是离散的,而值域是连续的。可以这样定义一个条带比例尺:

1
2
3
const scale = d3.scaleBand()
.domain(data.map(datum=>datum.name))
.range([0,120]);

坐标轴

一个坐标轴为一个group(<g>),通常需要两个坐标轴。

坐标轴中包含:

  • 一个<path>用于横跨坐标轴的覆盖范围

  • 若干个刻度(.tick){对应于比例尺的定义域},每个刻度也是一个group

  • 每个可读下还包含一个<line>和一个<text>

    <line> : 展示坐标轴的轴线,如左到右或者上到下

    <text> : 展示坐标轴的刻度值

  • (可选)一个标签用以描述坐标轴

坐标轴的定义通常需要结合比例尺,例如:

1
2
3
4
5
6
// 定义坐标轴
const yAxis = d3.axisLeft(yScale);
const xAxis = d3.axisLeft(xScale);
// 绘制坐标轴
const yAxisGroup = g.append('g').call(yAxis);
const xAxisGroup = g.append('g').call(xAxis);

注意:任何坐标轴在初始化之后会默认放置在坐标原点,需要进一步的平移。

可以对坐标轴的风格进行修改,例如:

1
d3.selectAll('.tck text').attr('font-size','2em');

.tick是D3对于坐标轴定义的统一Class。

记住:左纵轴坐标需要.attr(‘transform’,’rotate(-90)’)来旋转

由于坐标轴初始化在父节点的左上角,而SVG范围之外的内容浏览器并不会显示,所以我们往往需要定义Margin,比如:

1
2
3
4
5
6
7
8
// 定义margin
const margin = {top:60,right:30,bottom:60,left:30}
// 计算实际操作的inner长与宽
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom
// 在SVG下额外定义一个组作为新的根节点
const g = svg.append('g').attr('id','maingroup')
.attr('transform','translate(${margin.left},${margin.top})');

得到的maingroup视图如下,然后坐标轴以maingroup作为父节点,就能看见margin的效果了。

![DV7IZTN~7XPL72_VU`021_O.png](https://i.loli.net/2021/10/19/3uUPfz9lHEKsrnb.png)

举一个具体的实例来说明:

Data-Join

这个点特别重要,能够实现数据的动态变化、动态更新。

Data-join实现了以数据为中心(Data-Driven)的可视化操作,将数据与图元进行绑定,根据数据来自动调整图元的属性。这样数据发生变化时,就不再需要手动添加、修改或者删除图元,而是可以自动完成。

图元绑定数据

1
d3.selectAll('.class').data(dataArray);

dataArray需要保证是一个数组,可以是普通数组,对象数组等任何形式。

.data(…)只考虑数据和图元数目相同的情况。

默认的绑定按照双方的索引顺序。

而数据的更新,只需要重新绑定到另一个dataArray就可以了。

调用形式

1
d3.selectAll('.class').data(myData).join('图元').attr(d=>...).attr((d,i)=>...)

.join(…)会根据数据的条目自动补全or删除图元。