阅读 343

【d3js】手把手教你绘制一个折线图

前言

咱们这期就绘制一个折线图,那么咱们分析下绘制一个折线图需要什么?

  1. 坐标轴(一个竖着和一个横着的)
  2. 来点网格线美观一下
  3. 折线图少不了曲线平滑的
  4. 如果再来个绘制连线的轨迹就更好了

好上面就是咱们要一步一步把折线图绘制出来的步骤啦!!!!

先来看看效果呈现吧:

zoo16m.gif

开始

开始咱们上面列出来的任务

第一步(绘制坐标轴)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
</html>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
    const height = 500, width = 500, margin = 25;
    //定义咱们的svg画布空间容器
    let svg = d3.select('body')
             .append('svg')
             .attr('width',width)
             .attr('height',height)


    //绘制一个横着的坐标轴
    function drawXAxis() {
         //创建线性比例尺,使用坐标轴必备
         const xScale = d3.scaleLinear().domain([0,10]).range([0, width - margin * 2]);

        //创建底部的x的坐标轴
        const xAxis = d3.axisBottom(xScale);

        //使坐标轴插入svg中
        svg.append('g').attr('class','x-axis').attr('transform',function(){
            //让平移到底部x对的位置,咱们还要绘制y轴呢
            return `translate(${margin}, ${ height - margin })`
        }).call(xAxis);
    }



     //绘制一个竖着的坐标轴
     function drawYAxis() {
         //创建线性比例尺,使用坐标轴必备
         const yScale = d3.scaleLinear().domain([10, 0]).range([0, width - margin * 2]);

        //创建底部的x的坐标轴
        const yAxis = d3.axisLeft(yScale);

        //使坐标轴插入svg中
        svg.append('g').attr('class','y-axis').attr('transform',function(){
            //让平移到底部x对的位置,咱们还要绘制y轴呢
            return `translate(${margin}, ${ margin })`
        }).call(yAxis);
    }

    drawXAxis();
    drawYAxis();

</script>

复制代码

效果呈现:

1630475308(1).png

代码分析;其实就是俩个坐标轴,然后给平移到交错的位置也就是咱们的x轴,y轴。是不是模型出来了!!!! 咱们接着来

第二步(绘制网格线)

上一步dom呈现图:

09fbb0a8ddc2e527da8c2a7c9cc4c7e.png

大家线观察下咱们的坐标轴的dom结构,都是一个g容器包裹然后里面就是咱们坐标轴的小线和刻度尺的文字。

那咱们是不是把连线也放里面,就不用关心translate的问题了

代码部分:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
</html>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
    const height = 500, width = 500, margin = 25;
    //定义咱们的svg画布空间容器
    let svg = d3.select('body')
             .append('svg')
             .attr('width',width)
             .attr('height',height)


    //绘制一个横着的坐标轴
    function drawXAxis() {
         //创建线性比例尺,使用坐标轴必备
         const xScale = d3.scaleLinear().domain([0,10]).range([0, width - margin * 2]);

        //创建底部的x的坐标轴
        const xAxis = d3.axisBottom(xScale);

        //使坐标轴插入svg中
        svg.append('g').attr('class','x-axis').attr('transform',function(){
            //让平移到底部x对的位置,咱们还要绘制y轴呢
            return `translate(${margin}, ${ height - margin })`
        }).call(xAxis);
    }



     //绘制一个竖着的坐标轴
     function drawYAxis() {
         //创建线性比例尺,使用坐标轴必备
         const yScale = d3.scaleLinear().domain([10,0]).range([0, width - margin * 2]);

        //创建底部的x的坐标轴
        const yAxis = d3.axisLeft(yScale);

        //使坐标轴插入svg中
        svg.append('g').attr('class','y-axis').attr('transform',function(){
            //让平移到底部x对的位置,咱们还要绘制y轴呢
            return `translate(${margin}, ${ margin })`
        }).call(yAxis);
    }

    function drawGrid() {
        //绘制y轴的线
        d3.selectAll('.y-axis .tick')
           .append('line')
           .attr('x1',0)
           .attr('y1',0)
           //大家不必疑惑这个height - margin * 2 他其实就是咱们的长度啊
           .attr('x2',(height - margin * 2))
           .attr('y2',0)
           .attr('stroke','#e4e4e4')

        //绘制x轴的线
        d3.selectAll('.x-axis .tick')
           .append('line')
           .attr('x1',0)
           .attr('y1',0)
           .attr('x2',0)
           .attr('y2',(- height  + margin * 2))
           .attr('stroke','#e4e4e4')
    }


    (async function draw() {
      await drawXAxis();
      await drawYAxis();
      await drawGrid();
    })();
    

