# d3
**Repository Path**: dev-edu/d3
## Basic Information
- **Project Name**: d3
- **Description**: d3.js完整课程资料
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 3
- **Forks**: 25
- **Created**: 2023-09-06
- **Last Updated**: 2025-07-14
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# D3js
# 一 概述
d3 是 **D**ata-**D**riven-**D**ocuments的缩写,是一个免费开源javascript库,用于创建动态的,可交互的数据可视化效果。
d3不是一个图表库,不能像echarts一样通过简单的配置或调用api来直接生成最终的图标。
d3相当于低级工具库,与svg配合优良,通过数据驱动轻松生成svg图形路径,比直接使用svg绘图要方便很多,而最终的图表效果也是通过生成的svg来绘制形成的。
d3也支持与canvas配合使用。
d3本身没有创建任何新的标签或语法来绘制图表,最终都是使用canvas和svg技术绘制图形。
d3遵循web标准,所以不需要其他框架的辅助,可以独立运行在浏览器中。
d3提供了丰富的API , 可以轻松操作dom ,实现dom创建,删除,更新,查找等,与jquery比较相似。
d3提供了丰富的功能,可以使我们更轻松的实现一些复杂的图形效果, 如地图,树图,缩放,刷选等。
d3核心理念是数据驱动文档,将数据与dom标签对象绑定,对数据操作即可对dom对象操作。 与vue相似。
d3虽然不如echarts或一些其他的可视化图表库使用方便,但可以高度定制,按需设计。
# 二 安装使用D3
**1 本地引入**
git下载:https://github.com/d3/d3/releases
```html
```
**2 cdn引入**
```html
```
* 官网提供了多种cdn引入方式
* 需要以模块化的方式使用
> 不需要模块化方式的cdn引入地址:https://d3js.org/d3.v7.min.js
**3 node安装**
```text
npm i d3
```
**4 observablehq平台在线使用**
可视化在线学习平台
类似于在线笔记本,可以云存储可视化作品,可以共享作品。
编写js片段,并可以即时运行这些代码并查看结果
集成了d3可视化库,可以直接使用d3
> 该平台有自己的一些语法规范, 所以对于初学者,还需要先了解这些新语法。
# 三 元素操作
## 1 selection对象
**d3操作元素时会产生一个selection对象,包含了我们操作的标签元素(多个)**
* 可能是已有的标签元素 (查找)
* 可能是新创建的标签元素(新建)
**查找已有的标签元素**
```javascript
const selection = d3.select(..)
const selection = d3.selectAll(..)
```
* 方法可以传递选择器,根据选择器找到匹配的元素,并包装成selection
* 方法可以传递dom对象,包装成selection
* selection可以使用select 和 selectAll方法继续查找子级标签
**新建标签元素**
```javascript
const selection = d3.create('div');
```
* 需要配合insert , append才能在窗口中展示新标签效果。
**获得selection中包含标签元素的个数**
```javascript
selection.size(); // 3
```
**遍历每一个标签元素**
```javascript
selection.each(function(d,i){
//d 是后面要讲到的给元素绑定的数据,目前是undefined (暂时忽略)
//i 元素下标
//this 就是当前元素
console.log(d , i , this);
})
```
* 这里不要使用箭头函数,否则this无法表示标签元素。
**获得包含的所有标签元素**
```javascript
const node = selection.node(); // 获得第一个p
const nodeList = selection.nodes() //[p,p,p]
```
* 获得selection中包含的元素对象。
## 2 添加(放置)元素
**放置元素在末尾**
```javascript
const selection = d3.select('body') ;
const selection1 = selection.append('input') ;//在selection末尾增加一个新标签
const selection1 = selection.append(function(){
return element ; //标签元素 (新,旧)
})
```
**插入元素在指定的位置**
```javascript
const selection1 = selection.insert('input') ; // 等价于append 插入在末尾
const selection1 = selection.insert('input','selector') ;//在selection中指定的selector前面插入新元素
const selection1 = selection.insert(()=>{return element},'selector')
```
> append 和 insert 在新建元素,放置元素的同时,也产生了一个selection对象
## 3 删除元素
```javascript
selection.remove(); //删除找到的标签 d3.select('#i1').remove()
```
## 4 修改元素
修改元素的属性,样式,内容,事件
**操作属性**
```javascript
selection.attr('name') ;//获得第一个元素的name属性值
selection.attr('name','dmc') ;//为selection中所有元素的name属性赋值,同时返回selection对象
selection.attr('name',function(){ return value ;})
```
* 当value是一个函数的时候,selection中每一个元素属性赋值时都会调用这个函数,将函数的返回值作为属性值。
* 接下来元素绑定数据时,比较常用这种方式为属性赋值
* 每一个元素赋值调用函数的时候都会传递3个参数
* d 当前元素绑定的数据 (暂时没有)
* i 元素下标
* nodeList 装载selection中这一组的元素, nodeList[i] 就是当前元素
* 如果不是箭头函数, this也表示当前这个元素。
**操作样式**
```javascript
selection.style('border') ;//获得第一个元素的指定样式
selection.style('border','2px solid #ccc') ;//设置样式
selection.style('border',function(){ return str ;})
```
```javascript
d3.selectAll('div')
.data(['red','green','blue'])
.style('margin','10px')
.style('padding','10px')
.style('width','200px')
.style('border','1px solid #ccc')
.style('background',function(d){
return d;
})
```
**操作内容**
```javascript
selection.html() //获得第一个元素的html内容
selection.text() //获得第一个元素的text文本内容
selection.html('');
selection.html(function(){return ''})
```
**操作事件**
```javascript
selection.on('click',function(e,d){})
```
* e 事件对象。 e.target 指向触发当前事件的元素。 如果是function函数,等价于this。
* d 当前元素绑定的数据 (暂时忽略)
# 四 数据绑定
## 1 数据应用
数据与标签逻辑上的关联
```javascript
const array = ['zs','li','dmc'] ;
const selection = d3.selectAll('div')
array.forEach((e,i)=>{
selection.filter(`:nth-child(${i+1})`).text(e)
})
```
## 2 datum数据绑定
为selection包含的所有元素绑定相同的数据。
```javascript
const selection = d3.selectAll('div');
selection.datum('dmc')
selection.text(function(d,i){
console.log(d,i);
return d + (i+1);
});
selection.attr("id",(d,i)=>d+(i+1))
//--------------------------------------------------
const selection = d3.selectAll('div')
.datum('dmc')
.text(function(d,i){
console.log(d,i);
return d + (i+1);
});
.attr("id",(d,i)=>d+(i+1))
```
> 在修改标签时,既可以传递具体的值,也可以传递函数,函数的返回值即为修改后的值
> 如果传递的是一个函数,这个函数被调用时,会传递标签绑定的数据,可以基于绑定的数据返回结果。
## 3 data数据绑定
为selection包含的元素绑定各自对应的数据
```javascript
const array = ['zs','ls','dmc'];
const selection = d3.selectAll('div')
selection.data(array);
selection.text((d,i)=>{
console.log(d,i);
return d ;
});
selection.attr('id',d=>d);
d3.selectAll('div').attr('class',d=>d);
```
* 默认按照下标匹配来绑定数据 datas[0] --- 绑定 --- elements[0]
* 数据绑定在标签对象中(`__data__属性`), 重复获得标签对象, 绑定的数据依然存在
## 4 数据key映射
将数据与标签匹配。
内部的匹配机制:
* 绑定数据时,会依次遍历selection中的所有标签元素 和 数据源
* 遍历过程中,会调用一个key函数,函数的返回值即为当前数据的key,默认返回的是下标
* 根据key,将数据绑定在对应的镖旗
可以提供key函数,自定义映射机制。
```javascript
//d 就是当前的数据 (遍历元素时,d=undefined)
//i 是下标 , 元素和数据有各自的下标
//list 当前内容所属的集合 (数据 list是数组, 元素 list就是NodeList集合
key = function(d , i , list){
}
d3.selectAll('div').data(array,key);
```
```html
```
## 5 enter() exit()
当数据与标签 数量不配时,通过这些函数对多余的标签 和 数据做处理
* 数据多了,需要增加标签(apped)
* 标签多了,需要删除多余标签(remove)
**enter()**
获得多余的数据,配合append方法,将多余的数据绑定在新添加的标签中。
```html
1
2
3
```
**exit()**
获得多余的标签,配合remove方法,删除这些多余的标签
```html
1
2
3
4
5
6
```
> 如果设置了自定义key函数,exit方法最终返回的是没有匹配的那部分多余的标签元素。
## 6 data-join
**三种数据绑定状态**
* update 数据数量 = 标签数量 , 数据重新绑定
* enter 数据数量 > 标签数量,获得多余数据(新增标签并绑定)
* exit 数据数量 < 标签数量 ,获得多余标签(删除标签)
**join() 会自动检测数据状态,并针对于不同的状态自动完成对应的操作**
* 需要传递一个参数,指定enter状态时,新增的标签名称 `join('div')`
```javascript
d3.select('body')
.selectAll('div')
.data(array)
.join('div')
.text(d=>d)
```
**join()还可以传递三个函数参数,分别表示enter, update, exit状态的操作函数**
* 三个函数表示自定义的状态处理
* d3在数据绑定时,会根据不同的状态,调用对应的函数
* 调用函数时,会传递selection对象,包含对应状态的内容。
* 通过selection自定义三种状态的处理
* 返回处理结果(selection)
* 需求:正常绑定的数据,div背景设置为red 。 多余数据绑定新增的div,设置背景green。多余的标签设置背景蓝色
```javascript
d3.select('body')
.selectAll('div')
.data(array,(d,i)=>d?i+1:i)
.join(
(selection)=>{
console.log('enter',selection);
return selection.append('div').style('background','green');
},
(selection)=>{
console.log('update',selection);
return selection.style('background','red')
},
(selection)=>{
console.log('exit',selection);
return selection.style('background','blue').remove()
}
)
.text(d=>d)
```

