前言
- 了解D3是什么,D3的基本用法
- 完成一个简易的可交互的柱状图
最近假期写了点东西,趁略有闲余之时,玩了一下D3,之前项目中主要是用各类charts,比如Echarts,诸如此类还有highCharts等,不过那种是用别人写好的库,添加好数据就完成了,就是配置的问题,这种类库最大的问题是定制很难,而D3是自己制作图形,可定制方面比较强,自由度很大,做完之后还是感觉很爽的
现在是数据时代,越来越多的人认识到数据的重要性,但是海量的数据却不能直观的展示并不能展现出它的价值,只有将数据分析、总结,才能够展现它的价值。所以有了数据分析和数据可视化。
PS:代码在最底下
D3是什么
D3js 是一个可以基于数据来操作文档的 JavaScript 库。D3 可以借助 SVG, Canvas 以及 HTML 将你的数据生动的展现出来. D3 结合了强大的可视化交互技术以及数据驱动 DOM 的技术, 让你可以借助于现代浏览器的强大功能自由的对数据进行可视化。
点开官网,大大的 Data-Driven Documents ,这就是为什么叫做D3了。
D3基础
安装
D3的引入有两种,一种是:
<script src="https://d3js.org/d3.v5.min.js"></script>
或者是npm或yarn
npm install d3
D3之选择器
D3的选择器有两种,一种是select,一种是selectAll
d3.select():选择所有指定元素的第一个
d3.selectAll():选择指定全部元素
<div class="box"></div>
<div class="box"></div>
<div id="app"></div>
let a1 = d3.select('div')// 标签
let a2 = d3.select('.box')// class的第一个
let a3 = d3.selectAll('.box')// class的全部
let a4 = d3.select('#app')// id
D3之绑定数据
D3绑定数据有两种,一种是datum,通常绑定单个元素,一种是data,通常绑定多种元素
datum():绑定一个数据到选择集上
data():绑定一个数组到选择集上,数组的各项值分别与选择集的各元素绑定
D3还支持向jquery一样的链式操作,比如:
var dataList = [1, 2, 3];
var container = d3.select("#app");
container.selectAll('p')
.data(dataList)// 将数据每一项与元素绑定
.text(function (d, i) {// d代表数据,i是数据的index编号,这里的d就是data()中绑定数据的各项
return "第 " + i + " 个元素绑定的数据是: " + d;
})
.style("color",function (d,i) {// 通过d和i的判断,可以轻松的让每一项执行不同的操作
if (d>2) {
return "red"
}
if (i==0) {
return "yellow"
}
}
D3之添加、删除元素
添加元素有两种,append(),insert(),删除元素是remove()
append():在选择集末尾插入元素
d3.select("body").append("p")
insert():在选择集前面插入元素
d3.select("body").insert("p","#second")
remove():删除元素
d3.select("#second").remove();
理解updata()、enter()、exit()
绑定数据的时候经常会出现数据与元素不匹配的情况,像数据一般动态请求过来,很难知道数据的个数而事前创建好元素,这时候就需要 enter 和 exit 来处理这个问题。enter 操作用来添加新的 DOM 元素,exit 操作用来移除多余的 DOM 元素
(1)数据数量 = 元素数量 此时就是update
(2)数据数量 > 元素数量 此时就是enter
(3)数据数量 < 元素数量 此时就是exit
updata指的是数据数量和元素相等,实际上并不存在这个函数,只是为了要与之后的 enter 和 exit 一起说明才想象有这样一个函数,元素正好满足时,后面直接操作即可。
enter是指数据数量大于元素数量,这个方法是最常用的一个方法,他可以让我们方便的构建出对应的元素而不用事先创建好,后面通常先跟 append() 操作
svg.selectAll("rect") //选择svg内所有的矩形
.data(dataList) //绑定数据
.enter() //指定选择集的enter部分
.append("rect") //添加足够数量的矩形元素
exit 是指数据数量小于元素数量,通常是要删除元素,使之与数据相匹配,后面通常跟 remove() 操作
svg.selectAll("rect") //选择svg内所有的矩形
.data(dataList) //绑定数据
.exit() //指定超出数据数量的元素部分
.remove() //将多余的删除
过渡
D3 支持动画效果,这种动画效果可以通过对样式属性的过渡实现。其补间插值支持多种方式,比如线性、弹性等。此外 D3 内置了多种插值方式,比如对数值类型、字符类型路径数据以及颜色等。
首先需要创建一个过渡对象。每个选择集中都有transition()方法,可用d3.select(“rect”).transition()的方式来创建过渡。
设定延迟的时间。过渡会经过一定时间后才开始发生。单位是毫秒。 transition.delay([delay])
设定过渡的持续时间(不包括延迟时间),单位是毫秒。 transition.duration([duration])
设定过渡样式,例如线性过渡、在目标处弹跳几次等方式。 transition.ease(vlaue[,arguments])
var svg = d3.select("#body")
svg.append("rect")
.attr("fill","yellow")
.attr("x",100)
.attr("y",100)
.attr("width",100)
.attr("height",100)// 初始长宽是100
.transition()// 设置过渡
.duration(750)// 过渡持续时间
.delay(function(d, i) { return i * 10; })// 过渡延迟,自定义,让每个元素过渡延迟不同
.attr("width",300)
.attr("height",300)// 过渡后长宽为300
svg基础
什么是SVG?
SVG 指可伸缩矢量图形 (Scalable Vector Graphics)
SVG 用来定义用于网络的基于矢量的图形
SVG 使用 XML 格式定义图形
SVG 图像在放大或改变尺寸的情况下其图形质量不会有所损失
SVG 是万维网联盟的标准
SVG 与诸如 DOM 和 XSL 之类的 W3C 标准是一个整体
SVG常用的标签有:
矩形 rect
圆形 circle
椭圆 ellipse
线 line
折线 polyline
多边形 polygon
路径 path
具体的大家可以网上查一查,今天主要用的是svg的矩形标签rect
D3之加载外部数据
D3有自己加载数据的方法
比如csv数据可以通过d3.csv(data,function) 进行操作
json同理,只需要将csv改为json
d3.csv('./data.csv').then((result) => {
var container = d3.select("#app");
container.selectAll('h1')
.data(result.columns)
.enter()
.append('h1')
.text(function (d) {
return d;
})
})
data.csv
10,20,30,40,50,60
D3之比例尺的使用
D3中有个重要的概念就是比例尺。比例尺就是把一组输入域映射到输出域的函数。映射就是两个数据集之间元素相互对应的关系。比如输入是1,输出是100,输入是5,输出是10000,那么这其中的映射关系就是你所定义的比例尺。
比例尺可以很方便的定义我们的数据的使用,而不用定死,我们可以定义比例尺,通过他的映射关系就可以很方便的转为正确对应的值了。
D3中有各种比例尺函数,有连续性的,有非连续性的,这里主要介绍两种常用的比例尺
d3.scaleLinear() 线性比例尺
使用d3.scaleLinear()创造一个线性比例尺,而domain()是输入域,range()是输出域,相当于将domain中的数据集映射到range的数据集中。
let scale = d3.scaleLinear().domain([1,5]).range([0,100])
映射关系:
定义好scale后,由此可见,可知
scale(1) // 输出:0
scale(4) // 输出:75
scale(5) // 输出:100
d3.scaleBand() 序数比例尺
d3.scaleBand()并不是一个连续性的比例尺,domain()中使用一个数组,不过range()需要是一个连续域。
let scale = d3.scaleBand().domain([1,2,3,4]).range([0,100])
映射关系:
由此可见:
scale(1) // 输出:0
scale(2) // 输出:25
scale(4) // 输出:75
d3.scaleBand()只针对domain()中的数据集映射相应的值。
这个比例尺可以很方便的让我们定义柱形图的x轴位置
颜色比例尺
D3提供了一些颜色比例尺,10就是10种颜色,20就是20种:
d3.schemeCategory10
d3.schemeCategory20
d3.schemeCategory20b
d3.schemeCategory20c
D3之坐标轴
坐标轴,是可视化图表中经常出现的一种图形,由一些列线段和刻度组成。坐标轴在 SVG 中是没有现成的图形元素的,需要用其他的元素组合构成。D3 提供了坐标轴的组件,如此在 SVG 画布中绘制坐标轴变得像添加一个普通元素一样简单。
D3提供了现成的坐标轴,d3.axisBottom(xScale)。d3.axisLeft(xScale)。其他方向同理
只需要将定义好的比例尺传进去,定义好坐标轴。
call() 函数,其参数是前面定义的坐标轴 axis。
创建一个元素,通过.call()放入定义好的坐标轴即可生成坐标轴
// 为坐标轴定义一个线性比例尺
var xScale = d3.scaleLinear()
.domain([0, d3.max(dataset)])// d3.max()是内置的方法,可在传入的数组中取出最大值
.range([0, 250]);
// 定义一个坐标轴
var xAxis = d3.axisBottom(xScale) //定义一个axis,由bottom可知,是朝下的
.ticks(7); //设置刻度数目
// 生成坐标轴
svg.append("g")
.call(axis);
完整代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>D3柱状图</title>
<script src="https://d3js.org/d3.v5.min.js"></script>
<style>
#wrap{
width: 700px;
height: 500px;
margin: 0 auto;
}
.container {
margin: 40px auto 0;
display: block;
}
#prompt {
width: 60px;
height: 40px;
border-radius: 4px;
text-align: center;
line-height: 40px;
background: #666;
color: #fff;
display: none;
position: absolute;
left: 0;
top: 0;
z-index: 1;
transition: all 0.1s;
}
.tool {
margin-top: 20px;
text-align: center;
}
</style>
</head>
<body>
<div id="wrap">
<div id="prompt"></div>
</div>
<hr/>
<div class="tool">
<button id="btn-sort">排序</button>
<button id="btn-add">添加</button>
</div>
<script>
const WIDTH = 600;// 画布宽度
const HEIGHT = 400;// 画布高度
const PADDING = 30;// 画布四周空间
// 模拟数据
let dataList = [24, 36, 15, 8, 28, 18];
// 排序标记
let sort_flag = false;
// 生成svg画布
d3.select('#wrap')
.append('svg')
.classed('container', true)
.attr('width', WIDTH + PADDING * 2)
.attr('height', HEIGHT + PADDING *2)
.style('background', '#f0f0f0')
// 容器画布
let container = d3.select('.container')
// y线性比例尺
let yScale = d3.scaleLinear()
.domain([0, d3.max(dataList)])
.range([HEIGHT, 0])// 坐标轴从下往上,所以反过来
let yAxis = d3.axisLeft(yScale);
// x序数比例尺
let xScale = d3.scaleBand()
.domain(d3.range(dataList.length))
.range([0, WIDTH])
.paddingInner(0.2)// 定义柱形之间的间隙
let xAxis = d3.axisBottom(xScale);
// y轴坐标轴展示
container.append('g')
.attr('id', 'yAxis')
.attr('transform', 'translate(' + PADDING + ',' + PADDING + ')')
.call(yAxis)
// x轴坐标轴展示
container.append('g')
.attr('id', 'xAxis')
.attr('transform', 'translate(' + PADDING + ',' + (HEIGHT + PADDING) + ')')
.call(xAxis)
// 柱状图容器
let rectGroup = container.append('g').attr('id', 'rectGroup');
// 文本容器
let textGroup = container.append('g').attr('id', 'textGroup');
// 加载rect
function renderRect() {
rectGroup.selectAll('rect')
.data(dataList)
.enter()
.append('rect')
.classed('rect', true)
.on('click', (d)=>{// 添加点击提示的事件
let x = d3.event.pageX;
let y = d3.event.pageY;
d3.select('#prompt')
.style('display', 'block')
.style('left', x + 'px')
.style('top', y + 'px')
.text(d)
})
container.selectAll('.rect')
.attr('width', xScale.bandwidth())
.attr('height', 0)
.attr('x', (d, i)=>{
return xScale(i) + PADDING
})
.attr('y', (d, i)=>{
return HEIGHT + PADDING
})
.style('fill', 'skyblue')
.transition()// 设置过渡
.duration(300)
.delay((d, i)=>{
return i * 20
})
.attr('height', (d, i)=>{
return HEIGHT - yScale(d)
})
.attr('y', (d, i)=>{
return yScale(d) + PADDING
})
}
// 加载文本
function renderText() {
textGroup.selectAll('text')
.data(dataList)
.enter()
.append('text')
.classed('text', true)
textGroup.selectAll('text')
.attr('text-anchor', 'middle')// 将文本中点设置为中心
.text((d, i)=>{
return d
})
.attr('x', (d, i)=>{
return xScale(i) + xScale.bandwidth() / 2 + PADDING
})
.attr('y', HEIGHT + PADDING - 10)
.transition()
.duration(300)
.delay((d, i)=>{
return i * 20
})
.attr('y', (d, i)=>{
return yScale(d) + PADDING - 10
})
.style('fill', (d, i)=>{
if (d > 25) {// 大于25的文本显示为红色
return 'red'
}
})
}
// 更新视图(修改数据时调用)
function refresh() {
// 更新比例尺
yScale.domain([0, d3.max(dataList)])
d3.select('#yAxis').call(yAxis);
xScale.domain(d3.range(dataList.length))
d3.select('#xAxis').call(xAxis);
// 重新加载内容
renderRect();
renderText();
}
// 加载事件
function initEvent() {
// 离开时关闭提示
d3.select('#wrap').on('mouseleave', ()=>{
d3.select('#prompt').style('display', 'none')
})
// 点击排序
d3.select('#btn-sort').on('click', ()=>{
rectGroup.selectAll('rect')
.sort((a, b)=>{// d3自带方法,升序降序
return sort_flag ? d3.descending(a, b) : d3.ascending(a, b)
})
.transition()
.duration(500)
.attr('x', (d, i)=>{
return xScale(i) + PADDING
})
textGroup.selectAll('text')
.sort((a, b)=>{// d3自带方法,升序降序
return sort_flag ? d3.descending(a, b) : d3.ascending(a, b)
})
.transition()
.duration(500)
.attr('x', (d, i)=>{
return xScale(i) + xScale.bandwidth() / 2 + PADDING
})
sort_flag = !sort_flag;// 切换顺序
})
// 点击添加
d3.select('#btn-add').on('click', ()=>{
let randomNum = Math.ceil(Math.random() * 50)// 生成一个随机数字
dataList.push(randomNum)// 放入数据
refresh();// 更新视图
})
}
// 视图初始化
renderRect();
renderText();
initEvent();
</script>
</body>
</html>