d3
绘制图表
柱状图
最基础部分
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>绘图区</title>
<style>
*{
box-sizing: border-box;
}
#main{
margin: 20px;
width: 600px;
height: 600px;
border: 1px solid #ddd;
}
.item{
cursor: pointer;
}
.item:hover{
opacity: 0.9;
}
</style>
</head>
<body>
<div id="main"></div>
<script src="https://d3js.org/d3.v6.js"></script>
<script>
/*===========1-必备数据===========*/
/*categories 类目信息*/
const categories=['html','css','js'];
/*数据源source:两个系列的数据*/
const source=[
//html css js
[ 30, 20, 40], //学习人数
[ 40, 30, 50], //就业人数
]
/*调色盘*/
const color=['#c23531','#2f4554', '#61a0a8', '#d48265', '#91c7ae','#749f83', '#ca8622', '#bda29a','#6e7074', '#546570', '#c4ccd3'];
/*===========2-建立容器对象===========*/
/*获取main 容器*/
const main=d3.select('#main')
/*声明绘图框尺寸
width 宽度,600
height 高度,600
*/
const width=600
const height=600
/*建立svg 对象
* svg 画布尺寸100%充满容器对象
* 绘图框尺寸按照600设置
* */
const svg=main.append('svg')
.attr('version',1.2)
.attr('xmlns','http://www.w3.org/2000/svg')
.attr('width','100%')
.attr('height','100%')
.attr('viewBox',`0 0 ${width} ${height}`)
/*===========3-建立轴相关的基础数据===========*/
/*-----x轴相关的基础数据-----*/
/*计算类目数量 len*/
const len=categories.length
/*用range()方法,基于类目数量,获取x轴的在图表坐标系中的数据 xChartData,如[0,1,2]*/
const xChartData=d3.range(len)
/*x轴在像素坐标内的起始点和结束点 xPixelRange,左右各偏移50*/
const xPixelRange=[50,width-50]
/*-----y轴相关的基础数据-----*/
/*计算数据源中所有数据的极值 maxY
* 用js原生方法flat()展开数据源,再通过max()方法取极值
* */
const maxY=Math.max(...source.flat())
/*声明y轴在图表坐标系中的数据起点和结束点 yChartRange*/
const yChartRange=[0,maxY]
/*声明y轴在像素坐标系中的数据起点和结束点 yPixelRange*/
const yPixelRange=[height-50,50]
/*===========4-建立比例尺===========*/
/*-----x 轴比例尺 xScale-----*/
/*
* 用scaleBand()方法建立分段比例尺 xScale
* 用domain()方法在比例尺中写入图表数据xChartData
* 用rangeRound()方法在比例尺中写入像素数据,即像素的起始位和结束位xPixelRange
* 用padding()方法设置类目的内边距,百分比单位,如0.1
* */
const xScale=d3.scaleBand()
.domain(xChartData)
.rangeRound(xPixelRange)
/*-----y 轴比例尺 xScale-----*/
/*
* 用scaleLinear()方法建立线性比例尺 yScale
* 用domain()方法在比例尺中写入图表数据yChartRange
* range()方法在比例尺中写入像素数据,即像素的起始位和结束位yPixelRange
* */
const yScale=d3.scaleLinear()
.domain(yChartRange)
.range(yPixelRange)
/*===========5-建立轴对象===========*/
/*-----x轴对象-----*/
/*基于比例尺xScale,用axisBottom()方法创建刻度朝下的坐标轴生成器 xAxisGenerator*/
const xAxisGenerator=d3.axisBottom(xScale)
/*利用坐标轴生成器绘制坐标轴
* 在svg中append 加入g 对象
* 用transform 属性中的translateY 设置x轴的y位置
* 用call()方法调用xAxisGenerator轴生成器,生成坐标轴
* 用selectAll()方法选择所有的text文本
* 用text()方法将图表数据设置为类目数据
* 用attr()方法设置字体大小
* */
svg.append('g')
.attr('transform', `translate(0, ${height-50})`)
.call(xAxisGenerator)
.selectAll('text')
.text(n=>categories[n])
.attr('font-size','12px')
/*-----y轴对象-----*/
/*基于比例尺yScale,用axisLeft()方法创建刻度朝左的坐标轴生成器 yAxisGenerator*/
const yAxisGenerator=d3.axisLeft(yScale)
/*利用坐标轴生成器生成坐标轴
* 在svg中append 加入g 对象
* 用transform 属性中的translate设置y轴的x位置
* 用call()方法调用xAxisGenerator轴生成器,生成坐标轴
* 用attr()方法设置字体大小
* */
svg.append('g')
.attr('transform','translate(50 0)')
.call(yAxisGenerator)
.attr('font-size','12px')
/*===========6-建立绘图区相关的基础数据===========*/
/*-----绘图区相关的基础数据-----*/
/*用x轴比例尺xScale的bandwidth()方法获取x轴上一个类目的像素宽xBandW*/
const xBandW=xScale.bandwidth()
console.log('xBandW',xBandW);
/*获取系列的数量n*/
const n=source.length
/*用类目宽除以系列数,得到一个类目中每个系列元素的宽,即列宽colW*/
const colW=xBandW/n
console.log('colW',colW);
/*计算调色盘颜色数量colorLen*/
const colorLen=color.length
/*===========7-架构绘图区===========*/
/*在svg中建立系列集合seriesObjs,在系列集合中建立系列对象
* 在svg中append 加入g 对象
* selectAll() 选择所有g元素,此处重点不在选择,而是建立一个选择集对象
* 用data() 方法将具备系列信息的数据源source绑定到系列集合中
* 用join() 基于数据源批量创建g元素,一个g代表一个系列,之后每个g元素里都会放入三个不同类目的柱状体
* 用transform 属性中的translate设置系列的x像素位——列宽乘以系列索引
* 基于系列索引,从调色盘中取色,然后将其作为一个系列中所有图形的填充色
* */
const seriesObjs=svg.append('g')
.selectAll('g')
.data(source)
.join('g')
.attr('transform',(seriesData,seriesInd)=>{
const seriesX=colW*seriesInd
console.log('seriesData:' + seriesData, 'seriesInd:' + seriesInd);
console.log(seriesInd + 'color:' + color[seriesInd%colorLen]);
return `translate(${seriesX},0)`
})
.attr('fill',(seriesData,seriesInd)=>color[seriesInd%colorLen])
/*在系列集合中建立柱状体集合rects
* 用系列集合seriesObjs 的selectAll()方法选择所有的rect元素,用于建立选择集对象
* 用data()方法将之前绑定在每个系列集合中的数据绑定到柱状体集合中
* 用join()基于每个系列的数据数据批量创建rect元素
* 用classed() 方法为其添加item属性
* */
const rects=seriesObjs.selectAll('rect')
.data(seriesData=>seriesData)
.join('rect')
.classed('item',true)
console.log('rects',rects);
/*=8-用attr()方法设置每个柱状体的x、y位置和width、height 尺寸=*/
/*
* 设置柱状体的x像素位
* 从回调参数中获取柱状体在当前系列中的索引rectInd,系列索引 seriesInd
* 基于柱状体在当前系列中的索引rectInd,用x轴比例尺xScale()获取柱状体在当前系列中的x像素位
* 设置柱状体像素宽width为列宽colW
* 设置柱状体的y像素位
* 从回调参数中解构柱状体数据rectData
* 基于柱状体数据rectData,用y轴比例尺yScale()获取柱状体的y像素位
* 设置柱状体的像素像素高
* 从回调参数中解构柱状体数据rectData
* 让y轴上刻度为0的像素位,减去刻度为柱状图实际数据的像素位,即为柱状图的像素高
* */
rects.attr('x',(rectData,rectInd)=>xScale(rectInd))
.attr('width',colW)
.attr('y',rectData=>yScale(rectData))
.attr('height',rectData=>yScale(0)-yScale(rectData))
</script>
</body>
</html>
鼠标事件
<script>
/*===========9-建立提示对象===========*/
/*用append()方法向容器对象main中添加div,作为提示对象tip
* 用attr() 方法为tip对象添加id
* */
const tip=main.append('div')
.attr('id','tip')
/*===========10-为柱状体添加鼠标事件===========*/
/*-----鼠标划入事件 mouseover-----*/
/*
* 从事件中的第一个回调参数解析目标对象和鼠标位置
* 鼠标位置 clientX,clientY
* 从事件中的第二个回调参数解析当前柱状体的数据
* 柱状体数据 rectData
* 柱状体名称 rectName
* 系列名 seriesName
* 基于鼠标位置和柱状体信息显示提示
* style()设置display 为block
* style()设置left、top位置
* html()设置元素的html 内容
* */
rects.on('mouseover',({clientX,clientY},{rectData,rectName,seriesName})=>{
tip.style('display','block')
.style('left',`${clientX}px`)
.style('top',`${clientY}px`)
.html(`
<div>${rectName}</div>
<div>${seriesName}:${rectData}</div>
`)
})
/*-----鼠标移动事件 mousemove-----*/
/*设置提示left、top位置*/
rects.on('mousemove',({clientX,clientY})=>{
tip.style('left',`${clientX}px`)
.style('top',`${clientY}px`)
})
/*-----鼠标划出事件 mouseout-----*/
/*隐藏提示*/
rects.on('mouseout',()=>{
tip.style('display','none')
})
</script>
transition 动画
<script>
/*=8-用attr()方法设置每个柱状体的x、y位置和width、height 尺寸=*/
/*
* 设置柱状体的x像素位
* 从回调参数中获取柱状体在当前系列中的索引rectInd,系列索引 seriesInd
* 基于柱状体在当前系列中的索引rectInd,用x轴比例尺xScale()获取柱状体在当前系列中的x像素位
* 设置柱状体像素宽width为列宽colW
* 设置柱状体的y像素位
* 从回调参数中解构柱状体数据rectData
* 基于柱状体数据rectData,用y轴比例尺yScale()获取柱状体的y像素位
* 设置柱状体的像素像素高
* 从回调参数中解构柱状体数据rectData
* 让y轴上刻度为0的像素位,减去刻度为柱状图实际数据的像素位,即为柱状图的像素高
* */
rects.attr('x',({rectData},rectInd)=>xScale(rectInd))
.attr('width',colW)
/*第一个关键帧-柱状体的初始状态
* y y轴中刻度0的像素位
* height 0
* */
rects.attr('y',()=>yScale(0))
.attr('height',0)
/*第二个关键帧-柱状体的完整状态
* transition() 建立补间动画
* duration() 动画时间
* delay 动画延迟
* ease 补间动画的插值算法,如d3.easeBounce,参考https://github.com/d3/d3-ease
* */
rects.transition()
.duration(1000)
.delay(({rectInd,seriesInd})=>{
console.log('rectInd:'+rectInd, 'seriesInd:'+seriesInd);
return (seriesInd+rectInd)*300
})
.ease(d3.easeBounce)
.attr('y',({rectData})=>yScale(rectData))
.attr('height',({rectData})=>yScale(0)-yScale(rectData))
</script>
鼠标缓动跟随
<script>
/*===========9-建立提示对象===========*/
/*用append()方法向容器对象main中添加div,作为提示对象tip
* 用attr() 方法为tip对象添加id
* */
const tip=main.append('div')
.attr('id','tip')
/*===========10-为柱状体添加鼠标事件===========*/
/*-----鼠标划入事件 mouseover-----*/
/*
* 从事件中的第一个回调参数解析目标对象和鼠标位置
* 鼠标位置 clientX,clientY
* 从事件中的第二个回调参数解析当前柱状体的数据
* 柱状体数据 rectData
* 柱状体名称 rectName
* 系列名 seriesName
* 基于鼠标位置和柱状体信息显示提示
* style()设置display 为block
* style()设置left、top位置
* html()设置元素的html 内容
* */
/*缓动跟随
* 更新终点位置endPos
* 开始缓动跟随
* */
rects.on('mouseover',({clientX,clientY},{seriesName,rectName,rectData})=>{
tip.style('display','block')
.html(`
<div>${seriesName}</div>
<div>${rectName}:${rectData}</div>
`)
easeTip.endPos={x:clientX,y:clientY}
easeTip.play=true
})
/*-----鼠标移动事件 mousemove-----*/
/*设置提示left、top位置*/
/*缓动跟随
* 更新终点位置endPos
* */
rects.on('mousemove',({clientX,clientY})=>{
easeTip.endPos={x:clientX,y:clientY}
})
/*-----鼠标划出事件 mouseout-----*/
/*隐藏提示*/
/*缓动跟随
* 删除动画帧
* */
rects.on('mouseout',()=>{
tip.style('display','none')
easeTip.play=false
})
/*===========12-提示的缓动跟随===========*/
/*EaseObj 缓动对象
* target 缓动目标
* fm 当前动画帧
* pos 绘图位置
* endPos 目标位置
* ratio 移动比例,如0.1
* _play 是否开始缓动跟随
* */
class EaseObj{
/*构造函数*/
constructor(target){
this.target=target
this.fm=0
this.pos={x:0,y:0}
this.endPos={x:0,y:0}
this.ratio=0.1
this._play=false
}
/*play 属性的取值器*/
get play(){
return play
}
/*play 属性的赋值器
* 现在的值不等于当过去值时
* 当现在的值为true时
* 缓动跟随
* 更新目标对象
* 连续渲染
* 当现在的值为false时
* 删除动画帧,取消连续渲染
* */
set play(val){
if(val!==this._play){
if(val){
this.render()
}else{
this.cancel()
}
this._play=val
}
}
/*render 渲染方法
* 按比值,让pos位置接近终点endPos
* 更新目标对象target的样式
* 连续渲染
* */
render(){
const {pos,endPos,ratio,target}=this
// 为什么不会超过鼠标位置呢?因为在运动过程中,pos.x 逐渐变大,到最后直接大于 endPos.x 时,也就无法再继续往上加,对于 canvas 视频中饼图弹性动画疑惑的解答
pos.x+=(endPos.x-pos.x)*ratio
pos.y+=(endPos.y-pos.y)*ratio
target.style('left',`${pos.x}px`)
.style('top',`${pos.y}px`)
this.fm=requestAnimationFrame(()=>{
this.render()
})
}
/*cancel 删除动画帧,取消连续渲染*/
cancel(){
cancelAnimationFrame(this.fm)
}
}
/*easeTip 实例化缓动对象*/
const easeTip=new EaseObj(tip)
</script>