# 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 1688097959944 > 该平台有自己的一些语法规范, 所以对于初学者,还需要先了解这些新语法。 # 三 元素操作 ## 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__属性`), 重复获得标签对象, 绑定的数据依然存在 1688117158712 ## 4 数据key映射 将数据与标签匹配。 内部的匹配机制: * 绑定数据时,会依次遍历selection中的所有标签元素 和 数据源 * 遍历过程中,会调用一个key函数,函数的返回值即为当前数据的key,默认返回的是下标 * 根据key,将数据绑定在对应的镖旗 1688355204229 可以提供key函数,自定义映射机制。 ```javascript //d 就是当前的数据 (遍历元素时,d=undefined) //i 是下标 , 元素和数据有各自的下标 //list 当前内容所属的集合 (数据 list是数组, 元素 list就是NodeList集合 key = function(d , i , list){ } d3.selectAll('div').data(array,key); ``` 1688354712868 ```html
``` ## 5 enter() exit() 当数据与标签 数量不配时,通过这些函数对多余的标签 和 数据做处理 * 数据多了,需要增加标签(apped) * 标签多了,需要删除多余标签(remove) **enter()** 获得多余的数据,配合append方法,将多余的数据绑定在新添加的标签中。 ```html
1
2
3
``` 1688357157546 **exit()** 获得多余的标签,配合remove方法,删除这些多余的标签 ```html
1
2
3
4
5
6
``` 1688357378115 > 如果设置了自定义key函数,exit方法最终返回的是没有匹配的那部分多余的标签元素。 1688363329652 ## 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。多余的标签设置背景蓝色 1688366980980 ```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) ``` ![1688367096834](images/09.png) ## 7 多重数据绑定 **数据绑定及绑定数据的应用** 1688368978640 * 数据绑定,会将数组中的每一个数据与标签绑定, 存储在标签对象的`__data__`属性中 * 修改标签时(attr , style , text 等), 如果提供的是一个函数 * 底层遍历处理selection包含的标签元素时,每一次都会调用这个函数,并将当前标签绑定的数据作为参数传递给这个函数 * 我们可以利用这个参数提供最终的修改结果 **多重数据绑定** 1688370053633 * 数据结构比较复杂,一般都是数组中还包含了子数组 * 在第一层数据绑定后,修改当前标签或子标签时,都会传递绑定的数据,可以根据需要使用这个数据 * 也可以为子标签再次绑定数据,绑定的可以是新的数组,也可以是当前绑定数据中的子数组,这样子标签绑定的就是新的数据了。 ```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 线性比例尺 值域 和 范围都是连续的 1688519935200 **创建比例尺** * 需要传递两个数组参数 * 第一个数组是值域的最小值和最大值 * 第二个数组是可视化范围的最小值和最大值 ```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)) ``` * 默认提供数字, 获得的是指定数量的刻度值 * 也可以指定时间间隔, 获得每个间隔的刻度值 * 可以指定的时间种类有多种: ![1688459034651](C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1688459034651.png) ## 3 ordinal scale 序数比例尺 值域和范围都是非连续的(离散点) 比较常用的就是颜色的映射 1688520022099 **创建和使用比例尺** ```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) ``` ![1688461109617](C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1688461109617.png) > 两个颜色之间也可以是连续的,可以配合具有连续特点的比例尺来使用。 ## 4 quantize scale 量化比例尺 值域是连续的,范围是非连续的 会对连续的值域空间分段,每一段对应范围的一个离散值 如: 0-60 及格 , 60-80 良好 , 80-100 优秀 1688522634954 **创建和使用比例尺** ```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分段比例尺 条带比例尺 值域是非连续的, 范围是连续的 由于可视化数据会影响图形绘制, 所以对每一段可视化数据需要了解更多的信息。如:其实数据,宽度等 多用来绘制柱状图 1688524609730 **创建和使用比例尺** ```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控制坐标轴的一些样式细节。 > > 如:设置刻度字体大小,刻度线粗细,颜色等。 1688540792826 ```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°) 1688615195564 **统一设置弧信息** * 在绘制多个弧图形时,有些信息是不变的。 ```javascript arc.innerRadius() .outerRadius() .startAngle() .endAngle() ``` **设置弧两端的留白** * 角度和半径可以控制留白距离 * 角度和半径越大, 留白越大。 反之越小 * 角度默认为0,所以角度一定要设置。 半径有默认值可用。 * 产生留白后的弧形 与 之前的弧形两端平行,直到内弧消失,弧形状才会变化。 ```javascript arc.padAngle(Math.PI/8); arc.padRadius(200) ; ``` 1688615364856 **设置弧的圆角** * 弧的4个断点也设置成弧线。 ```javascript arc.cornerRadius(10) ``` 1688615532905 **获得弧的中心位置** * 获得的是一个坐标,由x和y组成 * 需要传递弧的信息,根据信息计算中心点的位置 ```java const [x,y] = arc.centroid({....}) ``` 1688615558400 ## 2 pie 饼生成器 不是用来生成路径的。 根据可视化数据,生成弧形数据(startAngle , endAngle) 配合弧生成器, 快速绘制饼图或环形图。 **创建饼生成器** ```javascript const pie = d3.pie(); ``` **生成弧数据** ```javascript const array = pie(data) ``` * 生成的数据数量与原始数据的数量相同 * 新数组包括 * data 当前位置的数据 * startAngle和endAngle * index顺序,影响角度顺序,最终会影响环形图顺序 1688626004761 **指定用于角度计算的属性值** * 当数据是对象形式存在时,指定用来计算角度的属性值。 ```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) ``` 1688626678644 ## 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) ; ``` 1688630786005 **设置曲线连接** ```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 1688630882344 ## 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); ``` 1688700084949 ## 5 stack 堆叠数据生成器 不是用来生成路径 根据原始数据,产生新结构的数据 使用新结构的数据实现堆叠的可视化效果 1688719728377 **堆叠数据的结构** 初始数据 ```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) ``` 1688712882200 1688712955684 **数据排序** ```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进行分组 ``` 1688717805164 * 此时在使用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); ``` 1688718695592 ```javascript stack.value(function(array,key){ return array[1].get(key).sale }) ``` 1688719441584 ## 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) ; ``` 1688956722776 ## 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); ``` 1688969403376 ## 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:设置图标(面积)大小 1688972112223 ## 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') ``` 1688974318667 ## 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() ``` 1688976546592 # 九 过渡动画 ## 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() 1689045530162 ```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 默认不存在, 后面对节点做计算时会增加该属性, 记录计算结果。 1689575875462 层级结构提供的一些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 }) ``` 1689577875156 ## 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') ``` 1689580488762 ### 4.3 tree 树结构处理 按照树级分层结构,计算每一层节点的坐标位置 ```javascript const tree = d3.tree() .size([width , height]) //指定坐标计算的参考空间,自动控制节点间距 .nodeSize([width,height]) //指定每一个节点坐标范围, 手动控制节点间距 tree(root); //为每一个节点增加增加x和y属性。 ``` 1689581320136 > 计算时默认是按照从上到下的顺序计算每一层节点的位置 > > 如果希望从左往右展示,在可视化绘制时,x对应d.y , y对应d.x 1689582256785 ### 5 cluster 集群数据处理 所有的叶子节点,无论是第几层,都在相同的区域展示 ```javascript const cluster = d3.cluster() .size([width-150,height-150]) //.nodeSize([30,200]) cluster(root) ``` 1689582943376 ### 6 partition分区处理 相当于链接树图的空间填充变体 链接树图中每一个节点对应的坐标点 分区图中每一个节点对应一个区域,这个区域由左上角点和右下角点确定。 所以在二级处理后,会为每一个节点增加(x0,y0) , (x1 , y1) 属性 , 根据属性绘制矩形。 注意:必须提供每一个区域的和数以及排序顺序 ```javascript const p = d3.partition() .size([width,height]) .round(boolean) // 控制整数计算, 不建议使用 .padding(2) //设置每一个区域之间留白 root.sum().sort() p(root); ``` 1689583816297 1689584844849 ### 7 pack 包处理 特点就是父级数据可以绘制大圈, 子级数据可以绘制小圈,大圈套小圈 也需要先求和 处理后会为每一个节点增加 x , y , r 属性 ```javascript const pack = d3.pack() .size([width,height]) pack(root); ``` 1689586323216 ### 8 treeMap 处理 特点:每一个节点都是一个矩形区域, 子节点区域在父节点区域内, 最终会覆盖掉父级节点 ​ 我们一般只绘制叶子节点。 处理后,为每一个节点增加(x0,y0) , (x1,y1) 属性。 ```javascript const map = d3.treemap() .size([width,height]) .tile(d3.treemapBinary) .padding(2) root.sum() map(root) ``` 1689646314416 ### 9 日晕图效果 本质就是partition分区效果 原来都是矩形区域, 日晕图是弧形区域。 size从原来的矩形区域[width,height], 变为现在的圆形区域[弧度/角度 , 半径] 此时x/x0/x1 表示弧度 , x0 起始弧度, x1终点弧度 , y0 短半径, y1 长半径 > 效果中我们会利用rotate旋转来确定文字的书写方向 > > d3在计算图形弧度角度时,参考的是y轴负方向(12点方向) > > rotate旋转角度时,参考的是x轴正方向(3点种方向) > > 所以需要注意根据d3计算的角度,旋转时需要额外处理90°偏差。 1689649345696 # 十二 地图数据处理 ## 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 1692601437995 * 借助可视化库提供的地图案例信息 * d3 * echarts * 工具网站 * geojson.io 增加特征属性, 局部重绘 1692601551271 * https://mapshaper.org/ 导出不同格式 (topojson) 1692601610363 ## 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]) ;//设置旋转角度 ``` 1692601706285 ## 6 网格生成器 用来绘制经纬度网格,最终生成的是一个经纬度网格的geojson数据 配合path + projection实现路径生成。 **使用网格生成器** ```javascript const graticule = d3.geoGraticule() .step([x间隔 , y间隔]) //间隔为0,不显示间隔线 const geojson = graticule(); //网格地理数据 const d = path(geojson); //网格路径 ``` 1692601734805 ## 7 世界地图绘制 1692601786509