效果预览
第一部分:柱状图的绘制
准备工作:建立画布
const [width, height] = [600,600];
const main = d3.select("#main");
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}`);
坐标轴的建立
scaleBand和scaleLinear
scaleBand:分段,不连续的数据
scaleLinear:线性,连续的数据
绘制x轴
1.x轴基础数据
/*category 类目数据*/
const category = ['html', 'css', 'js'];
2.x轴图表数据和像素数据
const len = category.length;
/*用range()方法,基于类目数量,获取x轴的在图表坐标系中的数据 xChartData,如[0,1,2]*/
const xChartData = d3.range(len);
/*x轴在像素坐标内的起始点和结束点 xPixelRange,左右各偏移50*/
const xPixelData = [50, width-50];
3.建立x轴比例尺
(domain相当于定义域,range相当于值域)
/*
* 用scaleBand()方法建立分段比例尺 xScale
* 用domain()方法在比例尺中写入图表数据xChartData
* 用rangeRound()方法在比例尺中写入像素数据,即像素的起始位和结束位xPixelRange
* */
const xScale = d3.scaleBand().domain(xChartData).rangeRound(xPixelData);
4.建立x轴对象
(axisLeft,axisRight,axisTop,axisBottom)
/*基于比例尺xScale,用axisBottom()方法创建刻度朝下的坐标轴生成器 xAxisGenerator*/
const xAxisGenerator=d3.axisBottom(xScale)
5.绘制x轴
/*利用坐标轴生成器绘制坐标轴
* 在svg中append 加入g 对象
* 用attr() 方法设置transform 属性中的translate位置
* 用call()方法调用xAxisGenerator轴生成器,生成坐标轴
* 用selectAll()方法选择所有的text文本
* 用text()方法将图表数据设置为类目数据
* 用style()方法设置字体大小
* */
svg.append('g')
.attr('transform',`translate(0 ${height-50})`)
.call(xAxisGenerator)
.selectAll('text')
.text((d,i)=>category[i])
.style('font-size','12px')//默认字体大小为10px
绘制y轴
与绘制x轴类似
1.y轴基础数据
const source=[
//html css js
[30,20,40],//学习人数
[40,30,50] //就业人数
]
2.y轴图表数据和像素数据
const maxY=Math.max(...source.flat())
/*声明y轴在图表坐标系中的数据起点和结束点 yChartRange*/
const yChartRange=[0,maxY]
/*声明y轴在像素坐标系中的数据起点和结束点 yPixelRange*/
const yPixelRange=[height-50,50]
3.建立y轴比例尺
(domain是定义域,range是值域)
//线性数据使用scaleLinear
const yScale=d3.scaleLinear()
.domain(yChartRange)
.range(yPixelRange)
4.建立y轴对象
const yAxisGenerator=d3.axisLeft(yScale)
5.绘制y轴
svg.append('g')
.attr('transform','translate(50,0)')
.call(yAxisGenerator)
.style('font-size','12px')
创建绘图区
1.建立绘图区相关的基础数据
//调色盘
const color=['pink','darkblue', '#c23531', '#2f4554']
/*用x轴比例尺xScale的bandwidth()方法获取x轴上一个类目的像素宽xBandW*/
const sBandW=xScale.bandwidth();
/*获取系列的数量n*/
const n=source.length;
/*用类目宽除以系列数,得到一个类目中每个系列元素的宽,即列宽colW*/
const colW=sBandW/n;
/*计算调色盘颜色数量colorLen*/
const colorLen=color.length
2.架构绘图区
const seriesObjs=svg.append('g')
.selectAll()
.data(source)
.join('g')
.attr('transform',(seriesData,seriesInd)=>{
const seriesX=seriesInd*colW
return `translate(${seriesX} 0)`
})
.attr('fill',(seriesData,seriesInd)=>{
return color[seriesInd % colorLen]
})
3.绘制柱状体
/*在系列集合中建立柱状体集合rects
* 用系列集合seriesObjs 的selectAll()方法选择所有的rect元素,用于建立选择集对象
* 用data()方法将之前绑定在每个系列集合中的数据绑定到柱状体集合中
* 用join()基于每个系列的数据数据批量创建rect元素
* 用classed() 方法为其添加item属性
* */
const rects=seriesObjs.selectAll('g')
.data(seriesData=>seriesData)
.join('rect')
.classed('item',true)
rects.attr('x',(rectData,rectInd)=>xScale(rectInd))
.attr('width',colW)
.attr('y',(rectData,rectInd)=>yScale(rectData))
.attr('height',(rectData,rectInd)=>yScale(0)-yScale(rectData))
设置间距
使用padding
const xScale = d3.scaleBand().domain(xChartData).rangeRound(xPixelData).padding(0.1);
第二部分:提示信息
优化数据
const dimensions = ["学习人数", "就业人数"];
const source2 = source.map((seriesData,seriesInd)=>{
const seriesName=dimensions[seriesInd]
return seriesData.map((rectData,rectInd)=>{
const rectName=category[rectInd]
return {rectData,rectInd,rectName,seriesInd,seriesName}
})
})
并且更改柱状图数据获取
rects.attr('x',({rectData,rectInd})=>xScale(rectInd))
.attr('width',colW)
.attr('y',({rectData,rectInd})=>yScale(rectData))
.attr('height',({rectData,rectInd})=>yScale(0)-yScale(rectData))
创建鼠标事件
(style)
<style type="text/css">
#main{
width: 600px;
height: 600px;
}
#tip {
width: 100px;
background-color: rgba(0, 0, 0, 0.3);
position: absolute;
color: white;
text-align: center;
margin: 5px;
box-sizing: border-box;
}
</style>
首先创建提示对象
//建立提示对象
const tip = main.append('div')
.attr('id', 'tip')
鼠标事件:
//鼠标悬浮事件
rects.on('mouseover',({clientX,clientY},{rectData,seriesName,rectName})=>{
tip.style('display','block')
.style('left',clientX+'px')
.style('top',clientY+'px')
.html(`
<div>${rectName}</div>
<div>${seriesName}:${rectData}</div>
`)
})
rects.on('mousemove',({clientX,clientY})=>{
tip.style('left',clientX+'px')
.style('top',clientY+'px')
})
/*隐藏提示*/
rects.on('mouseout',({clientX,clientY})=>{
tip.style('display','none')
})
第三部分:动画
transition动画
替换下面代码
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})=>(rectInd+seriesInd)*500)
.attr('y',({rectData})=>yScale(rectData))
.attr('height',({rectData})=>yScale(0)-yScale(rectData))
提示的缓动跟随
/*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.05
this._play=false
}
/*play 属性的取值器*/
get play(){
return this._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,target,ratio}=this
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.bind(this))
}
/*cancel 删除动画帧,取消连续渲染*/
cancel(){
cancelAnimationFrame(this.fm)
}
}
/*easeTip 实例化缓动对象*/
const easeTip=new EaseObj(tip)
更改鼠标事件
//添加鼠标事件
rects.on('mouseover', ({
clientX,
clientY
}, {rectName,seriesName,rectData}) => {
tip.style('display', 'block')
// .style('left', clientX + 'px')
// .style('top', clientY + 'px')
.html(`
<div>${rectName}的${seriesName}</div>
<div>人数:${rectData}</div>
`)
easeTip.endPos={x:clientX,y:clientY}
easeTip.play=true
})
rects.on('mousemove', ({
clientX,
clientY
}) => {
// tip.style('left', clientX + 'px')
// .style('top', clientY + 'px')
easeTip.endPos={x:clientX,y:clientY}
})
rects.on('mouseout', () => {
// tip.style('display', 'none')
easeTip.play = false;
})
一点点优化:上面所有步骤走完可以发现还存在一个小小的问题,鼠标第一次移动到柱状体上,提示是从(0,0)位置触发移动到鼠标位置,改为第一次直接触发不跟随。
EaseObj的constructor中添加:
this.first = true;
render函数
render(){
const {pos,endPos,target,ratio}=this
if(this.first){
target.style('left', endPos.x + 'px')
.style('top', endPos.y + 'px');
this.pos = endPos;
this.first = false;
return;
}
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.bind(this))
}