绘制水泡图
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)
}
}