## 7 多重数据绑定
**数据绑定及绑定数据的应用**
* 数据绑定,会将数组中的每一个数据与标签绑定, 存储在标签对象的`__data__`属性中
* 修改标签时(attr , style , text 等), 如果提供的是一个函数
* 底层遍历处理selection包含的标签元素时,每一次都会调用这个函数,并将当前标签绑定的数据作为参数传递给这个函数
* 我们可以利用这个参数提供最终的修改结果
**多重数据绑定**
* 数据结构比较复杂,一般都是数组中还包含了子数组
* 在第一层数据绑定后,修改当前标签或子标签时,都会传递绑定的数据,可以根据需要使用这个数据
* 也可以为子标签再次绑定数据,绑定的可以是新的数组,也可以是当前绑定数据中的子数组,这样子标签绑定的就是新的数据了。
```text
divs--绑定--[ [1,2] , [11,22] , [111,222] ]
div -- 绑定-- [1,2]
div -- 绑定-- [11,22]
div -- 绑定-- [111,222]
div.attr('name' , function(d){}) ;
d == [111,222]
div.append('span').attr('name',function(d){})
div的子标签span也可以使用[111,222]
div.selectAll('span').data(d=>d).join('span')
div所有的子标签绑定d这个数组的每一个内容
span使用的就不再是[111,222]这个数组了,而是其中的111或222具体的数值
```
```javascript
const data = [
{province:'黑龙江',cities:['哈尔滨','齐齐哈尔','牡丹江','佳木斯']},
{province:'吉林',cities:['长春','吉林','松原','延边']},
{province:'辽宁',cities:['沈阳','大连','鞍山','铁岭']},
]
// const selection = d3.select('body')
// .selectAll('dl')
// .data(data)
// .join('dl')
// selection.append('dt')
// .text(d=>d.province)
// selection.selectAll('dd')
// .data(d=>d.cities)
// .join('dd')
// .text(d=>d)
const selection = d3.select('body')
.selectAll('dl')
.data(data)
.join('dl')
.call(dl=>{
dl.append('dt')
.text(d=>d.province)
})
.call(dl=>{
dl.selectAll('dd')
.data(d=>d.cities)
.join('dd')
.text(d=>d)
})
```
# 五 比例尺
可视化图形设计的辅助工具
作用是将真实数据映射成可视化数据
例如:将[0,300] 映射到 [0,600]范围
150 对应的可视化数值就是 300
比例尺映射条件包括2部分:
* 值域:真实数据范围
* 范围:可视化数据范围
值域 和 范围 有可能是连续的,也可能是非连续的。
d3提供多种api,用来创建不同类型的比例尺
比例尺本身是一个函数,通过函数实现映射
## 1 linear scale 线性比例尺
值域 和 范围都是连续的
**创建比例尺**
* 需要传递两个数组参数
* 第一个数组是值域的最小值和最大值
* 第二个数组是可视化范围的最小值和最大值
```javascript
const x = d3.scaleLinear([0,300] , [0,600]);
```
**使用比例尺映射**
* 将真实数据映射成可视化数据
```javascript
x(150) ;//300
x(300) ;//600
```
**反向映射**
* 将可视化数据映射成真实数据
```javascript
x.invert(600) ;//300
x.invert(300) ;//150
```
**边界控制**
* 默认即使超过边界,也可以按照比例尺进行映射。
* 可以使用clamp函数控制边界
```javascript
//未开启边界控制时,超出边界的映射
x(400);//800
x.clamp(true);
//开启边界控制时,超出边界的映射
x(400);//600
```
**使用domain( [min,max] ) 和 range( [min,max] ) 分别设置值域 和 范围**
* 传参是设置值域和范围
* 没有传参可以获得值域和范围
```javascript
x.domain([0,300]);
x.range([0,600]);
x.domain();//[0,300]
x.range();//[0,600]
```
**快速计算一组数据中的最大值和最小值**
```javascript
const array = d3.extent([100,60,90,130,80,150,90]) ;//[60,150]
const data = [
{name:'zs1',score:100},
{name:'zs2',score:60},
{name:'zs3',score:80},
{name:'zs4',score:120},
{name:'zs5',score:90},
{name:'zs6',score:150},
{name:'zs7',score:70}
]
d3.extent(data,d=>d.score)
d3.max([100,60,90,130,80,150,90]) ;//150
d3.max(data,d=>d.score);
d3.min(...);
```
**分段比例映射**
* 给值域和范围提供多个数值
* 分段映射
```javascript
const x = d3.scaleLinear()
.domain([0,200,300])
.range([0,300,600])
x(100);// 150
x(250);// 450
```
**范围优化**
```javascript
[1.1 , 19.6] --> [0,300]
const x = d3.scaleLinear([],[]).nice();
[0 , 20] --> [0 , 300]
```
**获得部分值域刻度**
```javascript
x.ticks(count) ; //获得指定数量的刻度值(数组)
```
* 实际获得的刻度数量可能和指定的数量不一致
* 默认count = 10 ;
## 2 time scale 时间比例尺
值域是时间范围, 最终也是映射到可视化的数据范围中
这个时间范围也是连续的。
**创建并使用比例尺**
```javascript
const x = d3.scaleTime( [] , [] )
.domain([new Date('2020-01-01 00:00:00') , new Date('2020-01-01 23:59:59')])
.range([0,600])
x(new Date('2020-01-01 12:00:00'))
```
**时间刻度的格式化**
```javascript
x.ticks().map(d3.timeFormat('%d %H'))
```
```text
%a - 周的缩写 Wed
%A - 周的全写 Wednesday
%b - 月份缩写 Jan
%B - 月份全写 January
%d - 月中的第几天.
%H - 24小时制 [00,23].
%I - 12小时制 [01,12].
%m - 月份数字 [01,12].
%M - 分钟 [00,59].
%s - 秒.
%y - 年份 [00,99].
%Y - 完整年份 1999.
```
更多日期格式化见官方文档:https://d3js.org/d3-time-format#locale_format
**自定义时间间隔**
```javascript
x.ticks(d3.timeHour.every(1))
```
* 默认提供数字, 获得的是指定数量的刻度值
* 也可以指定时间间隔, 获得每个间隔的刻度值
* 可以指定的时间种类有多种:

## 3 ordinal scale 序数比例尺
值域和范围都是非连续的(离散点)
比较常用的就是颜色的映射
**创建和使用比例尺**
```javascript
const x = d3.scaleOrdinal( [] , [])
.domain(['香蕉','橘子','苹果'])
.range(['yellow','orange','red'])
x('香蕉') ;//yellow
```
**设置未知映射**
```javascript
x.unknow('#ccc');
x('葡萄') ;//#ccc
```
**使用d3提供的配色方案**
```javascript
const x = d3.scaleOrdinal( [] , [])
.domain(['香蕉','橘子','苹果'])
.range(d3.schemeCategory10)
```

