【d3】利用d3绘制柱状图

2,192 阅读3分钟

效果预览

第一部分:柱状图的绘制

准备工作:建立画布

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))
}