D3 v4.x 的echarts化(2-5)—— 折线图

折线图参考echarts示例:http://www.echartsjs.com/examples/editor.html?c=line-smooth

折线图github地址:https://github.com/dkr380205984/myComponent/blob/master/src/page/d3/line.vue

d3制作折线图的难点在于如何实现折线图的动画效果

由于折线是由path标签绘制生成的,路径的信息都记录在同一个标签中,因此你想要绘制路径的过程变成动画显然需要借助svg本身的API,未找到相关资料。

所以需要剑走偏锋,换一个角度去实现。

在这里我提供两种实现折线图动画的设计思路。

第一种:利用SVG中stroke-dasharray及stroke-dashoffset属性(这两个属性介绍的传送门

stroke-dasharray属性用来设置描边的点划线的图案范式。就是设置实线和虚线的宽度

stroke-dashoffset则指定了dash模式到路径开始的距离,就是实线虚线绘制的起点距路径开始的距离

我们可以利用这两个属性完成画线的效果。思路大概如下

把实线长度用stroke-dasharray设为折线段的长度,同时通过stroke-dashoffset设置偏移量为相等的值,这个时候这个折线段就完全偏移到你看不见的地方去了,然后通过动画属性将stroke-dashoffset的值从长度过渡到0,就能实现画线的效果

事实上在上述步骤中,我们会用到折线段的长度,目前没有找到API提供方法获取折线段的长度,因此我们只能通过先绘制一条折线(不显示),然后用dom的方式去获取长度,这种做法十分愚蠢,如果有好的解决方案可以在下面留言。

 // 透明折线绘制完成后,我们可以获取到长度了
    let path = document.getElementsByClassName('line')
    let length = path[0].getTotalLength()

第一种方案用起来比较复杂,下面提供 一种简单的思路,这种方法在区域图中也用到了,而且看起来效果还不错。

设计思路:我们用一个rect(白纸)把除了坐标轴的信息都遮挡住,然后再让这张白纸像幕布一样慢慢的拉开,就完成上述效果了。具体效果可以看下一章的区域图,有兴趣的童鞋可以自己试一下。下面是折线图的源码(带详细注释)

<template>
  <div id = "line"></div>
</template>

<script>
import * as d3 from 'd3'
export default {
  data: function () {
    return {
      data: [{
        name: '小米',
        value: 10.7
      }, {
        name: '华为',
        value: 20.8
      }, {
        name: '联想',
        value: 36.4
      }, {
        name: '三星',
        value: 40.8
      }, {
        name: '苹果',
        value: 90.8
      }, {
        name: '其他',
        value: 100.8
      } ],
      width: '',
      heigth: '',
      padding: {
        left: '30px',
        right: '30px',
        top: '5px',
        bottom: '20px'
      }
    }
  },
  methods: {
    getStyle: function (obj, attr) {
      if (obj.currentStyle) {
        return obj.currentStyle[attr]
      } else {
        return document.defaultView.getComputedStyle(obj, null)[attr]
      }
    }
  },
  mounted () {
    let _this = this
    let dom = document.getElementById('line')
    // dom容器宽高,参数padding获取
    let width = parseFloat(this.width) || parseFloat(this.getStyle(dom, 'width'))
    let height = parseFloat(this.height) || parseFloat(this.getStyle(dom, 'height'))
    let padLeft = parseFloat(this.padding.left)
    let padRight = parseFloat(this.padding.right)
    let padTop = parseFloat(this.padding.top)
    let padBottom = parseFloat(this.padding.bottom)
    let minHeight = parseFloat(this.minHeight) || 0
    if (isNaN(width) || isNaN(height)) {
      console.error('width 或 height 参数错误')
      return
    }
    // 检查padding参数是否有问题
    if (isNaN(padLeft) || isNaN(padRight) || isNaN(padTop) || isNaN(padBottom)) {
      console.error('padding 参数错误')
      return
    }
    // 开始绘图,创建svg画布
    let svg = d3.select('#line')
      .append('svg')
      .attr('width', width)
      .attr('height', height)
    // 创建x比例尺
    let xData = _this.data.map((d) => d.name)
    let xScale = d3.scaleBand().domain(xData).range([0, width - padLeft - padRight])
    let xAxis = d3.axisBottom().scale(xScale)
    // 绘制x轴
    svg.append('g')
      .attr('transform', 'translate(' + padLeft + ',' + (height - padBottom) + ')')
      .call(xAxis)
      .style('font-size', '12px')
    // 创建y比例尺
    let yData = _this.data.map((d) => d.value)
    let max = d3.max(yData)
    let yScale = d3.scaleLinear().domain([0, max]).range([height - padTop - padBottom, minHeight]).nice()
    max = yScale.domain()[1]
    let yAxis = d3.axisLeft().scale(yScale)
    // 绘制y轴
    svg.append('g')
      .attr('transform', 'translate(' + padLeft + ',' + padTop + ')')
      .call(yAxis)
      .style('font-size', '12px')
    // 让我们来做一个动画吧~~~~~~
    // 设置折线路径
    let line = d3.line()
      .x(function (d, i) {
        // 这里用xScale会有问题
        return padLeft + (width - padLeft - padRight) / _this.data.length * (i + 0.5)
      })
      .y(function (d, i) {
        return yScale(d.value)
      })
      // 这里有很多平滑曲线的种类可以选择,如最常用的d3.curveBasis,我找到一种可以把点绘制在线上的,当然还有很多其他方法
      .curve(d3.curveCatmullRom)
    // 绘制一条透明的折现,这条折线用于后续折现的长度获取,获取长度后就可以做动画了
    svg.append('path')
      .attr('stroke', 'rgba(0,0,0,0)')
      .attr('stroke-width', '4px')
      .attr('fill', 'none')
      .attr('class', 'line')
      .attr('d', line(_this.data))
      .attr('class', function (d) {
        return 'line'
      })
    // 透明折线绘制完成后,我们可以获取到长度了
    let path = document.getElementsByClassName('line')
    let length = path[0].getTotalLength()
    // 获取到长度后,我们再绘制一条肉眼看得到的线
    svg.append('path')
      .attr('stroke', '#19CAAD') // 我们只需要设置线条颜色就够了
      .attr('stroke-width', '4px')
      .attr('fill', 'none') // 要把填充设置为none
      .attr('class', 'line')
      .style('stroke-dasharray', length)
      .style('stroke-dashoffset', length)
      .style('animation', 'dash 2s forwards')
      .attr('d', line(_this.data))
      .attr('class', function (d) {
        return 'line'
      })
    // 我们还需要把dash添加到style中去
    let rule = '@keyframes dash {0%{stroke-dashoffset:' + length + ';}100%{ stroke-dashoffset: 0;} }'
    let style = document.createElement('style')
    style.type = 'text/css'
    style.innerHTML = rule
    document.getElementsByTagName('head')[0].appendChild(style)
    // 动画部分到此为止~~~~~~~~~~ 蠢的法力无边
    // 这里有另外一个比较简单的解决思路,加遮罩,遮罩的动画会比弧好做的多,这里不做演示,仅供参考
    // 绘制点的位置
    let circle = svg.selectAll('.circle')
      .data(_this.data)
      .enter()
      .append('circle')
      .attr('class', 'circle')
      .style('fill', '#F4606C')
      .attr('r', 0)
      .attr('cx', function (d, i) {
        return padLeft + (width - padLeft - padRight) / _this.data.length * (i + 0.5)
      })
      .attr('cy', function (d) {
        return yScale(d.value)
      })
    // 给circle加动画
    circle.transition()
      .delay(function (d, i) { return i * 200 })
      .duration(1000)
      .attr('r', 6)
    // 给circle加鼠标事件,添加tooltips
    let toolTips = d3.select('body').append('div')
      .attr('class', 'toolTips')
      .style('opacity', 0)
      .style('position', 'absolute')
    circle.on('mouseover', function (d) {
      d3.select(this)
        .transition()
        .duration(400)
        .attr('r', 10)
    })
    circle.on('mousemove', function (d) {
      let html = `<div class="clearfix"><div class="border" style="background:'#F4606C'"></div><span>${d.name}:${d.value}</span></div>`
      let mouseX = d3.event.clientX + 30
      let mouseY = d3.event.clientY - 30
      // 如果你的style用了scoped,那你的样式应该写到App.vue中去,否则插入元素的样式不会生效
      toolTips.html(`<div class="tolTp">${html}</div>`)
        .style('opacity', 1)
        .style('left', mouseX + 'px')
        .style('top', mouseY + 'px')
    })
    circle.on('mouseout', function (d) {
      d3.select(this)
        .transition()
        .duration(400)
        .attr('r', 6)
      toolTips.style('opacity', 0)
      toolTips.html('')
    })
  }
}
</script>

<style lang="less">
#line{
  width: 600px;
  height: 600px;
  margin: 20px 20px;
  padding: 15px 25px;
  border:1px solid #cccccc;
  position: relative;
}
.tolTp{
  padding:8px 12px;
  background: rgba(0, 0, 0, 0.7);
  color:white;
  .border{
    width: 6px;
    height: 6px;
    border-radius: 3px;
    background: #83bff6;
    float: left;
    margin:7px 8px 7px 0;
  }
  span{
    float: left;
    line-height: 20px;
  }
}
</style>

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值