</script>
复制代码

效果呈现:

10e528999a5afa8543b6eb7c28e10ba.png

总结:两条直线绘制成网格, 代码改动部分新增drawGrid方法

第三步(绘制path线)

绘制折线图的的连线,其实就是一天path路径,有人该疑问这个path算位置是不是好麻烦的啊?放心d3js有相关api(d3.line()),帮你把一组[{x: 1, y: 1}, { x: 2, y:2 }]给你转化成这个样的path路径M1,1 L2,2

代码绘制:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
</html>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
    const height = 500, width = 500, margin = 25;
    //定义咱们的svg画布空间容器
    let svg = d3.select('body')
             .append('svg')
             .attr('width',width)
             .attr('height',height);

     //创建线性比例尺,使用坐标轴必备
    const yScale = d3.scaleLinear().domain([10, 0]).range([0, width - margin * 2]);
    const xScale = d3.scaleLinear().domain([0,10]).range([0, width - margin * 2]);

    //绘制一个横着的坐标轴
    function drawXAxis() {

        //创建底部的x的坐标轴
        const xAxis = d3.axisBottom(xScale);

        //使坐标轴插入svg中
        svg.append('g').attr('class','x-axis').attr('transform',function(){
            //让平移到底部x对的位置,咱们还要绘制y轴呢
            return `translate(${margin}, ${ height - margin })`
        }).call(xAxis);
    }



     //绘制一个竖着的坐标轴
     function drawYAxis() {
        //创建底部的x的坐标轴
        const yAxis = d3.axisLeft(yScale);

        //使坐标轴插入svg中
        svg.append('g').attr('class','y-axis').attr('transform',function(){
            //让平移到底部x对的位置,咱们还要绘制y轴呢
            return `translate(${margin}, ${ margin })`
        }).call(yAxis);
    }

    function drawGrid() {
        //绘制y轴的线
        d3.selectAll('.y-axis .tick')
           .append('line')
           .attr('x1',0)
           .attr('y1',0)
           //大家不必疑惑这个height - margin * 2 他其实就是咱们的长度啊
           .attr('x2',(height - margin * 2))
           .attr('y2',0)
           .attr('stroke','#e4e4e4')

        //绘制x轴的线
        d3.selectAll('.x-axis .tick')
           .append('line')
           .attr('x1',0)
           .attr('y1',0)
           .attr('x2',0)
           .attr('y2',(- height  + margin * 2))
           .attr('stroke','#e4e4e4')
    }


    //数据定义, 两条线
    const data = [
        [
            {x:0,y:6},
            {x:1,y:5},
            {x:2,y:3},
            {x:3,y:5},
            {x:4,y:5},
            {x:6,y:4},
            {x:7,y:3},
            {x:8,y:3},
            {x:9,y:2},
            {x:10,y:10},
        ],
        d3.range(10).map(function(i){
            return {x:i,y:Math.min(i)}
        })
    ]

    function drawLine() {
        //d3.line是把数组的坐标生成一个path路径
        let line = d3.line()
                .x(function(d){
                    //这个d就是咱们的data[0] 遍历的数据了 return也就是坐标 相当于帮咱们生成了一个 M0,0 L 1,2.....这个样
                  return xScale(d.x)
                })
                .y(function(d){
                  return yScale(d.y)
                })
      //添加path
      svg.selectAll('path.path')
       .data(data)
       .enter()
       .append('path')
       .attr('class','path')
       .attr('d',function(d){
          return line(d)
        })
       .attr('stroke', '#2e6be6')
       .attr('fill', 'none')
       .attr('transform',`translate(${margin}, ${margin})`)
    }


    (async function draw() {
      await drawXAxis();
      await drawYAxis();
      await drawGrid();
      await drawLine();
    })();
    

