D3:Data-Driven Documents
• 通过D3提供的接口来基于数据操控文档的各个图元。
标题对于D3(本讲解)最为重要的标签,主要操作的对象(画布)
HTML - 导入D3.js
D3.js作为JavaScript的外库,必须先将其导入,如:
-
Python的import,C/C++的include、
-
Java的import、node.js的require… … …
-
通过Script标签导入
- 直接通过互联网链接
https://d3js.org/d3.v5.min.js - 通过本地服务器链接(推荐)
./d3.min.js - 通过unpkg链接
https://unpkg.com/browse/d3@5.15.0/dist/d3.js
尽可能使用本地的d3.min.js库。
- 直接通过互联网链接
svg-可缩放矢量模型
- svg d3用来绘制的"画布"
- 可缩放矢量图形(英语:scalable vector graphics,svg)
- svg是d3.js主要操作的对象
- const svg=de3.select(‘svg’)
- d3.js获取svg对象
- svg同时也是一个容器,用于包含画在上面的各个图元
- svg作为矢量图,不会随着图片的缩放而发生失真
引入svg
<svg height="200" width="200" style="display:block;margin:0 auto">
<g transfrom="translate(0,60)">
<rect width=100 height=100 fill="#eee" />
<circle r=15 fill="#72bf67" cx=25 cy=30></circle>
<circle r=15 fill="RGB(100,149,237)" cx=75 cy=30></circle>
<g transform="translate(15,60) rotate(10)">
<path d="M0,0 A40,40 10 0,0 65,0" fill="none" stroke="gray" stroke-width=5></path>
</g>
</g>
</svg>
HTML–文档对象模型
- HTML -> DOM
- DOM -> Document Object Model
- 对于根节点的操作会影响到子节点;
- 最常用的父节点
- Axis可封装成一个group
- Legend(图例)可封装成一个group
https://en.wikipedia.org/wiki/File:DOM-model.svg
JavaScript – D3中的常用接口
- 模板字符串:
- let a = 10;
- let myString =
abc-${a}
; (myString最终为’abc-10’)
- 数组 a = [1, 2, 3]
- 对象 a = {name: ‘Shao-Kui’, age: 24.3, lab: ‘cscg’}
- D3数据可视化中常见对象数组,如:
- a = [{name: ‘Shao-Kui’, age: 25.3, dept: ‘cs’},
- {name: ‘Wen-Yang’, age: 23, dept: ‘cs’},
- {name: ‘Yuan’, age: 29, dept: ‘cs’}]
- D3数据可视化中常见对象数组,如:
- 数组的排序 a.sort()
- 可加入回调函数来替代缺省的排序方案,如对日期排序
- a.sort(function(a,b){ return new Date(b.date) - new Date(a.date); }
- 数组的查询 a.find( d => d.name === ‘Wen-Yang’)
- 把字符串转换成数值:+(‘3.14’)
- D3.js经常读取CSV、JSON等文件,会涉及大量的数组、对象的操作!
D3语法基础概览
- 使用D3获取、修改、增加与删除节点(图元)
- 数据的读取 – CSV
- D3.js的数值计算。
- 比例尺:
- 线性比例尺(Linear Scale)
- “条带”比例尺(Band Scale)
- 坐标轴的绘制:
- Margin。
- Data-Join基础
- 基于D3的基础语法与Data-Join绘制柱状图
元素(标签)的标识
- 当我们在一个同学群体中(比如微信群)对某些同学发出通知时
- 请学号为2020123456的同学在东主楼集合
- 请计算机系研一的同学在东主楼集合
- 在一个群体中,索引个体:
- 通过唯一的标识索引到唯一的个体
- 通过共同点索引到一批个体
元素(标签)的标识
- 操作元素首先需要知道元素的标识
- 即要得到已有或已经创建的元素
- 元素的ID
- 可以唯一找到元素的标识符
- 元素的Class
- 人为赋予的“类别”可以标记元素的集合,其中的元素标签可以不相同!
- 元素的标签
- HTML自带的标签名称,可以找到一批同类别的物体,如所有的“矩形”
- 使用自带的标签往往难以直接索引到目标元素
-
, ,
使用D3查询SVG
- d3.select(…)
- d3.select(‘#rect1’)
- 查询ID为’rect1’的元素
- #表示后面的字符串是一个ID
- 只找一个,若有重名也只返回第一个
- d3.selectAll(…)
- d3.selectAll(‘.class1’)
- 查询所有class是’class1’的元素
- d3.selectAll(‘rect’)
- 查询所有标签是’rect’的元素(rect为SVG中的矩形标签)
- 有多少返回多少
- 可配合Data-Join选取‘不存在’的图元
- ID前加‘#’,Class前加‘.’ ,标签名前不加符号。
- 基于层级的查询:
- d3.select(‘#maingroup rect’)
- d3.selectAll('.tick text’)
- d3.selectAll(‘#secondgroup rect’)
- 如:’#secondgroup rect’
- 首先会找到id为secondgroup的标签
- 进一步找到secondgroup的子标签中是rect的
- 仍然是对rect做查询,只是结果通过父标签做了筛选
- d3.select(…)也可用于查询类别,如
- d3.select(‘.class1’)
- 但只会返回找到的第一个元素
- 因此对于class、标签名称的查询建议使用d3.selectAll
- 对于特定某一个元素的查询建议使用d3.select
使用D3设置SVG中的属性
- 常见的属性
- id, class(特殊的属性,可以使用.attr设置)
- x, y, cx, cy
- fill, stroke
- height, width, r (圆的半径)
- transform -> translate, rotate, scale
- SVG的属性非常多,且属性的取值 **范围&类型 **各不同
- tip1: 尽可能记住一些常见的属性,以提高编程速度
- tip2: 遇到不认识or想要设置某个属性,一定要查阅
https://developer.mozilla.org/zh-CN/docs/Web/SVG/Attribute
- 屏幕空间的坐标系与常见坐标系不同
- 左上方为原点
- Y、X分别垂直向下、水平向右
element.attr(…)
- 设置元素的属性: element.attr(‘attr_name’, ‘attr_value’)
- 两个参数:属性名、设置的值
- rect1.attr(‘y’, ‘100’)
- d3.select(‘#rect1’).attr(‘y’, ‘100’)
- 获取元素的属性: element.attr(‘attr_name’)
- 一个参数:属性名
- 链式调用
- selection.attr(…).attr(…).attr(…)
- .attr(…)返回的是选择的图元本身
修改整组属性
- DOM
- 父节点的属性会影响子节点
- 子节点的属性会相对于父节点
- 下方代码可以直接移动组内所有元素
- d3.select('#maingroup’)
- .attr(‘transform’, ‘translate(200, 100)’)
使用D3 添加&**删除 **SVG元素
- element.append(…)
- const myRect = svg.append(‘rect’);
- const myRect = d3.select(‘#mainsvg’).append(‘rect’)
- const myRect = d3.select(‘#mainsvg’).append(‘rect’).attr(‘x’, ‘100’)
- D3的链式添加(调用)
- const myRect = d3.select(‘#mainsvg’).append(‘g’).attr(‘id’, ‘maingroup’)
- .append(‘rect’).attr(‘fill’, ‘yellow’)
- element.remove()
- 会移除整个标签
- Tip:在debug的过程中可以考虑使用’opacity’属性hack出移除的效果
- element.attr(‘opacity’, ‘0’)
操控SVG
数据的读取 – CSV数据
- 第一行为属性列表,后续每行对应一‘条’数据。
- CSV本质上是纯文本,区别于EXCEL的格式。
-
d3.csv(…):
- 读取目标路径下的某一个CSV文件。
- 例:d3.csv(‘static/data/hello.csv’);
-
d3.csv是一个JavaScript异步函数:
- 不可以直接获得它的返回值,如:
- let myData = d3.csv(‘static/data/hello.csv’); ❌
-
d3.csv(‘path/to/data.csv’).then( data => { // ‘数据读取后的代码逻辑’ } )
- 要通过.then( **data **=> {…} )的方式来获得读取后的数据。
- then(…)中的 ‘data => {…}’ 是一个函数。
- 此函数接受的输入(参数),即data,为读取后的数据。
-
JavaScript异步机制
- d3.csv作为异步函数,即便没有读取好数据,后面的代码也会继续执行
- d3.csv被调用后,其返回值是一个JavaScript的‘Promise’对象(object)
- Promise‘询问’:数据读取好了之后要做什么?‘做什么’即对应.then()中函数的内容。
-
代码调用示例:
- 读取后的数据格式(接口)与原本的CSV结构不同。
D3.js的数值计算
- 数据可视化常涉及对数据的处理与计算:
- 下述三个接口分别用于计算数组的最大值、最小值、[最小值,最大值]。
- d3.max(array)
- 返回数组中的最大值。
- d3.max([5,4,6,1,8,16,9]) // 16
- d3.min(array)
- 返回数组中的最小值。
- d3.min([5,4,6,1,8,16,9]) // 1
- d3.extent(array)
- 同时返回最小值与最大值,以数组的形式,即[最小值,最大值]。
- d3.extent([5,4,6,1,8,16,9]) // [1, 16]
- 数组中的内容可以是任意对象:
- 每个对象可能包含多个属性。
- 具体取哪个属性的最大值通过回调函数来提示d3.max、d3.min与d3.extent。
- 例:
- let a = [ {name: ‘Shao-Kui’, age:25, height: 176}, {name:‘Wen-Yang’, age:24, height: 180}, {name:‘Liang Yuan’, age: 29, height: 172}, {name:‘Wei-Yu’, age:23, height: 173}]
- d3.max(a, d => d.age) // 29
- d3.max(a, d => d.height) // 180
- d3.extent(a, d => d.height) // [172, 180]
- d3.min(a, d => d.age) // 23
比例尺
- 比例尺用于把实际数据空间映射到屏幕(画布)空间,即两个空间的转化。
- 常用于映射数据and创建坐标轴。
- 区别主要在于数据的尺度不同
Scale - Linear
- d3.scaleLinear():
- 定义一个线性比例尺,返回的是一个函数。
- let scale = d3.scaleLinear(); // scale为函数
- scale.domain([min_d, max_d]).range([min, max]):
- 设置比例尺的定义域与值域。
- 线性比例尺的定义域和值域都是连续的(Continuous),需分别给出最大值与最小值。
- const scale = d3.scaleLinear().domain([20, 80]).range([0, 120]);
- 比例尺本质上是一个函数:
- scale(20) // 0
- scale(50) // 60
- 常结合读取的数据与d3.max等接口连用:
- const xScale = d3.scaleLinear() .domain([0, d3.max(data, d => d.value)]).range([0, innerWidth]);
Scale - Band
- d3.scaleBand():
- 定义一个‘条带’比例尺,返回的是一个函数。
- let scale = d3.scaleBand();
- scale.domain(array).range([min, max]):
- 设置比例尺的定义域与值域
- Band比例尺的定义域是离散的(Discrete),值域是连续的。
- const scale = d3.scaleBand().domain([‘a’, ‘b’, ‘c’]).range([0, 120]);
- 比例尺本质上是一个函数:
- scale(‘b’) // 40
- scale(‘c’) // 80
- 常结合JavaScript的array.map接口一起使用:
- let a = [{name: ‘Shao-Kui’, value:6}, {name:‘Wen-Yang’, value:6}, {name:‘Yuan Liang’, value:16}]
- a.map(d => d.name) // [‘Shao-Kui’, ‘Wen-Yang’, ‘Yuan Liang’]
- const yScale = d3.scaleBand().domain(data.map(d => d.name)).range([0, innerHeight])
- scale.padding(0.1):
- 设置条带的间距占各自区域的比重。
- scale.bandwidth():
- 返回条带的长度。
引入坐标轴
- 一个坐标轴为一个group(
),通常需要两个坐标轴。 - 坐标轴中包含:
- 一个
用于横跨坐标轴的覆盖范围 - 若干个刻度(.tick)
- 每个刻度也是一个group
- 每个刻度下属还会包含一个
和一个 -
用于展示坐标轴的轴线,如左到右或上到下 -
用于展示坐标轴的刻度值,如实数、姓名、日期
-
- (可选)一个标签用以描述坐标轴
- 一个
- 坐标轴的定义通常需要比例尺。
-
定义坐标轴(获得结果仍是函数):
- const yAxis = d3.axisLeft(yScale);
- const xAxis = d3.axisBottom(xScale);
- axisLeft:左侧坐标轴。
- axisBottom:底侧坐标轴。
- 坐标轴的刻度对应比例尺的定义域。
- 坐标轴在画布的绘制对应比例尺的值域。
- 仅是对坐标轴的定义,还未绘制。
-
绘制坐标轴:
- const yAxisGroup = g.append(‘g’).call(yAxis);
- const xAxisGroup = g.append(‘g’).call(xAxis);
- 实际配置后会发现
中增添了与坐标轴相关的元素
-
任何坐标轴在初始化之后会默认放置在坐标原点,需要进一步的平移。
-
关于 selection.call(…)
-
函数的输入为另一个函数。
-
另一个函数以selection本身(即图元)作为输入
-
另一个函数中会根据函数体的内容修改selection对应的图元。
-
定义一个空白的
,D3会帮助我们定义好 另一个函数,我们通过.call(…)让 得以在 另一个函数中修改。 - const yAxis = d3.axisLeft(yScale);
- const yAxisGroup = g.append(‘g’).call(yAxis);
配置坐标轴
- 可以对坐标轴的风格进行修改:
- 坐标轴本质上是图元的集合。
- d3.selectAll(‘.tick text’).attr(‘font-size’, ‘2em’);
- .tick是D3对于坐标轴定义的统一class
- 坐标轴的标签加入不在D3-Axis接口的负责范围内:
- 通过对坐标轴的
标签 .append(‘text’)来实现 - (左)纵轴坐标需要 .attr(‘transform’, ‘rotate(-90)’) 来旋转
- 纵轴坐标旋转后,x / y 会颠倒甚至取值范围相反
- 回忆DOM:父节点的属性会影响子节点,而坐标轴默认的’fill’属性是 ‘none’,因此请一定手动设置文字颜色 .attr(‘fill’, ‘black’)
- 通过对坐标轴的
引入坐标轴 - Margin
- SVG对于D3.js是一个“画布”。
- SVG范围外的任何内容属于画布之外,浏览器将不予显示。
- 然而坐标轴通常初始化在所在父节点的左上角。
- 定义Margin:
- const margin = {top: 60, right: 30, bottom: 60, left: 200}
- 计算实际操作的 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})
);
- const g = svg.append(‘g’).attr(‘id’, ‘maingroup’).attr(‘transform’,
- Tip: HTML确实在样式表中提供margin属性,然而设置其他图元的位置,仍需要计算innerWidth(Height)。
引入坐标轴
- 调用示例:
- 比例尺可通过坐标轴可视化。
Data-Join
-
本质上是将数据与图元进行绑定:
- 每个国家的人数绑定到矩形的长度;
- 疫情感染的人数比例绑定到圆的半径;
- 产品的销量绑定到矩形的长度;
- 各类别商品的销售占比绑定到扇形的弧度。
-
Why?
- 以数据为中心(Data-Driven)的可视化操作:
- 根据数据自动调整图元的属性。
- .attr(…)接口可基于图元自己绑定的数据自动调整属性值。
- 数据发生变化时可以自动对图元增删改查:
- 不再需要手动添加、‘修改’、删除图元。
- 根据数据的增加or删除or更新,自动补充or移除or更新图元。
- 以数据为中心(Data-Driven)的可视化操作:
-
Data-Join并不是必要的操作,不使用Data-Join同样可以画出所有可视化作品。
-
Data-Join只是让D3.js编程变得更高效且语法更简洁。
-
d3.selectAll(‘.class’)**.data( dataArray ) **
-
dataArray在保证是一个数组的前提下可以是任何形式:
- 例: [0, 2, 32, 18];
- 例:[{name: ‘Sebastian’, value:384}, {name:’ Ciel’, value:32}, {name:‘Wen-Yang’, value:16}, {name:‘Shao-Kui’, value:19}];
-
.data(…)只考虑数据和图元数目相同的情况:
- dataArray是一个数组,其中的每‘条’数据会与一个图元绑定。
-
默认的绑定按照双方的索引顺序:
- (Data的Key:后续D3中会讨论。)
-
不调用.data(…),则图元不会与任何数据绑定!
-
数据的更新只需要重新绑定另一个 dataArray 即可。
- 调用形式:
- **d3.selectAll(‘.class’).data(myData).join(‘图元’).attr(d => …).attr((d, i) => …) **
- .join(…)会根据数据的条目补全or删除图元。
- 若有新增的数据,则会自动增加对应图元。
- 若有修改的数据,则会自动更新对应图元。
- 若有删除的数据,则会自动移除对应图元。
Data-Join – 用函数设置图元属性
- selection.attr(‘attrbuteName’, ‘value’)
- 通过值设置属性
- selection.attr(‘attrbuteName’, (d, i) => {…})
- 通过函数设置属性,函数的输入为绑定的数据,返回值为图元得到的属性值
- d为Data-Join中,‘.data(array)’绑定给每个图元的数据。
- i为Data-Join中,‘.data(array)’绑定图元的顺序,即图元对应原本数组的第几个
- 例:d3.selectAll(‘rect’).attr(‘width’, (d, i) => 1000 * d.age )
- 例:d3.selectAll(‘circle’).attr(‘cy’, (d, i) => 200 * i + 30);
- 由于绑定数据的不同,故得到的结果也不同。
- 设置图元属性的函数遵循如下规则(顺序性):
- 函数可仅使用 d => {…},即只有一个参数,但此时函数体无法使用索引。
- 即使未使用到绑定的数据,如需使用索引,仍需要完整的写出 (d, i) => {…}。
基于D3的基础语法与Data-Join绘制柱状图
- 数据来源:
- https://www.kaggle.com/gregorut/videogamesales
Tip:颜色 – ‘fill’属性
- PlanA:人为定义一系列颜色组合
- PlanB:使用D3提供的颜色组合(见下页)
- PlanC:采样
Tip:D3提供的各种色盘
- 定义一个离散数据到离散数据的映射
- 如:每个水果对应到某个颜色
- D3.js的内嵌(自带)配色方案?
- https://github.com/d3/d3-scale-chromatic
网络数据的数据结构?
- 网络数据包括节点的集合与边的集合:
- 节点与边通常分布在不同的文件中,通过节点的ID索引
- D3.js也没有统一的网络数据结构规范:
- 只要能整理成D3.js对应接口接受的格式即可
- 常见的数据形式:
- 【节点列表】+【连接矩阵】
- 【节点数、边数与基于ID的连接】
- 【节点列表】+【边列表】
【节点数、边数与基于ID的连接】
【节点列表】+【边列表】
【连接矩阵】
d3力模拟基础
- d3的力模拟与“transition”是完全不同的两个体系
- let nodes = [{}, {}, {}, {}, {}, {}];
- let simulation = d3.forceSimulation(nodes) 定义后会发生…
- 补全nodes中每个节点的数据结构:
- 包括index, x, y, vx, vy,后两者为速度。
- 开始模拟粒子运动:
- 粒子质量为1。
- 不断地通过内部timer触发’tick’事件。
- 根据一系列的‘力’来计算每个例子的加速度、速度、位置…
- ‘力’都是哪来的呢?
- 补全nodes中每个节点的数据结构:
不同力的作用
- d3.forceManyBody().strength( value ):
- 粒子之间两两的作用力,类似于‘万有引力’。
- .strength(value)’用来设置力的大小,value为正互相吸引,为负则互相排斥。
- d3.forceCenter(w, h).strength( value ):
- 指向某一个中心的力,会尽可能让粒子向中心靠近。
- .strength(…)的用法同上。
- ‘d3.forceCenter(w, h)’中的‘w’与‘h’为中心的位置,通常为画布的中心。
- d3.forceLink(links).strength(strength).distance(distance):
- 部分粒子之间的两两作用力,不同于‘d3.forceManyBody’。
- 'd3.forceLink’中,每个节点仅仅会与一部分节点有力的作用。
- 有链接的节点间,受力的作用,保持在特定的距离,即靠近互斥、远离吸引。
- 是否有链接需要通过图的边集合给出。
- ‘.strength( vs )’ 与 ‘.distance( vd )’分别设置力的大小与预期的距离。
- Link要通过一个数据格式给出,即link的source与target。
- 格式非常类似于‘d3.hierarchy’给出的root.links()
编程实例:
‘Tic-Toc’
- forceSimulation会通过每次‘tick’来更新当前节点的状态:
- 状态包括位置、速度、加速度等。
- 更新后的状态仅仅为‘状态’:
- 不会反映到任何图元,仅修改数据。
- 需要添加修改图元属性的回调函数。
- 人为设置每次tick要如何更新图元
- simulation.on(‘tick’, ticked);
- 在初始化每个图元后,只要为simulation配置了’tick’的回调,simulation会自动开始模拟。
基于‘d3-force’实现力导图
数据来源:http://networkrepository.com/socfb-Caltech36.php
编程实例:
Tip:带权重的图?
- d3.forceLink(links).strength(…).distance(…):
- 本质上根据link的权重设置forceLink的strength与distance。
- 分别输入回调函数,基于每一个‘link’元素来设置各自的力与距离。
- 编程实例: