绘制水泡图

D3绘制右侧水泡图。

1、修改 App.vue

添加 Right 组件。

<template>
  <div class="side-r-wrap">
    // ...
    <Right></Right>
  </div>
</template>

<script>
// ...
import Right from '@/components/Right'

export default {
  // ...

  components: {
    // ...
    Right
  }
}
</script>

2、构建 Right 组件

在 /src/components/ 创建 Right.vue:

<template>
  <div class="right">
    <div class="people-sex">
      <h2 class="chart-title">男女干警比例</h2>
      <div class="sex-chart" id="sexChart">
        <svg>
          <defs>
            <linearGradient id="outline" x1="0%" y1="0%" x2="0%" y2="100%">
              <stop offset="0%" style="stop-color: rgb(6, 124, 255); stop-opacity: 1;"></stop>
              <stop offset="100%" style="stop-color: rgb(160, 60, 218);stop-opacity: 1;"></stop>
            </linearGradient>
            <linearGradient id="innerBall" x1="0%" y1="0%" x2="0%" y2="100%">
              <stop offset="0%" style="stop-color: rgb(6, 124, 255); stop-opacity: 1;"></stop>
              <stop offset="100%" style="stop-color: rgb(160, 60, 218);stop-opacity: 1;"></stop>
            </linearGradient>
          </defs>
        </svg>
      </div>
      <div class="sex-legend">
        <p class="male">男性
          <span>{{male}}</span>
          <em>%</em>
        </p>
        <p class="female">女性
          <span>{{100 - male}}</span>
          <em>%</em>
        </p>
      </div>
    </div>
  </div>
</template>
<script>
  import * as d3 from 'd3'
  import axios from 'axios'
  import WaterBall from '../assets/scripts/charts/waterBall'
  import api from '../assets/scripts/tool/api'
  import Data from '../data/bandTop'
  Data()
  export default {
    name: 'right',
    data () {
      return {
        male: 0
      }
    },
    mounted () {
      const self = this
      axios.get(api.iottop5)
        .then(response => {
          const data = response.data.result
          self.deal(data.sex)
        })
        .catch(error => {
          console.error(error)
        })
    },
    methods: {
      deal (data) {
        this.male = data.male
        const config = {}
        const waterBall = new WaterBall('#sexChart', config)
        waterBall.drawCharts(data)
      }
    }
  }