</script>
复制代码

效果呈现:

f59862e5a0dbd4c8720388093e054fe.png 总结:利用d3.line()帮咱们生成path的路径,新增函数drawLine() 是不是感觉少了点什么?对给人感觉节点不太明显,如果要是曲线平滑的话就更好了?

折现图呈现效果优化篇

节点不太明显? 曲线平滑?

分析:
节点不明显?咱们对比chart图表发现他们在每个点位置有个圆
曲线平滑? 咱们d3js中有对应的api绘制曲线(.curve(d3.curveCardinal)

代码呈现:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
</html>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
    const height = 500, width = 500, margin = 25;
    //定义咱们的svg画布空间容器
    let svg = d3.select('body')
             .append('svg')
             .attr('width',width)
             .attr('height',height);

     //创建线性比例尺,使用坐标轴必备
    const yScale = d3.scaleLinear().domain([10, 0]).range([0, width - margin * 2]);

    const xScale = d3.scaleLinear().domain([0,10]).range([0, width - margin * 2]);

    //绘制一个横着的坐标轴
    function drawXAxis() {

        //创建底部的x的坐标轴
        const xAxis = d3.axisBottom(xScale);

        //使坐标轴插入svg中
        svg.append('g').attr('class','x-axis').attr('transform',function(){
            //让平移到底部x对的位置,咱们还要绘制y轴呢
            return `translate(${margin}, ${ height - margin })`
        }).call(xAxis);
    }



     //绘制一个竖着的坐标轴
     function drawYAxis() {
        //创建底部的x的坐标轴
        const yAxis = d3.axisLeft(yScale);

        //使坐标轴插入svg中
        svg.append('g').attr('class','y-axis').attr('transform',function(){
            //让平移到底部x对的位置,咱们还要绘制y轴呢
            return `translate(${margin}, ${ margin })`
        }).call(yAxis);
    }

    function drawGrid() {
        //绘制y轴的线
        d3.selectAll('.y-axis .tick')
           .append('line')
           .attr('x1',0)
           .attr('y1',0)
           //大家不必疑惑这个height - margin * 2 他其实就是咱们的长度啊
           .attr('x2',(height - margin * 2))
           .attr('y2',0)
           .attr('stroke','#e4e4e4')

        //绘制x轴的线
        d3.selectAll('.x-axis .tick')
           .append('line')
           .attr('x1',0)
           .attr('y1',0)
           .attr('x2',0)
           .attr('y2',(- height  + margin * 2))
           .attr('stroke','#e4e4e4')
    }


    //数据定义, 两条线
    const data = [
        [
            {x:0,y:6},
            {x:1,y:5},
            {x:2,y:3},
            {x:3,y:5},
            {x:4,y:5},
            {x:6,y:4},
            {x:7,y:3},
            {x:8,y:3},
            {x:9,y:2},
            {x:10,y:10},
        ],
        d3.range(10).map(function(i){
            return {x:i,y:Math.min(i)}
        })
    ]

    function drawLine() {
        //d3.line是把数组的坐标生成一个path路径
        let line = d3.line()
                .x(function(d){
                    //这个d就是咱们的data[0] 遍历的数据了 return也就是坐标 相当于帮咱们生成了一个 M0,0 L 1,2.....这个样
                  return xScale(d.x)
                })
                .y(function(d){
                  return yScale(d.y)
                })
                .curve(d3.curveCardinal)  //曲线效果
          
      svg.selectAll('path.path')
       .data(data)
       .enter()
       .append('path')
       .attr('class','path')
       .attr('d',function(d){
          return line(d)
        })
       .attr('stroke', '#2e6be6')
       .attr('fill', 'none')
       .attr('transform',`translate(${margin}, ${margin})`)
    }


    function drawCircle() {
        data.forEach(item => {
           svg.append('g')
              .selectAll('.circle')
              .data(item)
              .attr('class','circle')
              .enter()
              .append('circle')
              .attr('cx',function(d){return xScale(d.x)})
              .attr('cy', function(d){return yScale(d.y)})
              .attr('r',4)
              .attr('transform', `translate(${margin}, ${margin})`)
              .attr('fill','#fff')
              .attr('stroke','rgba(56, 8, 228, .5)')
       });
    }


    (async function draw() {
      await drawXAxis();
      await drawYAxis();
      await drawGrid();
      await drawLine();
      await drawCircle();
    })();
    

</script>
复制代码

效果呈现(是不是好看多了):

b2ba728408819189769bd9d33dc0cb7.png

总结: 新增drawCircle()绘制圆点, 使用d3.line().curve()绘制曲线

第四步(让连线动起来)

怎么让连线动画动起来呢?

连线path动画的话使用svg的stroke-dashoffsetstroke-dasharray
线上的圆点的话就每个点delay分批指定就好了

stroke-dasharray: 用于绘制虚线
stroke-dasharray:虚线的偏移量

那么问题来了?用虚线能绘制实线吗?当然可以 就是利用这个虚线偏移做的动画。
在《张鑫旭》大佬的博客里面发现了一段通俗易懂的解释:
用中文解释就是,一根火腿肠12厘米,要在上面画虚线,虚线间隔有15厘米,如果没有dashoffset,则火腿肠前面15厘米会被辣椒酱覆盖!实际上只有12厘米,因此,我们看到的是整个火腿肠都有辣椒酱。现在,dashoffset也是15厘米,也就是虚线要往后偏移15厘米,结果,辣椒酱要抹在火腿肠之外,也就是火腿肠上什么辣椒酱也没有。如果换成上面的直线SVG,也就是直线看不见了。我们把dashoffset值逐渐变小,则会发现,火腿肠上的辣椒酱一点一点出现了,好像辣椒酱从火腿肠根部涂抹上去一样。

呈现效果:

代码实现:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
</html>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
    const height = 500, width = 500, margin = 25;
    //定义咱们的svg画布空间容器
    let svg = d3.select('body')
             .append('svg')
             .attr('width',width)
             .attr('height',height);

     //创建线性比例尺,使用坐标轴必备
    const yScale = d3.scaleLinear().domain([10, 0]).range([0, width - margin * 2]);

    const xScale = d3.scaleLinear().domain([0,10]).range([0, width - margin * 2]);

    //绘制一个横着的坐标轴
    function drawXAxis() {

        //创建底部的x的坐标轴
        const xAxis = d3.axisBottom(xScale);

        //使坐标轴插入svg中
        svg.append('g').attr('class','x-axis').attr('transform',function(){
            //让平移到底部x对的位置,咱们还要绘制y轴呢
            return `translate(${margin}, ${ height - margin })`
        }).call(xAxis);
    }



     //绘制一个竖着的坐标轴
     function drawYAxis() {
        //创建底部的x的坐标轴
        const yAxis = d3.axisLeft(yScale);

        //使坐标轴插入svg中
        svg.append('g').attr('class','y-axis').attr('transform',function(){
            //让平移到底部x对的位置,咱们还要绘制y轴呢
            return `translate(${margin}, ${ margin })`
        }).call(yAxis);
    }

    function drawGrid() {
        //绘制y轴的线
        d3.selectAll('.y-axis .tick')
           .append('line')
           .attr('x1',0)
           .attr('y1',0)
           //大家不必疑惑这个height - margin * 2 他其实就是咱们的长度啊
           .attr('x2',(height - margin * 2))
           .attr('y2',0)
           .attr('stroke','#e4e4e4')

        //绘制x轴的线
        d3.selectAll('.x-axis .tick')
           .append('line')
           .attr('x1',0)
           .attr('y1',0)
           .attr('x2',0)
           .attr('y2',(- height  + margin * 2))
           .attr('stroke','#e4e4e4')
    }


    //数据定义, 两条线
    const data = [
        [
            {x:0,y:6},
            {x:1,y:5},
            {x:2,y:3},
            {x:3,y:5},
            {x:4,y:5},
            {x:6,y:4},
            {x:7,y:3},
            {x:8,y:3},
            {x:9,y:2},
            {x:10,y:10},
        ],
        d3.range(10).map(function(i){
            return {x:i,y:Math.min(i)}
        })
    ]

    function drawLine() {
        //d3.line是把数组的坐标生成一个path路径
        let line = d3.line()
                .x(function(d){
                    //这个d就是咱们的data[0] 遍历的数据了 return也就是坐标 相当于帮咱们生成了一个 M0,0 L 1,2.....这个样
                  return xScale(d.x)
                })
                .y(function(d){
                  return yScale(d.y)
                })
                .curve(d3.curveCardinal)  //曲线效果
          
      svg.selectAll('path.path')
       .data(data)
       .enter()
       .append('path')
       .attr('class','path')
       .attr('d',function(d){
          return line(d)
        })
       .attr('stroke', '#2e6be6')
       .attr('fill', 'none')
       .attr('transform',`translate(${margin}, ${margin})`)
    }


    function drawCircle() {
        data.forEach(item => {
           svg.append('g')
              .selectAll('.circle')
              .data(item)
              .enter()
              .append('circle')
              .attr('class','circle')
              .attr('cx',function(d){return xScale(d.x)})
              .attr('cy', function(d){return yScale(d.y)})
              .attr('r',4)
              .attr('transform', `translate(${margin}, ${margin})`)
              .attr('fill','#fff')
              .attr('stroke','rgba(56, 8, 228, .5)')
              .style('stroke-width',0);
       });
    }


    function drawAnimations() {
        //连线动画
        svg.selectAll('path.path')
        .attr('stroke', '#2e6be6')
        .attr('transform','translate(25,25)')
        .style('stroke-dasharray',function(){
            return d3.select(this).node().getTotalLength()
        })
        .style('stroke-dashoffset',function(){
            return d3.select(this).node().getTotalLength()
        })
        .transition()
        .duration(2000)
        .delay(200)
        .ease(d3.easeLinear)
        
        .style('stroke-dashoffset',0);

       //圆点
       svg.selectAll('.circle')
            .style('stroke-width',0)
            .transition()
            .duration(1000)
            .delay(function(d,i){
                return i * 100
            })
            .ease(d3.easeLinear)
            .style('stroke-width',1)
    } 


    (async function draw() {
      await drawXAxis();
      await drawYAxis();
      await drawGrid();
      await drawLine();
      await drawCircle();
      await drawAnimations();
    })();
    

</script>
复制代码

效果图:

zoo16m.gif

总结

基础折线图是不是已经画出来了?嘿嘿当然还有好多功能没有实现呢tooltiplegend..... 等我在完善完善,搞定了发出来。

结束语

  • 大家好 我是三原,多谢您的观看,我会更加努力(๑•̀ㅂ•́)و✧多多总结。
  • 每个方法都是敲出来验证过通过的,有需要可以放心复制。
  • 如果您给帮点个赞👍就更好了 谢谢您~~~~~
  • 期待您的关注
文章分类
前端
文章标签