Html结构
<!DOCTYPE html>
<html lang="zh_CN">
<head>
<meta charset="UTF-8" />
<title>柱状图</title>
<style>
html {
height: 100%;
}
body {
height: 100%;
margin: 0;
}
#main {
width: 100%;
height: 100%;
background-color: antiquewhite;
}
#tip {
position: absolute;
margin-left: 10px;
margin-top: 30px;
line-height: 22px;
background-color: rgba(0, 0, 0, 0.6);
padding: 4px 9px;
font-size: 13px;
color: #fff;
border-radius: 3px;
pointer-events: none;
display: none;
}
</style>
</head>
<body>
<div id="main"></div>
<script src="https://d3js.org/d3.v6.min.js"></script>
<script>
// ...
</script>
</body>
</html>
1. 必备数据
// X轴数据源
const categories=['html','css','js'];
// Y轴数据源
const source = [
[30, 20, 40], //学习人数
[40, 30, 50], //就业人数
];
/*dimensions 维度信息*/
const dimensions=['学习人数','就业人数'];
// 调色盘
const color=['#c23531','#2f4554', '#61a0a8', '#d48265', '#91c7ae','#749f83', '#ca8622', '#bda29a','#6e7074', '#546570', '#c4ccd3'];
const width=600
const height=600
/*优化数据源
* 使用map方法遍历数据源
* 将其中的数据变成对象:
* {
* rectData:柱状体数据,
* rectInd:柱状体索引,
* rectName:柱状体名称,
* seriesInd:系列索引,
* seriesName:系列名称
* }
**/
const source2=source.map((seriesData,seriesInd)=>{
const seriesName=dimensions[seriesInd]
return seriesData.map((rectData,rectInd)=>{
const rectName=categories[rectInd]
return {rectData,rectInd,rectName,seriesInd,seriesName}
})
})
2. 创建svg
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}`)
3. 绘制X轴
-
创建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] -
创建X轴比例尺
/* * 用scaleBand()方法建立分段比例尺 xScale * 用domain()方法在比例尺中写入图表数据xChartData * 用rangeRound()方法在比例尺中写入像素数据,即像素的起始位和结束位xPixelRange * */ const xScale=d3.scaleBand() .domain(xChartData) .rangeRound(xPixelRange) -
创建X轴对象
/*基于比例尺xScale,用axisBottom()方法创建刻度朝下的坐标轴生成器 xAxisGenerator*/ const xAxisGenerator=d3.axisBottom(xScale) -
绘制X轴
/** 利用坐标轴生成器绘制坐标轴 * 在svg中append 加入g 对象 * 用transform 属性中的translate设置X轴的y位置 * 用call()方法调用xAxisGenerator轴生成器,生成坐标轴 * 用selectAll()方法选择所有的text文本 * 用text()方法将图表数据设置为类目数据 * 用attr()方法设置字体大小 * */ svg.append('g') .attr('transform',`translate(0,${height-50})`) .call(xAxisGenerator) .selectAll('text') .text(d=>categories[d]) .style('font-size','12px')
4. 绘制Y轴
-
创建Y轴数据
/** 计算数据源中所有数据的极值 maxY * 用js原生方法flat()展开数据源,再通过max()方法取极值 * */ const maxY=Math.max(...source.flat()) /*声明y轴在图表坐标系中的数据起点和结束点 yChartRange*/ const yChartRange=[0,maxY] /*声明y轴在像素坐标系中的数据起点和结束点 yPixelRange*/ const yPixelRange=[height-50,50] -
创建Y轴比例尺
/** * 用scaleLinear()方法建立线性比例尺 yScale * 用domain()方法在比例尺中写入图表数据yChartRange * range()方法在比例尺中写入像素数据,即像素的起始位和结束位yPixelRange * */ const yScale=d3.scaleLinear() .domain(yChartRange) .range(yPixelRange) -
创建Y轴对象
/*基于比例尺yScale,用axisLeft()方法创建刻度朝左的坐标轴生成器 yAxisGenerator*/ const yAxisGenerator=d3.axisLeft(yScale) -
绘制Y轴
/** 利用坐标轴生成器生成坐标轴 * 在svg中append 加入g 对象 * 用transform 属性中的translate设置y轴的x位置 * 用call()方法调用xAxisGenerator轴生成器,生成坐标轴 * 用style()方法设置字体大小 * */ svg.append('g') .attr('transform','translate(50 0)') .call(yAxisGenerator) .style('font-size','12px')
5. 绘图区(柱状图绘制)
-
建立基础数据
/*用x轴比例尺xScale的bandwidth()方法获取x轴上一个类目的像素宽xBandW*/ const xBandW=xScale.bandwidth() /*获取系列的数量n*/ const n=source.length /*用类目宽除以系列数,得到一个类目中每个系列元素的宽,即列宽colW*/ const colW=xBandW/n /*计算调色盘颜色数量colorLen*/ const colorLen=color.length -
构建绘图区
/* 在svg中建立系列集合seriesObjs,在系列集合中建立系列对象 * 在svg中append 加入g 对象 * selectAll() 选择所有g元素,此处重点不在选择,而是建立一个选择集对象 * 用data() 方法将具备系列信息的数据源source绑定到系列集合中 * 用join() 基于数据源批量创建g元素,一个g代表一个系列,之后每个g元素里都会放入三个不同类目的柱状体 * 用transform 属性中的translate设置系列的x像素位——列宽乘以系列索引 * 基于系列索引,从调色盘中取色,然后将其作为一个系列中所有图形的填充色 **/ const seriesObjs=svg.append('g') .selectAll('g') .data(source2)// 使用优化后的数据源 .join('g') .attr('transform',(seriesData,seriesInd)=>{ const seriesX=colW*seriesInd 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) /* 设置柱状体的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)); -
创建动画
/* 第一个关键帧-柱状体的初始状态 * 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})=>(seriesInd+rectInd)*300) .ease(d3.easeBounce) .attr('y',({rectData})=>yScale(rectData)) .attr('height',({rectData})=>yScale(0)-yScale(rectData))
6. 鼠标事件(鼠标经过提示信息)
-
创建提示对象
const tip = main.append("div").attr("id", "tip"); -
为柱状图加鼠标事件
/* 鼠标划入事件 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"); });
7. 缓动跟随
-
创建类
/* 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-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) -
修改鼠标事件
/* 鼠标划入事件 mouseover * 从事件中的第一个回调参数解析目标对象和鼠标位置 * 鼠标位置 clientX,clientY * 从事件中的第二个回调参数解析当前柱状体的数据 * 柱状体数据 rectData * 柱状体名称 rectName * 系列名 seriesName * 基于鼠标位置和柱状体信息显示提示 * style()设置display 为block * html()设置元素的html 内容 * 设置缓动跟随 **/ 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位置 **/ rects.on("mousemove", ({ clientX, clientY }) => { easeTip.endPos={x:clientX,y:clientY} }); /* 鼠标划出事件 mouseout * 隐藏提示 **/ rects.on("mouseout", () => { tip.style("display", "none"); easeTip.play=false });