</script>
<style scoped>
  .people-sex {
    position: absolute;
    top: 220px;
    right: 70px;
    width: 540px;
    height: 516px;
    background: url(../assets/images/common/tip-title-bg.png) no-repeat top left;
  }

  .sex-chart {
    margin-top:70px;
  }

  .sex-legend {
    position: absolute;
    top: 50%;
    right: 4%;
    transform: translateY(-50%);
  }

  .sex-legend p {
    font-size: 38px;
    color: #fff;
    padding-left: 40px;
    line-height: 1.5;
  }

  .sex-legend p span {
    color: #44ff86;
    font-size: 40px;
    margin: 0 12px 0 8px;
  }

  .sex-legend p em {
    color: #44ff86;
    font-size: 28px;
  }

  .sex-legend .male {
    background: url(../assets/images/fantasy/castle/male-legend.png) no-repeat left center;
  }

  .sex-legend .female {
    background: url(../assets/images/fantasy/castle/female-legend.png) no-repeat left center;
  }

  .jizhan {
    position: absolute;
    top: 743px;
    right: 70px;
    width: 540px;
    height: 690px;
    background: url(../assets/images/common/tip-title-bg.png) no-repeat top left;
  }

  .jizhan-list {
    margin-top: 136px;
  }

  .jizhan-item {
    width: 100%;
    height: 100px;
    margin-top: 10px;
    background: url(../assets/images/fantasy/castle/top-item-bg.png) 0% 0% / 100% 100% no-repeat;
  }

  .jizhan-item-index {
    float: left;
    height: 100%;
    line-height: 100px;
    width: 82px;
    text-align: center;
    color: #14c7fb;
    font-size: 36px;
  }

  .jizhan-item-container {
    float: left;
    box-sizing: border-box;
    height: 100%;
    padding: 20px 0px 20px 10px;
  }

  .jizhan-bar-container {
    width: 445px;
    height: 25px;
    line-height: 25px;
  }

  .jizhan-back-bar {
    position: relative;
    float: left;
    width: 360px;
    height: 24px;
    margin-right: 10px;
    background: url(../assets/images/fantasy/castle/top-progress-bg.png) 0% 0% / 100% 100% no-repeat;
  }

  .jizhan-outer-bar {
    position: absolute;
    top: 0;
    left: 0;
    height: 24px;
    background: linear-gradient(to right, #1963a7 0%, #bec374 100%)
  }

  .jizhan-value {
    float: left;
    color: #3da3ff;
    font-size: 30px;
  }

  .jizhan-item-name {
    font-size: 30px;
    color: #b0caf9;
  }

</style>

3、构建 api

修改 /src/assets/scripts/tool/api.js:

export default {
  // ...
  iottop5: onlineApiHost + 'iot/overview/top5'
}

4、制作模拟数据

在 /src/data 下创建 bandTop.js 文件:

import {
  urlReg
} from '../assets/scripts/tool/utils'

import Mock from 'mockjs'

const data = () => {
  Mock.mock(urlReg('/iot/overview/top5'), {
    'code': 1,
    'msg': 'success',
    'result': {
      'sex': {
        'male': '@natural(20,80)',
        'female': '@natural(20,80)'
      }
    }
  })
}
export default data

5、利用D3制作水泡图

在 /assets/scripts/charts/ 创建 waterBall.js 文件:

// /assets/scripts/charts/waterBall.js

/*
 * @Description: 水球图
 */
import * as d3 from 'd3'
export default class WaterBall {
  defaultSetting () {
    return {
      width: 400,
      height: 400,
      radius: 120,
      fillOuterLine: 'url(#outline)', // 外圈圆
      innerBall: 'url(#innerBall)' // 100% 实心圆填充
    }
  }
  constructor (selector, option) {
    const defaultSetting = this.defaultSetting()
    this.config = Object.assign(defaultSetting, option)
    const {
      width,
      height
    } = this.config
    // 创建svg
    this.svg = d3.select(selector).select('svg')
      .attr('width', width)
      .attr('height', height)
    this.defs = this.svg.select('defs')
  }
  drawCharts (data) {
    const {
      width,
      height,
      radius,
      innerBall,
      fillOuterLine
    } = this.config
    let rate
    if (data.male === 0 || data.female === 0) {
      rate = 0
    } else {
      rate = Math.floor(data.male)
    }
    const dataset = [
      [
        [rate, 100 - rate]
      ]
    ]
    // 设置布局
    const clockwisePie = d3.pie() // 顺时针,针对数据类型:[small,bigger]
      .startAngle(Math.PI)
      .endAngle(3 * Math.PI)
    const anticlockwisePie = d3.pie() // 逆时针,针对数据类型:[bigger,small]
      .startAngle(Math.PI)
      .endAngle(-Math.PI)
    // 绘制男性
    const maleArc = d3.arc()
      .innerRadius(radius - 8)
      .outerRadius(radius + 8)
    // 绘制女性
    const femaleArc = d3.arc()
      .innerRadius(radius - 30)
      .outerRadius(radius - 14)
    // 处理好结构
    const ballUpdate = this.svg.selectAll('.ball')
      .data(dataset)
    const ballEnter = ballUpdate.enter().append('g').attr('class', 'ball')
    ballUpdate.exit().remove()
    const ballGroup = this.svg.selectAll('.ball').data(dataset)
      .attr('transform', `translate(${width / 2 - 50},${height / 2})`)
    // 绘制内部实心圆
    ballEnter.append('circle').attr('class', 'innerCircle')
    ballGroup.select('.innerCircle')
      .attr('r', radius - 14)
      .attr('fill', 'rgba(79,35,129,0.6)')
    // 绘制100%的实心圆
    ballEnter.append('circle').attr('class', 'fillCircle')
    ballGroup.select('.fillCircle')
      .attr('r', radius - 14)
      .attr('fill', innerBall)
      .attr('clip-path', 'url(#areaWave)')
    // 绘制外圈渐变填充圆
    ballEnter.append('circle').attr('class', 'outLine')
    ballGroup.select('.outLine')
      .attr('r', radius)
      .attr('fill', 'none')
      .attr('stroke', fillOuterLine)
      .attr('stroke-width', 4)
    // 绘制外圈纯色填充圆 -- 男性占比
    ballEnter.append('path').attr('class', 'maleCircle')
    ballGroup.select('.maleCircle')
      .attr('fill', '#01e2fa')
      .attr('d', d => {
        return d[0][0] >= d[0][1] ? maleArc(clockwisePie(d[0])[0]) : maleArc(anticlockwisePie(d[0])[0])
      })
    // 绘制外圈纯色填充圆 -- 女性占比
    ballEnter.append('path').attr('class', 'femaleCircle')
    ballGroup.select('.femaleCircle')
      .attr('fill', '#ff03b9')
      .attr('d', d => {
        const femaleData = [
          [d[0][1], d[0][0]]
        ]
        if (femaleData[0][0] >= femaleData[0][1]) {
          return femaleArc(clockwisePie(femaleData[0])[0])
        } else {
          return femaleArc(anticlockwisePie(femaleData[0])[0])
        }
      })
    // 制作波浪纹 - clipPath
    const clipPathUpdate = this.defs.selectAll('clipPath').data(d3.range(1))
    clipPathUpdate.enter().append('clipPath').append('path')
    clipPathUpdate.exit().remove()
    const waveClipCount = 2
    const waveClipWidth = radius * 4
    const waveHeight = 10.26
    const waveOffset = 0
    const waveCount = 1
    let wavaData = []
    for (let i = 0; i <= 40 * waveClipCount; i++) {
      wavaData.push({
        x: i / (40 * waveClipCount),
        y: (i / (40))
      })
    }
    const waveScaleX = d3.scaleLinear()
      .range([0, waveClipWidth])
      .domain([0, 1])
    const waveScaleY = d3.scaleLinear()
      .range([0, waveHeight])
      .domain([0, 1])
    // translateY为radius 对应 0%
    // translateY为-radius 对应 100%
    const wavePercentScale = d3.scaleLinear()
      .domain([0, 100])
      .range([radius, -radius])
    const clipArea = d3.area()
      .x(d => {
        return waveScaleX(d.x)
      })
      .y0(d => {
        return waveScaleY(Math.sin(Math.PI * 2 * waveOffset * -1 + Math.PI * 2 * (1 - waveCount) + d.y * 2 * Math.PI))
      })
      .y1(2 * radius)
    let clipPath = this.defs.selectAll('clipPath')
      .attr('id', 'areaWave')
      .select('path')
        .datum(wavaData)
        .attr('d', clipArea)
        .attr('fill', 'yellow')
        .attr('T', 0)
    clipPath.transition()
      .duration(2000)
      .attr('transform', `translate(${-3 * radius},${wavePercentScale(rate)})`)
      .on('start', () => {
        clipPath.attr('transform', `translate(${-3 * radius},${radius})`)
      })
    // 绘制图表名字
    ballEnter.append('text').attr('class', 'chart-name')
    ballGroup.select('.chart-name')
      .attr('y', -radius / 4)
      .attr('text-anchor', 'middle')
      .attr('fill', '#f4f8fc')
      .attr('font-weight', 'bold')
      .attr('font-size', 30)
      .text('男性占比')
    // 绘制百分占比数值 -- 严格的绘制顺序决定层级
    ballEnter.append('text').attr('class', 'valueText')
    ballGroup.select('.valueText')
      .attr('y', radius / 4 + 20)
      .attr('text-anchor', 'middle')
      .attr('fill', '#f4f8fc')
      .attr('font-size', 70)
      .text(0)
      .transition()
      .duration(3000)
      .on('start', function () {
        d3.active(this)
          .tween('text', function (d) {
            const that = d3.select(this)
            return function (t) {
              that.text(Math.floor(t * d[0][0]))
            }
          })
      })
    // 绘制value值百分比符号
    ballEnter.append('text').attr('class', 'percentText')
    ballGroup.select('.percentText')
      .attr('y', 40)
      .attr('x', 70)
      .attr('text-anchor', 'middle')
      .attr('fill', '#fff')
      .attr('font-size', 40)
      .text('%')
    // 用定时器做波浪动画
    setTimeout(function () {
      let distance = -3 * radius
      d3.timer(() => {
        distance++
        if (distance > -radius) {
          distance = -3 * radius
        }
        clipPath.attr('transform', `translate(${distance},${wavePercentScale(rate)})`)
      })
    }, 2000)
  }
}

results matching ""

    No results matching ""