加上数据更新功能,这个功能代码在github上

echarts效果参考链接:http://echarts.baidu.com/examples/editor.html?c=pie-doughnut

github源码地址:https://github.com/dkr380205984/myComponent/blob/master/src/page/d3/pie.vue

饼状图/圆环图探究

创新点:颜色过渡

功能点:两种动画模式

颜色过渡设计方法:首先你能通过d3.pie提供的方法将你的数据变成一段段圆弧,圆弧有两个基本参数,startAngle,endAngle,也就是起始角度,在你将数据绘制成真实弧线之前,你需要将起始角度分成一百份,当然50份可能也够用,不用纠结这个问题,然后将这一百份数据绘制连续过渡的颜色即可。连续性的颜色D3也提供了方法,所以不用担心需要自己写一个100段的颜色组。

关于饼图的其他功能点,可以看代码的注释,足够清晰,需要讨论其他功能点的可以下方留言。

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

<script>
import * as d3 from 'd3'
export default {
  data: function () {
    return {
      data: [{
        name: '小米',
        value: 60.8
      }, {
        name: '华为',
        value: 20.8
      }, {
        name: '联想',
        value: 30.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]
      }
    },
    // 对象深度拷贝
    deepClone (obj) {
      var newObj = obj.constructor === Array ? [] : {}
      if (typeof obj !== 'object') {
        return
      } else {
        for (var i in obj) {
          if (obj.hasOwnProperty(i)) {
            newObj[i] = typeof obj[i] === 'object' ? this.deepClone(obj[i]) : obj[i]
          }
        }
      }
      return newObj
    }
  },
  mounted: function () {
    let _this = this
    let dom = document.getElementById('pie')
    // 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 color = d3.scaleOrdinal(d3.schemeCategory10)
    // 设置颜色数组,这个自定义的好看一些
    let color = ['#1890FF', '#2FC25B', '#FACC14', '#223273', '#8543E0', '#13C2C2', '#3436C7', '#F04864']
    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('#pie')
      .append('svg')
      .attr('width', width)
      .attr('height', height)
    // 用d3提供的API将一组表格数据转换成生成饼图或者环形图需要的数据
    let pie = d3.pie()
      .value((d) => d.value)(_this.data)
    // 先绘制一下图例
    padTop += 40 // 先预留40像素的高度放图例
    let legend = d3.select('#pie')
      .append('div')
      // 创建dom容器
      .attr('class', 'legendContainer clearfix')
      .style('height', '40px')
      .style('position', 'absolute')
      .style('left', 'calc(50% + ' + _this.getStyle(dom, 'paddingLeft') + ')')
      .style('top', _this.getStyle(dom, 'paddingTop'))
      .style('transform', 'translate(-50%,0)')
      .style('display', 'inline-table')
      // 每个图例容器
      .selectAll('.legend')
      .data(pie)
      .enter()
      .append('div')
      .attr('class', 'legend')
    legend.html(function (d, i) {
      let colors = 'to right' + ',' + color[d.index] + ',' + color[d.index + 1]
      return `<div class="chart" style="background: linear-gradient(${colors})"></div><span>${d.data.name}</span>`
    })
    // 给图例添加点击事件
      .on('click', function (d) {
      // 在这里可以触发一个回调函数,让用户自己去操作数据,
      // 这里的操作有点复杂,在封装的时候会把这个点击操作完善一下,实现echarts的效果
        console.log(d)
      })
    // 创建弧生成器
    // 计算一下外半径的最大值,取宽高中的最小值
    let outerRadius = d3.min([width - padLeft - padRight, height - padTop - padBottom])
    let arc = d3.arc()
      .innerRadius(120) // 设置环的内半径,为0的时候则是圆
      .outerRadius(outerRadius / 2.8) // 设置环的外半径,用计算得到的最大值除以>2的值
    // 用创建的弧生成器和已经通过pie方法得到数据绘制路径
    // 第一步,创建弧的container
    let path = svg.selectAll('.arcs')
      .data(pie)
      .enter()
      .append('g')
      .attr('class', 'arcs')
      .attr('transform', 'translate(' + (padLeft + (width - padLeft - padRight) / 2) + ',' + (padTop + (height - padTop - padBottom) / 2) + ')')
    // 第二步,将每个弧分很多段,实现渐变效果
    path.selectAll('path')
    // 这里我将原来的数据的起始角度拆分成100份,返回一个新的数组
      .data((d, i) => {
        let arr = []
        for (let i = 0; i < 100; i++) {
          let copy = _this.deepClone(d)
          copy.endAngle = copy.endAngle - 1 / 360 * Math.PI // 如果你不希望你的圆环看起来像一个光盘,你可以这么处理,给每段弧留个起始空隙
          copy.startAngle = copy.startAngle + 1 / 360 * Math.PI
          let delt = (copy.endAngle - copy.startAngle) / 100
          copy.startAngle = copy.startAngle + delt * i
          copy.endAngle = copy.startAngle + delt
          arr.push(copy)
        }
        return arr
      })
      .enter()
      .append('path')
      .transition()
      .delay(function (d, i) {
        // return i * 15 // 这种动画比较容易想到,但看起来似乎没有后面一种好看,可以试试,比较传统
        return (i + (d.index * 100)) * 10 // 这里的动画处理是重点,每一段弧的起始时间需要加上d.index*100,才能实现环环相扣的动画效果
      })
      .attr('fill', function (d, i) {
        // 创建一个线性比例尺
        let linear = d3.scaleLinear()
          .domain([0, 100])
          .range([0, 1])
        // compute用d3.interpolate方法将某个颜色区间内的颜色均分划分
        let compute
        if (d.index === pie.length - 1) {
          compute = d3.interpolate(color[d.index], color[0])
        } else {
          compute = d3.interpolate(color[d.index], color[d.index + 1])
        }
        return compute(linear(i))
      })
      .attr('stroke', function (d, i) {
        let linear = d3.scaleLinear()
          .domain([0, 100])
          .range([0, 1])
        let compute
        if (d.index === pie.length - 1) {
          compute = d3.interpolate(color[d.index], color[0])
        } else {
          compute = d3.interpolate(color[d.index], color[d.index + 1])
        }
        return compute(linear(i))
      })
      .attr('d', function (d, i) { return arc(d) })
    // 给每段添加鼠标交互,注意我用了mouseenter和mouseleave,而不是mouseover和mouseout,这样可以防止事件多次触发
    let arcHover = d3.arc()
      .innerRadius(outerRadius / 2.8) // 设置环的内半径,为初始环的外半径
      .outerRadius(outerRadius / 2.5) // 设置环的外半径,稍微大于初始环的外半径
    // 这里的设计思路就是在外面加一层环,看起来像弹出来,其实是两层
    path.on('mouseenter', function (d) {
      // 给中间加文字
      svg.data([d])
        .append('text')
        .attr('class', 'text')
        .text(function (d) { return d.data.name })
        .attr('x', padLeft + (width - padLeft - padRight) / 2)
        .attr('y', padTop + (height - padTop - padBottom) / 2)
        .attr('dx', function (d) {
          return -d.data.name.length / 2 + 'em'
        })
        .attr('dy', '0.5em')
        .attr('style', 'font-size:28px;font-weight:bold;letter-spacing:3px')
        .attr('fill', function (d) { return color[d.index] })
      // 添加过渡动画
      d3.select(this)
        .selectAll('.hover')
        .data((d, i) => {
          let arr = []
          for (let i = 0; i < 100; i++) {
            let copy = _this.deepClone(d)
            copy.endAngle = copy.endAngle - 1 / 360 * Math.PI // 如果你不希望你的圆环看起来像一个光盘,你可以这么处理,给每段弧留个起始空隙
            copy.startAngle = copy.startAngle + 1 / 360 * Math.PI
            let delt = (copy.endAngle - copy.startAngle) / 100
            copy.startAngle = copy.startAngle + delt * i
            copy.endAngle = copy.startAngle + delt
            arr.push(copy)
          }
          return arr
        })
        .enter()
        .append('path')
        .attr('class', 'hover')
        .attr('fill', function (d, i) {
          // 创建一个线性比例尺
          let linear = d3.scaleLinear()
            .domain([0, 100])
            .range([0, 1])
          // compute用d3.interpolate方法将某个颜色区间内的颜色均分划分
          let compute
          if (d.index === pie.length - 1) {
            compute = d3.interpolate(color[d.index], color[0])
          } else {
            compute = d3.interpolate(color[d.index], color[d.index + 1])
          }
          return compute(linear(i))
        })
        .attr('stroke', function (d, i) {
          let linear = d3.scaleLinear()
            .domain([0, 100])
            .range([0, 1])
          let compute
          if (d.index === pie.length - 1) {
            compute = d3.interpolate(color[d.index], color[0])
          } else {
            compute = d3.interpolate(color[d.index], color[d.index + 1])
          }
          return compute(linear(i))
        })
        .attr('d', function (d, i) { return arc(d) })
        .transition()
        .duration(200)
        .ease(d3.easeBounceOut)
        .attr('d', function (d, i) { return arcHover(d) })
    })
    // 添加tooltips
    let toolTips = d3.select('body').append('div')
      .attr('class', 'toolTips')
      .style('opacity', 0)
      .style('position', 'absolute')
    path.on('mousemove', function (d) {
      let html = `<div class="clearfix"><div class="border" style="background:${color[d.index]}"></div><span>${d.data.name}:${d.data.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')
    })
    // 这里没有做动画把他弹回去,没什么必要,眼睛会帮我们缓存一部分图像,看起来也ok
    path.on('mouseleave', function (d) {
      d3.select(this)
        .selectAll('.hover')
        .remove()
      svg.select('.text').remove()
      toolTips.style('opacity', 0)
      toolTips.html('')
    })
  }
}
</script>

<style lang="less">
#pie{
  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;
  height: 20px;
  .border{
    width: 6px;
    height: 6px;
    border-radius: 3px;
    background: #83bff6;
    float: left;
    margin:7px 8px 7px 0;
  }
  span{
    float: left;
    line-height: 20px;
  }
}
.legend{
  float: left;
  .chart{
    width: 30px;
    height: 18px;
    margin: 11px 6px;
    background: rebeccapurple;
    float: left;
    border-radius: 4px;
    cursor: pointer;
    &:hover{
      opacity: 0.4;
    }
  }
  span{
    float: left;
    line-height: 40px;
    font-size: 12px;
    margin-right: 10px;
  }
}
</style>

 

Logo

前往低代码交流专区

更多推荐