> 两个颜色之间也可以是连续的,可以配合具有连续特点的比例尺来使用。
## 4 quantize scale 量化比例尺
值域是连续的,范围是非连续的
会对连续的值域空间分段,每一段对应范围的一个离散值
如: 0-60 及格 , 60-80 良好 , 80-100 优秀
**创建和使用比例尺**
```javascript
const x = d3.scaleQuantize()
.domain([0,30])
.range(['red','green','blue'])
x(5) ;//red
x(8) ;//red
x(15);//green
x(20);//blue
```
* 内部分分段是左闭右开。
**反向映射获得的是一个数组,表示分段空间**
```javascript
x.invertExtent('red') ;// [0,10] 10是取不到的。
```
**获得分段的阈值**
```javascript
x.thresholds();//[10,20]
```
## 5 band scale分段比例尺
条带比例尺
值域是非连续的, 范围是连续的
由于可视化数据会影响图形绘制, 所以对每一段可视化数据需要了解更多的信息。如:其实数据,宽度等
多用来绘制柱状图
**创建和使用比例尺**
```javascript
const data = [{type:'金牌',count:50},....]
const x = d3.scaleBand()
.domain(['金牌','银牌','铜牌'])
.range([0,600])
x('金牌');//0
x('银牌');//200
x('铜牌');//400
```
* 使用比例尺获得是对应条带(分段)起始数值
**获得分段步长(宽度)**
* 获得从一段的起点到下一段起点之间的距离
```javascript
x.step(); // 200
```
**获得条带宽度**
* 去除留白后的,可以绘制图形的区域宽度。默认与step相同
```javascript
x.bandwidth(); // 200 0-200
```
**设置每段之间的留白**
* 设置每个条带两端的内边距
* paddingOuter 两端的留白,会影响step的宽度
* paddingInner 条带之间的留白,会影响bandwidth的宽度
* 留白的控制使用的是比例
```javascript
x.padding(0.1) ;
```
**数值取整**
* setp, width有可能出现小数
* 利用round方法可以做取整的处理
```javascript
x.round(true);
```
# 六 坐标轴
**创建坐标轴对象(函数)**
* 此时还没有真正的绘制坐标轴
* 只负责设置坐标轴的一些信息(长度,方向,刻度等)
* 需要传递一个比例尺。 这个比例尺必须有连续的范围
* 比例尺的范围即为坐标轴长度
* 比例尺的值域即为坐标轴的刻度范围
* 比例尺的刻度即为坐标轴的刻度
```javascript
const scale = d3.scaleLinear([2,33],[0,500]).nice()
const axis = d3.axisLeft(scale) ; //axisBottom() , axisRight() , axisTop()
```
**绘制坐标轴**
* 只需要提供绘制的位置即可(svg , g)
* 默认从0,0原点绘制。 可以通过设置父级标签(g)的transform,将坐标轴移动到画布的指定位置
* 更改比例尺,坐标轴不会自动重绘,需要手动重绘。
```javascript
axis(svg)
svg.call(axis);
```
**刻度设置**
```javascript
axis.ticks(5) ;//设置刻度的数量
axis.tickValues([0,5,10,15,20,25,30]);//自定义刻度
axis.tickFormat(d3.timeFormat('%H'))
```
**刻度线设置**
```javascript
axis.tickSize(10); //设置刻度线的长度 包含 tickSizeInner , tickSizeOuter , 负值反向
axis.tickPadding(10); // 设置数字与刻度线之间的留白
axis.offset(10) ;//刻度线与中线之间的距离
```
> 可以css控制坐标轴的一些样式细节。
>
> 如:设置刻度字体大小,刻度线粗细,颜色等。
```css
.tick text{
font-size:12px;
}
```
# 七 d3-tip工具
基于d3的可视化提示工具
不是d3自带的,使用时需要引入
* github下载:https://github.com/bumbeishvili/d3-v6-tip
* node安装: npm i d3-v6-tip
**创建tip工具**
```javascript
const tip = d3.tip()
```
**启用tip工具**
* 将tip工具作用在svg标签上。 这样svg中的所有子标签都可以使用tip工具
```javascript
svg.call(tip);
tip(svg);
```
**使用tip工具**
* 开启tip工具的所有子标签都可以使用tip工具
```javascript
svg.selectAll('circle')
.on('mouseover',tip.show)
.on('mouseout',tip.hide)
```
**设置tip**
```javascript
tip.html((e,d)=>{return `奖牌数:${d}
`}) ;//设置展示的内容
```
* e 触发的事件对象,包含对象源
* d触发事件标签绑定的数据
```javascript
tip.attr('class','d3-tip') ;//设置内置的类选择器,使得样式生效。
```
* 需要引入d3-tip.css
```javascript
tip.direction('n');//设置提示展示的方向 四面八方 n, s, e, w, nw, ne, sw or se
```
```javascript
tip.offset([top,left]) ; //设置提示与原图形位置偏移。默认在(方向)中间位置
```
# 八 图形生成器
根据可视化数据,快速生成较为复杂的path路径。如:弧, 线段,曲线,饼图,环等
## 1 arc 弧生成器
**创建生成器**
```javascript
const arc = d3.arc();
```
* 生成器本身是一个函数
**使用生成器**
* 使用生成器,提供所需的数据,生成弧图形的路径
* 所需数据包含:起始弧度,结束弧度,外半径,内半径
```javascript
const d = arc({
startAngle:0,
endAngle:Math.PI/2,
outerRadius:200,
innerRadius:0
})
//d : 'M0,-200A200,200,0,0,1,200,0L0,0Z'
path.attr('d',d); //绘制图形
```
* 从 0,0原点开始生成路径
* 角度是与y轴负方向的夹角
* 逻辑上提供的是角度, 但实际计算时使用的是弧度。
* 上述代码中 startAngle 是0弧度 , endAngle是Math.PI/2弧度(90°)
**统一设置弧信息**
* 在绘制多个弧图形时,有些信息是不变的。
```javascript
arc.innerRadius()
.outerRadius()
.startAngle()
.endAngle()
```
**设置弧两端的留白**
* 角度和半径可以控制留白距离
* 角度和半径越大, 留白越大。 反之越小
* 角度默认为0,所以角度一定要设置。 半径有默认值可用。
* 产生留白后的弧形 与 之前的弧形两端平行,直到内弧消失,弧形状才会变化。
```javascript
arc.padAngle(Math.PI/8);
arc.padRadius(200) ;
```
**设置弧的圆角**
* 弧的4个断点也设置成弧线。
```javascript
arc.cornerRadius(10)
```
**获得弧的中心位置**
* 获得的是一个坐标,由x和y组成
* 需要传递弧的信息,根据信息计算中心点的位置
```java
const [x,y] = arc.centroid({....})
```
## 2 pie 饼生成器
不是用来生成路径的。
根据可视化数据,生成弧形数据(startAngle , endAngle)
配合弧生成器, 快速绘制饼图或环形图。
**创建饼生成器**
```javascript
const pie = d3.pie();
```
**生成弧数据**
```javascript
const array = pie(data)
```
* 生成的数据数量与原始数据的数量相同
* 新数组包括
* data 当前位置的数据
* startAngle和endAngle
* index顺序,影响角度顺序,最终会影响环形图顺序
**指定用于角度计算的属性值**
* 当数据是对象形式存在时,指定用来计算角度的属性值。
```javascript
const data = [
{name:'zs',score:100},
{name:'ls',score:90},
{name:'ww',score:80},
{name:'zl',score:85},
{name:'dmc',score:95},
{name:'zzt',score:60},
]
pie.value(d=>d.score)
```
**排序**
```javascript
pie.sort( function(a,b){ return a.score - b.score} )
```
* 比较器函数每次会按顺序传递数组中的两个数据
* 根据返回值比较大小
* `<0 a < b`
* `=0 a = b`
* `>0 a>b`
* d3中提供了比较器函数
`d3.ascding(a.score,b.score)` 升序
`d3.descending(a.score,b.score)` 降序
**设置留白角度**
```javascript
pie.padAngle(0.1);
```
**设置开始角度和结束角度**
* 默认第一个图形的角度从0开始,是y轴负方向
* 可以通过设置开始角度,控制第一个图形的起始位置
```javascript
pie.startAngle(Math.PI/2);
pie.endAngle(Math.PI*2 + Math.PI/2)
```
## 3 line 线条生成器
根据提供的数据,确定连线中的每一个点,自动生成path路径。
**创建生成器**
```javascript
const line = d3.line(x , y)
const line = d3.line()
.x(x)
.y(y)
const x = function(d){
return xScale(d.date) ;
}
```
* x 和 y函数(处理器) 用来计算data中每一个数据对应点的x 和 y坐标
* line生成器执行时,会遍历data中的每一个数据
* 每一个数据都会调用指定的x函数,获得x坐标, 调用指定的y函数,获得y坐标
**使用生成器**
```javascript
line(data) ;
```
**设置曲线连接**
```javascript
line.curve(d3.curveMonotoneX)
```
* d3.curveLinear 直线(默认)
* d3.curveStep 阶梯线
* curveStepAfter
* curveStepBefore
* d3.curveBumpX 曲线经过每一个点,形成x方向的切线
* d3.curveBumpY
* d3.curveNatural 自然曲线,经过所有的点
* d3.curveBasis 基本样条曲线 曲线可能不经过点。
* 更多曲线见官方文档:https://d3js.org/d3-shape/curve#curveCatmullRomOpen
## 4 area 区域生成器
* 简单而言,线条生成器,是每一个数据都会产生一个点,将多个点连成线。区域生成器,每一个数据可以产生2个点。每两个数据就会形成一个合围的区域(4个点)
**创建并生成器**
```javascript
const area = d3.area()
area(data);
```
**数据处理器**
* 线条生成器需要提供2个处理器,分别是通过数据处理获得点的x和y坐标
* 区域生成器就需要4个处理器,分别通过数据处理获得两个点的x0和y0坐标, x1和y1坐标
```javascript
area.x0( x0 );
area.y0( y0 );
area.x1( x1 );
area.y1( y1 );
area.x( x ) ;//x0 和 x1 使用相同的处理函数 结果x0=x1
area.y( y ) ;//结果 y0 = y1
//x0函数
//d为area处理数据时的每一个数据对象
//value即为根据d处理后的坐标值
function x0(d){
return value;
}
```
**线条设置**
```javascript
area.curve(d3.curveStep);
```
## 5 stack 堆叠数据生成器
不是用来生成路径
根据原始数据,产生新结构的数据
使用新结构的数据实现堆叠的可视化效果
**堆叠数据的结构**
初始数据
```text
const data = [
{company:'阿里',type:'电子产品',sale:125},
{company:'阿里',type:'服装',sale:352},
{company:'阿里',type:'食品',sale:520},
{company:'京东',type:'电子产品',sale:438},
{company:'京东',type:'服装',sale:85},
{company:'京东',type:'食品',sale:266},
{company:'拼多多',type:'电子产品',sale:92},
{company:'拼多多',type:'服装',sale:316},
{company:'拼多多',type:'食品',sale:283},
];
或
const data = [
{company:'阿里',电子产品:125,服装:352,食品:520},
...
]
```
使用stack完成数据堆叠处理,效果如下:
```text
[
[key:电子产品
[阿里0,125],
[京东0,438],
[拼多多0,92]
],
[key:服装
[阿里125,352+125=477],
[京东438,85+438=523],
[拼多多92,316+92=408]
],
[key:食品
[阿里477,520+477=997],
[京东523,523+266=789]
[拼多多408,408+283=692]
],
]
```
**创建和使用生成器**
```javascript
const stack = d3.stack()
const array = stack(data)
```
* 不是随便的数据就可以处理,要满足一定的要求
* 需要指定堆叠的key,表示哪类数据需要堆叠, 顺序后面可以控制。
* 要确保在一条记录中可以找到所有key所对应的数据。
上述数据结构中,第一个数据就不符合条件,需要处理后再堆叠
**堆叠设置**
```javascript
stack.keys(['电子产品','服装','食品']) ;
```
* 指定堆叠的所有key,数据堆叠处理时,会根据这些key找到对应的数据,并堆叠
```javascript
stack.value( fn )
```
* 当使用指定的key,可以从当前数据中取出指定的值,就可以直接堆叠计算
* 如果使用当前的key,不能直接取出数据 需要使用value函数,来提供value获取器,指定具体获得value值过程
```javascript
const data = [
{company:'阿里',电子产品:125,服装:352,食品:520},
{company:'京东',电子产品:438,服装:85,食品:266},
{company:'拼多多',电子产品:92,服装:316,食品:283},
]
const stack = d3.stack()
.keys(['电子产品','服装','食品'])
stack(data)
```
**数据排序**
```javascript
stack().order( d3.stackOrderNone)
```
* `d3.stackOrderNone`默认 按照keys顺序
* `d3.stackOrderReverse` 按照key的反顺序
* `d3.stackOrderAscending` key对应数据的总数升序
* `d3.stackOrderDescending` key对应数据的总数升序
> 设置不同的排序方式,堆叠数据的最大值所在的位置也会改变
>
> 最终以index顺序为准, 不以数组下标位置为准
```javascript
const y = d3.scaleLinear()
//.domain([0 , d3.max(array[array.length-1] ,d=>d[0] ) ])
.domain([0,d3.max( array.filter(d=>d.index==array.length-1)[0] , d=>d[1] )])
.range([400 , 0])
.nice()
```
**数据分组处理**
* 将一些无法直接堆叠的数据,进行分组,使得可以堆叠
* 允许堆叠数据的特点是:在一条记录中,所有的key都可以找到与之对应的数据
* 有些结构简单,通过key可以直接获得数据
* 有些结构复杂,需要一些列处理才可以找到key对应的数据
```javascript
d3.group(data,d=>d.company)//按照指定的key进行分组
```
* 此时在使用stack数据堆叠时,无法直接根据key="电子产品"找到对应的数据,需要一定的查找逻辑
* 此时使用stack.value函数指定查找逻辑
* value函数会传递4个参数,前两个比较重要
```javascript
stack.value(function(array,key){
const arr = array[1]
for(let i=0;i
```javascript
//按照指定的多个key一直分组,先按照企业分组,每一个企业中的所有数据再按照产品类型分组
d3.index(data,d=>d.company,d=>d.type);
```
```javascript
stack.value(function(array,key){
return array[1].get(key).sale
})
```
## 6 lineRadial径向线生成器
理解成line生成器翻版
line 会根据数据和比例尺,直接计算出每一个点的x,y坐标,连线
lineRadial 会根据数据和比例尺,计算出每一个点位置的角度和半径 (基于原点+y轴负方向),连线
**创建设置并使用生成器**
```javascript
const lr = d3.lineRadial()
.angle( fn(d) )
.radius( fn(d) )
.curve( d3.curveLinear )
lr(data)
```
* d 是 data中的每一条记录
* 通过我们传递的函数,根据d计算每条记录对应的角度和半径。
```javascript
const angleScale = d3.scaleTime()
.domain(d3.extent(data,d=>new Date(d.date)))
.range([0 , Math.PI * 2 ])
const raduisScale = d3.scaleLinear()
.domain(d3.extent(data,d=>d.avg))
.range([150,200])
const lr = d3.lineRadial()
.angle( d=>angleScale(new Date(d.date)) )
.radius( d=>raduisScale(d.avg) )
.curve( d3.curveNatural)
const d = lr(data) ;
```
## 7 areaRadial径向区域生成器
area区域图翻版
将x0,y0 , x1 , y1的计算 转换成角度和半径的计算
* `x0---startAngle`
* `x1---endAngle`
* `y0---innerRadius`
* `y1---outerRadius`
* `x---angle startAngle == endAngle`
* `y---radius innerRadius == outerRadius`
**创建设置使用生成器**
```javascript
const angleScale = d3.scaleTime()
.domain(d3.extent(data,d=>new Date(d.date)))
.range([0 , Math.PI * 2 ])
const radiusScale = d3.scaleLinear()
.domain([d3.min(data,d=>d.min) , d3.max(data,d=>d.max)])
.range([150,200])
const ar = d3.areaRadial()
.angle(d=>angleScale(new Date(d.date)))
.innerRadius(d=>radiusScale(d.min))
.outerRadius(d=>radiusScale(d.max))
const d = ar(data);
```
## 8 symbol符号生成器
绘制一些小图标
```javascript
const symbol = d3.symbol(type,size)
const d = symbol();
```
* 图形默认从0,0原点开始绘制。
* type:有一些列的type类型,主要分2部分 symbolsFill , symbolsStroke 。都是数组,包含了适合的图标
* `d3.symbolCircle`
* `d3.symbolSquare`
* `d3.symbolStar`
* ......
* size:设置图标(面积)大小
## 9 path 自定义路径
使用`d3.path()`生成一个path对象 (实际是 CanvasPathMethods对象)
其提供了与canvas2D 类似的绘图方法
```text
path.moveTo(x,y)
path.lineTo(x,y)
path.arc(x , y , r , startAngle , endAngle , anticlockwise)
path.rect(x,y,width,height)
path.quadraticCurveTo(cx,cy,x,y);
path.bezierCurveTo(cx1,cy1,cx2,cy2,x,y);
path.closePath()
```
* context.beginPath() , context.save() , context.stroke() 等都不属于图形路径绘制方法。
```javascript
const context = d3.path();
context.moveTo(0,0);
context.lineTo(10,0);
context.lineTo(10,-5);
context.lineTo(20,3);
context.lineTo(10,11);
context.lineTo(10,6)
context.lineTo(0,6);
context.closePath()
svg.append('g').attr('transform','translate(400,300)')
.append('path')
.attr('d',context.toString())
.attr('fill','none')
.attr('stroke','#666')
```
## 10 canvas图形绘制
每一个路径生成器,都有一个context方法。可以用来传递canvas的context对象,就相当于拿context绘制了对应的图形。
我们只需要通过context设置状态,实现填充或描边操作即可。
```javascript
arc.context(context);
line.context(context);
area.context(context);
...
```
```javascript
const arc = d3.arc()
.innerRadius(100)
.outerRadius(200)
.padAngle(0.1)
.cornerRadius(10)
const canvas = document.createElement('canvas')
canvas.width = width ;
canvas.height = height ;
canvas.style.border = '2px solid #00f';
document.body.append(canvas);
const context = canvas.getContext('2d');
context.save();
context.translate(400,300)
context.beginPath();
context.strokeStyle = '#fac' ;
context.strokeWidth = 2 ;
context.fillStyle = '#acf' ;
//一切准备就绪了,准备绘制一个弧路径时
//设置context对象,表示使用d3的弧生成器来绘制canvas图形
arc.context(context);
//开始绘制,此时返回值d没有意义,相当于在arc方法内部,调用了context.moveTo,arc等方法
let d = arc({
startAngle:0,
endAngle:Math.PI/2,
})
//图形路径绘制完毕,选择填充或描边。
context.stroke();
context.fill()
```
# 九 过渡动画
## 1 动画实现过程
* 选择需要动画元素
* 创建动画对象
* 动画时间设置(持续时间,延迟时间)
* 动画效果设置(属性,样式,文本)
```javascript
g.select('circle')
.transition()
.duration(2000)
.delay(1000)
.attr('r',200)
```
## 2 selection与transition对象
selection是d3应用的基本对象。 select , selectAll , append , insert等方法创建。
通过调用transition()方法,就可以创建一个transition对象,包含了标签元素,可以对包含的元素进行过渡处理
所以transition与selection有类似的方法
* select() , selectAll() 基于transition现有的标签,寻找子标签元素
* attr() , style() , text()
```javascript
const data = ['a','b','c','d'] ;
const colorScale = d3.scaleOrdinal()
.domain(data)
.range(d3.schemeSet3)
const g = svg.append('g').attr('transform','translate(100,100)')
g.selectAll('circle')
.data(data)
.join('circle')
.attr('cx',(d,i)=>i*100 + 50)
.attr('cy',50)
.attr('r',45)
.attr('fill',d=>colorScale(d))
g.transition()
.duration(2000)
.attr('transform','translate(100,300)')
.selectAll('circle')
.transition(2000)
.delay((d,i)=>i*1000)
.attr('fill',(d,i)=>i===data.length-1?colorScale(data[0]):colorScale(data[i+1]))
```
> transition对象不能重复动画, 要开启新动画, 需要创建新的transition对象。
## 3 控制动画
**中断动画**
```javascript
selection.interrupt()
```
```javascript
circles.on('click',(e,d)=>{
//终止之前的动画
circles.interrupt()
//开启新动画
d3.select(e.target)
.transition('a')
.duration(3000)
.attr('cy',350)
})
```
**监控动画**
```javascript
transition
.on('start' , (d,i,list)=>{} )
.on('interrupt' , (d,i,list)=>{})
.on('end' ,(d,i,list)=>{} )
```
* list[i] 就是触发当前监控事件的标签对象
* 如果使用function函数,this就是触发事件的标签对象。
```javascript
circles.on('click',(e,d)=>{
//终止之前的动画
circles.interrupt()
d3.select(e.target)
.transition('a')
.duration(3000)
.attr('cy',350)
.on('interrupt',function(d,i){
d3.select(this).attr('fill','#ccc')
})
.on('end',(d,i,list)=>{
d3.select(list[i]).attr('fill','steelblue')
})
})
```
**动画命名**
```javascript
selection.transition('a')
```
* 如果没有命名,会认为是两个相同名字的动画
* 相同名字的动画,后者会覆盖前者。 后者执行时,前者停止。
* 如果需要两个动画同时执行,就要为不同的动画设置不同的名字。
## 4 缓动函数
控制动画过程中属性变化的速率 (匀速,先快后慢,先慢后快,先快后慢再快 等)
```javascript
tansition.ease(缓动函数)
```
`d3.easeLinear 匀速`
`d3.easeExpIn 先慢后快(指数)`
`d3.easeExpOut 先快后慢`
`d3.easeExpInOut 先慢后快再慢`
`d3.easeBounceIn 弹跳式`
更多的缓动函数见官方文档:https://d3js.org/d3-ease#_ease
**缓动函数机制**
* 默认会存在一个比值: 时间与距离
* 2s时间 实现cx 从 100-500的变化 2s/400
* 在运动过程中,会不停的调用缓动函数,并传递一个比例t [0,1] ,表示时间比例
* 目前已经执行的时间与总时间比值 0.5s/2s = > t = 0.25
* 在根据默认比值,就知道0.25时刻对应的0.5s对应的变化距离应该是 100
* t=0表示开始状态, t=1表示终止状态
* 在缓动函数中,可以自定义计算逻辑,使得根据传入的t,计算并返回一个t' [0,1]
* 表示在t时刻,变化到t’时刻的状态
* t=0.5 , t'=t=0.5 , 在0.5时刻,数值变换到0.5时刻对应的200
* t=0.5 , t'=t*t=0.25 在0.5时刻,数值变换到0.25时刻对应的 100
**自定义缓动函数**
```javascript
g.append('circle')
.attr('cx',50)
.attr('cy',150)
.attr('r',50)
.attr('fill','#fac')
.on('click',(e,d)=>{
d3.select(e.target)
.transition()
.duration(2000)
.ease(myEase)
.attr('cx',650)
})
let i = 1 ;
let b = true ;
function myEase(t){
i++;
if(i%20 == 0){
b = !b ;
i = 1;
}
return b ? t*t : t*t*t ;
}
```
## 5 插值器
在每一个是t之间数值是如何变化的, 由插值器决定。
**插值器特点**
* 用来定义属性过渡的效果,也可以定义两点之间的连线插值
* 也就是从a变换到b的过程中,每一个时刻需要变化的值,默认每一个时刻变化的值是相同的(线性,匀速)
* 例如a = 100 , b = 200 也就是100-200之间过渡
* 0.1时刻,长度应该为110,也就是在100-200之间插入一个110
* 0.2时刻,长度应该为120
* 插值器也需要变换的时刻t
**插值器种类**
`d3.interpolateNumber(0,100) 返回数字插值器`
`d3.interpolateRound(0,100) 返回正数插值器`
`d3.interpolateDate(date1,date2) 返回日期插值器`
`d3.interpolateString('0px','100px') 返回字符串插值器`
* 最终还是根据字符串中的数字进行插值,所以最后还是0-100的变化
* 数字的变化符合interpolateNumber插值器
* 插入值的格式与最终字符串的格式相同 ('0a','10px') , t=0.1是, 插入的值1px
* 如果字符串中有多组数字,每组数字都进行插值('0a10b','100x50y') t=0.1 插入的值 10x10.5y
* 如果字符串中的数字分组不匹配,就只对匹配的分组插值,剩余的作为格式
* ('10px 100px' , '20px 200px')(0.5) = '15px150px'
* ('10px 100px' , '20px')(0.5) = '15px'
* ('10px' , '20px 200px')(0.5) = '15px 200px'
`d3.interpolateRgb('rgb(100,0,200)' , 'rgb(200,0,100)') 返回rgb颜色插值器`
* 即使传递的是red 或#f00 最终也会转换成rgb数字
* t=0.5 插入的rgb(150,0,150)
`d3.interpolateHsl('hsl(90)' , 'hsl(120)') 返回hsl颜色插值器`
* d3提供了一些颜色转换的函数
```javascript
const color = d3.color('red' / 'rgb(255,0,0)' / '#f00' / 'hsl(0 100% 50%)')
color.formatRgb()
color.formatHex()
color.formatHsl()
d3.rgb('red');
d3.hsl('red');
```
* 数组插值, 对象插值, 变换插值
**自定义插值器**
* 自定义插值器工厂函数, 需要定义两个参数 start, end
* 工厂函数会返回一个插值器函数,插值器函数需要定义比例t
* 插值函数利用start, end , t 计算指定时刻对应的插值并返回
```javascript
function myInterpolate(start , end){
return function(t){
return start + (end - start) * t
}
}
```
**过渡中使用插值器**
* 过渡的内容一般包括 attr, style , text
* 默认情况下, 数字使用数字插值器, 字符串使用字符串插值器, 日期使用日期插值器,颜色使用颜色插值器
* transition在提供attr这些过渡函数外,还提供了attrTween , styleTween , textTween函数,指定插值器
> text默认没有插值器, 直接返回设置的最终值。
>
> 如果希望text也有一个过渡的效果,需要使用textTween方法
```javascript
g.append('text')
.attr('x',10)
.attr('y',20)
.text('0px')
.transition()
.duration(2000)
.attr('x',610)
//.text('610px')
// .textTween(()=>{
// return d3.interpolateString('0px','610px');
// })
.textTween(()=>{
return myInterpolate(0,610);
})
function myInterpolate(start , end){
const interpolate = d3.interpolateRound(start,end);
return function(t){
return Math.round(start + (end-start)*t) + ' px'
//return interpolate(t) + ' px';
}
}
```
> 使用d3提供的字符串插值器interpolateString 过渡过程中会出现小数,这里使用所以自定义字符串插值器
# 十 交互
## 1 drag 拖拽工具
**创建工具**
```javascript
const drag = d3.drag() ;
```
**应用工具**
* 将拖拽工具应用在需要实现拖拽行为的标签上 (selection)
```javascript
const circle = d3.select('circle');
circle.call(drag);
drag(circle);
```
* 可以有拖拽操作,但没有效果
* 所谓的应用拖拽工具,表示可以监控这个标签拖拽数据
* 坐标点, 移动距离
* 最终的拖拽效果需要我们利用拖拽数据,编码实现。
**监听拖拽行为**
```javascript
drag.on('start',(e,d)=>{})
.on('drag',(e,d)=>{})
.on('end',(e,d)=>{})
```
* start 鼠标按下 监控拖拽开始,监控目标是标签
* drag 鼠标移动 监控拖拽过程,监控目标是window
* end 鼠标抬起 监控拖拽结束,监控目标是window
* e 事件对象
* 这个e是d3封装后的事件对象
* 包含 x , y 当前鼠标所在的位置坐标
* 包含 dx , dy 基于上一次鼠标移动距离
* 包含 sourceEvent 原生事件对象 , 包含target 目标标签
* d 触发当前拖拽行为的标签绑定的数据。
> selection包含多个标签时,每次只会对其中的一个标签进行拖拽行为,所以需要对那一个标签属性做修改
>
> 一般都是通过e.sourceEvent.target获得触发事件的标签
>
> 但由于drag监控的是整个window,所以一旦鼠标脱离标签, 尽管可以继续监控拖拽,但无法确定目标
>
> 所以需要额外的一些处理从而确保始终可以操作拖拽的目标。
```javascript
const data = [100,200,300,400,500] ;
const circles = svg.selectAll('circle')
.data(data)
.join('circle')
.attr('cx',d=>d)
.attr('cy',100)
.attr('r',40)
.attr('fill',(d,i)=>d3.schemeSet2[i])
let target ;
const drag = d3.drag()
.on('start',(e,d)=>{
target = e.sourceEvent.target ;
})
.on('drag',(e,d)=>{
//d3.select(e.sourceEvent.target)
d3.select(target)
.attr('cx',e.x)
.attr('cy',e.y)
})
.on('end',()=>{
target = null ;
})
circles.call(drag);
```
## 2 brush 刷子工具
轻易轻松刷取(选取)一块区域,并获得区域的范围信息(左上角坐标, 右下角坐标)
刷子工具自带 拖拽区域,放缩区域,移动区域 功能
* 这些功能都不影响我们的可视化标签及效果, 只提供区域信息
* 最终需要我们通过编码 利用刷子信息实现效果变化。
**创建工具**
```javascript
const brush = d3.brush(); //横纵都可以控制
const brush = d3.brushX(); //只能横向控制
const brush = d3.brushY(); //只能纵向控制
```
**设置刷子范围**
* 可以在什么样的范围内,产生拖拽区域,并拖拽
* 可以使用刷子的范围 和 刷子的范围 。
* 这里设置的是使用刷子的范围(边界)
```javascript
brush.extent([ [左上角点] , [右下角点]])
```
**监听获得数据**
```javascript
brush.on('start' , (e,d)=>{}) //鼠标按下
.on('brush' , (e,d)=>{}) //鼠标移动
.on('end' , (e,d)=>{}) //鼠标抬起
```
* e 是d3包装的事件对象
* 包含sourceEvent
* 包含selection二维数组, 包含 左上角坐标, 右下角坐标 。 刷子范围
**应用刷子**
* 必须应用在g标签上
```javascript
g.call(brush)
brush(g);
```
**编码控制刷子区域**
```javascript
bursh.move(g,[ [] ,[] ])
g.call(brush,[[],[]])
```
> 随机产生某一范围内的数字
```javascript
const data = Array.from({length:300} , d3.randomUniform(0,15))
```
* 产生300个 0-15之间的随机数(含小数),并存入数组
## 3 zoom 缩放工具
提供了鼠标的滚轮放缩和拖拽操作
* 在缩放的过程中, 我们只能监听获得缩放信息,不能直接看到缩放的效果
* 需要我们根据缩放信息,自己处理缩放效果。
> 放缩的同时伴随着位置的平移
**创建工具**
```javascript
const zoom = d3.zoom()
```
**监控获得信息**
```javascript
zoom.on('start',(e,d)=>{})
.on('zoom',(e,d)=>{})
.on('end',(e,d)=>{})
```
* 拖拽
* start 鼠标按下, zoom 鼠标拖拽, end 鼠标抬起
* 滚轮
* 每一次滚动都伴随着一组 start + zoom + end
* 隐藏操作(双击)
* 放大
* e d3包装的事件对象
* 包含 sourceEvent , 包含了target
* 包含缩放信息 transform (k , x , y)
* k 缩放比例 (滚轮向上放大, 滚轮向下缩小)
* x,y 平移的位置
> 放缩会影响平移, 拖拽也会影响平移
>
> 拖拽不会影响缩放
**应用工具**
* 一般建议在svg上应用工具。
```javascript
svg.call(zoom)
zoom(svg)
```
**缩放限制**
默认可以无限缩放
* scale [0 , infinity]
* translate [ [-infinity ,-infinity] , [infinity , infinity] ]
```javascript
zoom.scaleExtent([0.5 , 2])
```
```javascript
zoom.translateExtent( [ [0-100,0-100] , [200+100,200+100] ] )
zoom.extent([ [0,0] , [200,200] ])
```
* 放缩会导致平移, 但不放缩也可以平移(拖拽)
* 平移的范围由2个边界决定
* translateExtent 设置平移的外边界
* extent 设置允许平移的参考边界,默认边界[ [0,0] [width,height] ]
```javascript
//平移范围:从0,0移动到-100,-100 向左最多平移100,向上最多平移100
zoom.extent([ [0,0] , [200,200]])
zoom.translateExtent([ [-100,-100] , [300,300] ])
//平移范围:从100,100移动到-100,-100 向左最多平移200,向上最多平移200
zoom.extent([ [100,100] , [200,200]])
zoom.translateExtent([ [-100,-100] , [300,300] ])
```
**缩放计算**
* k , x , y
```javascript
nWidth = width * transform.k
nHeight , nR
nx = x * transform.k + transform.x = transform.applyX(x)
ny = y * transform.k + transform.y = transform.applyY(y)
transform.invertX(nx) ;//计算放缩平移前的x位置
transform.invertY(ny) ;//计算放缩平移前的y位置
//按照比例缩放重新计算比例尺的domain值域 (范围不变)
//放缩过程:
// 0-50 ==> 100,700
// 有了放缩信息
// x.invert(transform.invertX(100)) , x.invert(transform.invertX(700))
// [10,20]值域 ticks 10 11 12 13 14 15 16 17 18 19 20
// 放大2倍。 原来范围可以展示n个数字, 现在范围可以展示n/2 , 但不是10-15,因为两侧都要变大
// 最终应该是 12.5 ,13 , 13.5 14 14.5 15 15.5 16 16.5 17 17.5
transform.rescaleX(scale);
```
**编码控制放缩平移**
```javascript
//指定元素平移指定的距离 x 移动k*dx , y 移动k*dy (相对平移,相对原来的位置)
zoom.translateBy(svg , dx , dy) ;
//指定元素平移至距指定的原点k*dx 和 k*dy距离 (绝对平移)
zoom.translateTo(svg , dx , dy , [cx , cy])
//案例1:zoom.scaleBy(svg,0.5,[400,300]) x平移 = 400-0 * 0.5 = 200
//案例2:zoom.scaleBy(svg,0.5,[0,0]) x平移 = 0-0 * 0.5 = 0
//案例3:zoom.scaleBy(svg,0.5,[100,100]) x平移 = 100-0 * 0.5 = 50
//当连续多次使用scaleBy的时候,后面的比例计算要基于之前的比例计算
//案例4:zoom.scaleBy(svg,0.5,[400,300]) k = 0.5
// zoom.scaleBy(svg,0.5,[400,300]) k = 0.5*0.5 = 0.25
// zoom.scaleBy(svg,0.5,[400,300]) k = 0.25*0.5 = 0.125
zoom.scaleBy(svg , k , [cx,cy])
//连续多次使用scaleTo,每次都是指定的比例k
zoom.scaleTo(svg , k , [cx,cy])
//一次性完成平移和放缩
//默认参考左上角点进行缩放和平移
//每次调用scale或translate方法会返回一个带有新值tranform对象,
const transform = d3.zoomIdentity
.translate(100,100)
.sacle(0.5)
zoom.transform(svg , transform)
```
# 十一 层级数据处理
属于数据处理器, 而不是路径生成器
将原始数据处理层可以绘制层级效果的层级数据
这里注意,不是随便的数据都可以处理成层级数据,需要有一定的规格
## 1 初始格式
```text
const data = {
name:'a',
children:[
{
name:'b',
children:[
{name:'b1'},
{name:'b2'}
]
},
{
name:'c',
children:[
{name:'c1'},
{name:'c2'}
]
},
.....
]
}
```
```text
const data = [
{name:'a',parent:''},
{name:'b',parent:'a'},
{name:'c',parent:'a'},
{name:'b1',parent:'b'},
{name:'b2',parent:'b'},
{name:'c1',parent:'c'},
{name:'c2',parent:'c'},
...
]
```
## 2 初级层级处理
层级数据的可视化需要两次处理
1. 将原始数据处理层d3支持的层级结构,只有结构,没有可视化绘制的信息(坐标,半径,长度)
2. 在之前的层级结构基础上,根据准备绘制的层级可视化效果,处理增加对应的可视化绘制信息
初级层级处理提供两种方式,分别对应上述两种初始格式
**Hierarchy**
* 从父级找子级
```javascript
const rootNode = d3.hierarchy(data)
```
* 将层级数据处理成树形结构, 最终返回的是树结构的根节点
* 有了根节点就可以获得所有的子节点。
> 在层级结构处理时,默认会根据children属性作为子级参考
>
> 如果子级参考属性不是children,可以在数据处理时,指定子级参考
```javascript
const data = {
name:'a',
cs:[..]
}
const rootNode = d3.hierarchy(data,d=>d.cs) //每条记录中的cs属性表示子级信息
```
**Stratify**
* 从子级找父级
* 需要指定子级和父级参考,会按照参考来进行层级处理
```javascript
const s = d.stratify()
.id(d=>d.name) //指定子级信息的参考
.parentId(d=>d.parent) //指定父级信息的参考
const root = s(data)
```
## 3 层级数据结构
处理后,原始数据的中每一条记录(包括子级记录)都会变成一个节点。
有一个根节点
每一个节点包含以下属性:
* data 当前节点对应的原始记录(可以包括子级记录)
* children 包含子节点的数组
* parent 父节点
* depth 深入 ,根节点0 , 每一层+1
* height 节点高度,当前节点到子节点最大距离
* value 默认不存在, 后面对节点做计算时会增加该属性, 记录计算结果。
层级结构提供的一些api
```javascript
const array = node.ancestors() //包含当前节点及其父级节点
const array = node.descendants() //包含当前节点及其所有后代节点(包括子节点的子节点)
const array = node.leaves() ;//包含所有的叶子节点
const node = node.count() ; //计算叶子节点的数量,为当前节点增加一个value属性存储这个数量,
//返回当前节点
const node = node.sum(fn(d)); //返回当前节点,增加value存储计算结果,
//根据函数, 对每条数据进行一个求和
// 这里面的d是每条记录,不是每一个节点
// 根据条件求和后,每一个父节点的value就是所有子节点求和的结果
//求和,未来可以根据和做排序,可以根据排序结构绘制可视化图形
const node = node.sort( fn(node1,node2) );//返回当前节点,对子节点进行大小比较并排序
//传递的函数来指定每两个节点之间的比较规则。
//函数返回 <0 0 >0 表示node1
```javascript
//叶子节点+2, 非叶子节点+0
//每一个节点的value记录的就是其叶子节点个数
/*
a
(4+4+0)
/ \
b c
(2+2+0) (2+2+0)
/ \ / \
b1 b2 c1 c2
2 2 2 2
*/
root.sum(function(d){
return d.cs ? 0 : 2
})
```
## 4 二次层级处理
就是在原有的可视化层级结构的基础上,增加可视化绘制信息。
### 4.1 文件读取数据
使用`d3.json(xxx.json)`读取文件数据
* 需要以服务器方式打开
* 读取需要一定的时间,所以使用的是异步的方式,返回的是一个promise对象
* 如何获得读取的数据呢?
* 利用promise.then(data=>{})
* 利用async 和 await 直接获得返回值
```javascript
let data ;
(async function(){
data = await d3.json('data/china_province_city.json')
})();
```
### 4.2 link图形生成器
实现两个点之间的连线
数据结构需要包含 source 和 target 。
source和target中需要包含起始和结尾的坐标, 可以是对象,也可以是数组。
`{source:{x:1,y:1},target:{x:2,y:2}}`
`{a:[1,1],b:[2,2]}`
提供了 srouce(d=>d.a)和target(d=>d.b) 来指定对应的source属性和target属性
提供了x(s=>s.x) 和 y(t=>t[1]) 来指定source和target中对应的x,y坐标
最终返回的就是一个路径
```javascript
const data = [
{
s:[100,100],
t:[500,60]
},{
s:[100,120],
t:[450,180]
}
]
const link = d3.link(d3.curveBumpX)
.source(d=>d.s) //这里的d是每条记录 {s:[],t:[]}
.target(d=>d.t)
.x(d=>d[0]) //这里的d是 s或t , [x,y]
.y(d=>d[1])
svg.selectAll('path')
.data(data)
.join('path')
.attr('d',link)
.attr('stroke','#666')
.attr('fill','none')
```
### 4.3 tree 树结构处理
按照树级分层结构,计算每一层节点的坐标位置
```javascript
const tree = d3.tree()
.size([width , height]) //指定坐标计算的参考空间,自动控制节点间距
.nodeSize([width,height]) //指定每一个节点坐标范围, 手动控制节点间距
tree(root); //为每一个节点增加增加x和y属性。
```
> 计算时默认是按照从上到下的顺序计算每一层节点的位置
>
> 如果希望从左往右展示,在可视化绘制时,x对应d.y , y对应d.x
### 5 cluster 集群数据处理
所有的叶子节点,无论是第几层,都在相同的区域展示
```javascript
const cluster = d3.cluster()
.size([width-150,height-150])
//.nodeSize([30,200])
cluster(root)
```
### 6 partition分区处理
相当于链接树图的空间填充变体
链接树图中每一个节点对应的坐标点
分区图中每一个节点对应一个区域,这个区域由左上角点和右下角点确定。
所以在二级处理后,会为每一个节点增加(x0,y0) , (x1 , y1) 属性 , 根据属性绘制矩形。
注意:必须提供每一个区域的和数以及排序顺序
```javascript
const p = d3.partition()
.size([width,height])
.round(boolean) // 控制整数计算, 不建议使用
.padding(2) //设置每一个区域之间留白
root.sum().sort()
p(root);
```
### 7 pack 包处理
特点就是父级数据可以绘制大圈, 子级数据可以绘制小圈,大圈套小圈
也需要先求和
处理后会为每一个节点增加 x , y , r 属性
```javascript
const pack = d3.pack()
.size([width,height])
pack(root);
```
### 8 treeMap 处理
特点:每一个节点都是一个矩形区域, 子节点区域在父节点区域内, 最终会覆盖掉父级节点
我们一般只绘制叶子节点。
处理后,为每一个节点增加(x0,y0) , (x1,y1) 属性。
```javascript
const map = d3.treemap()
.size([width,height])
.tile(d3.treemapBinary)
.padding(2)
root.sum()
map(root)
```
### 9 日晕图效果
本质就是partition分区效果
原来都是矩形区域, 日晕图是弧形区域。
size从原来的矩形区域[width,height], 变为现在的圆形区域[弧度/角度 , 半径]
此时x/x0/x1 表示弧度 , x0 起始弧度, x1终点弧度 , y0 短半径, y1 长半径
> 效果中我们会利用rotate旋转来确定文字的书写方向
>
> d3在计算图形弧度角度时,参考的是y轴负方向(12点方向)
>
> rotate旋转角度时,参考的是x轴正方向(3点种方向)
>
> 所以需要注意根据d3计算的角度,旋转时需要额外处理90°偏差。
# 十二 地图数据处理
## 1 geojson地理信息数据结构
本质就是json , 在json的基础上做了扩展, 包含了一些特有的属性,用来存储交互地理信息
**基本结构**
```json
{
type ,
coordinates,
geometries,
geometry,
features,
properties,
crs
}
```
**type地理信息类型**
* Point , MultiPoint 点, 多点
* LineString , MultiLineString 线 , 多线
* Polygon , MultiPolygon 面, 多面
* GeometryCollection 地理信息集合, 包含上述的点线面
* FeatureCollection 地理特征集合, 包含一组下面的地理特征
* Feature 地理特征包含地理信息(点线面)和 特征描述(properties)
**coordinates坐标**
* 不同的地理图形,有不同的坐标
```json
{
type:'Point',
coordinates : [x , y]
}
{
type:'MultiPoint',
coordinates : [
[x1 , y1],
[x2 , y2],
...
]
}
```
```json
{
type:'LineString',
coordinates : [
[x1 , y1],
[x2 , y2],
...
]
}
{
type:'MultiLineString',
coordinates : [
[
[x1 , y1],
[x2 , y2],
],
[
[x1 , y1],
[x2 , y2],
],
....
]
}
```
```json
{
type:'Polygon',
coordinates : [
[ //表示面
[x1 , y1],
[x2 , y2],
],
[ //在面中,扣掉的部分
[x1 , y1],
[x2 , y2],
],
....
]
}
{
type:'MultiPolygon',
coordinates : [
[
[ //表示面1
[x1 , y1],
[x2 , y2],
],
[ //在面中,扣掉的部分
[x1 , y1],
[x2 , y2],
],
....
],
[
[ //表示面2
[x1 , y1],
[x2 , y2],
],
[ //在面中,扣掉的部分
[x1 , y1],
[x2 , y2],
],
....
],
....
]
}
```
**geometries地理信息集合**
表示一个地理信息的集合, 与type="GeometryCollection"类型联用, 表示一组地理信息
```json
{
type:'GeometryCollection',
geometries:[{
type:'Point',
coordinates:[...]
}]
}
```
**features地理特征集合**
表示地理信息特征集合, 由一个一个地理信息特征组成, 每一个特征包括(地理信息,相关特征属性)
与type = "FeatureCollection"类型联用
**geometry和properties**
**geometry**作为features的子对象,表示每一个地理信息
**properties**作为features的子对象,表示每一个地理信息的特征属性(任意)
与type="Feature"类型联用
```json
{
type:'FeatureCollection',
features:[{
type:'Feature',
geometry:{
type:'Point',
coordinates:[]
},
properties:{
key:value
}
},{}]
}
```
## 2 地理信息获取
* 阿里云可视化平台(DataV),可以获得国内的省份信息
http://datav.aliyun.com/portal/school/atlas/area_selector?spm=a2crr.23498931.0.0.29b915ddg9jkRK
* 借助可视化库提供的地图案例信息
* d3
* echarts
* 工具网站
* geojson.io 增加特征属性, 局部重绘
* https://mapshaper.org/ 导出不同格式 (topojson)
## 3 topojson结构
* 处理了geojson中的冗余数据, 节约存储空间
* D3作者制定规则
* 需要引入topojson.js处理topojson格式,将其转换成geojson
```javascript
(async ()=>{
let data = await d3.json('data/china-topo.json') ;
console.log(data); //topojson数据
data = topojson.feature(data , data.objects['china-geo'])
console.log(data); //geojson数据
})()
```
> data.objects属于topojson的固定结构,但内部的china-geo是非固定的
>
> 每次处理topojson数据时, 具体的属性需要自行观察或者参考开发文档
## 4 地图可视化
使用geoPath根据地理数据生成地图路径
```javascript
const path = d3.geoPath()
const d = path(geojson);
```
> 默认的地图效果不是很好,我们可以自定义投影机制(放缩,位移)
>
> 也可以使用d3提供的投影机制。
**path的常用方法**
```java
path.area(geojson) ; //计算地图信息的面积
path.bounds(geojson) ;//计算地图信息的边界 [ [左上角] , [右下角] ]
path.centriod(geojson) ;//计算地图面心坐标
path.projections(projection) //提供投影机制
```
## 5 地图投影
根据地理信息,将其映射到平面坐标系中, 根据最终绘制的不同效果, 会有不同的投影方式。
* 自定义投影(不推荐)
* d3提供的投影器
**投影使用**
```javascript
//创建投影器
const prejection = d3.geoNaturalEarth1()
//应用投影器
const path = d3.geoPath(projection) ;
const path = d3.geoPath();
path.projection(project) ;
```
* 自定义投影(不推荐)
* d3提供多种投影方式
**投影API**
```javascript
projection.fitSize([width,height] , geojson) //自动控制大小
projection.fitExtent([[左上角],[右下角]] , geojson) //根据指定的空间范围控制大小
projection(point) ; //将经纬度点转换成坐标点
projection.invert(point) ; //将坐标点还原成经纬度点
projection.center([x,y]) ; //设置中心点
projection.translate([left , top]) ;//设置平移
projection.rotate([x,y,z]) ;//设置旋转角度
```
## 6 网格生成器
用来绘制经纬度网格,最终生成的是一个经纬度网格的geojson数据
配合path + projection实现路径生成。
**使用网格生成器**
```javascript
const graticule = d3.geoGraticule()
.step([x间隔 , y间隔]) //间隔为0,不显示间隔线
const geojson = graticule(); //网格地理数据
const d = path(geojson); //网格路径
```
## 7 世界地图绘制
