构建左侧信息展示
1、App 组件 修改
/App.vue
<template>
<div class="castle side-r-wrap">
<TopSide :title="title"></TopSide>
<TopMid></TopMid>
<Left></Left>
</div>
</template>
<script>
import TopSide from '@/components/TopSide'
import TopMid from '@/components/TopMid'
import Left from '@/components/Left'
export default {
name: 'Castle',
data () {
return {
title: '公安警情可视化'
}
},
components: {
TopSide,
TopMid,
Left
}
}
</script>
<style scoped>
</style>
2、构建 Left.vue
/src/components/Left.vue
<template>
<div class="left">
<div class="diandongche">
<h2 class="chart-title">年度刑事案件数
</h2>
<ul class="alarm-list">
<li>
<p class="alarm-list-name">
<span>今年</span>
</p>
<p class="alarm-list-value">{{thisyear}}</p>
</li>
<li>
<p class="alarm-list-name">
<span>环比</span>
</p>
<p class="alarm-list-value" :data-state="hbstate">{{huanbi}}</p>
</li>
<li>
<p class="alarm-list-name">
<span>去年</span>
</p>
<p class="alarm-list-value">{{lastyear}}</p>
</li>
</ul>
</div>
<div class="caseResolved">
<h2 class="chart-title">年度出警数</h2>
<div class="case-resolved-chart" id="caseResolvedChart"></div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import api from '../assets/scripts/tool/api'
import Data from '../data/fantasy/castle/left'
import MultiPie from '../assets/scripts/charts/multiPie'
Data()
export default {
name: 'left',
data () {
return {
thisyear: '',
hbstate: '',
huanbi: '',
lastyear: ''
}
},
mounted () {
const self = this
axios.get(api.iotalarm)
.then(response => {
const data = response.data.result
self.dealDDC(data.diandongche)
self.dealCase(data.caseResolved)
})
.catch(error => {
console.error(error)
})
},
methods: {
dealDDC (data) {
let hbstate
let hbValue = data.huanbi
if (hbValue < 0) {
hbstate = 'down'
hbValue = Math.abs(hbValue)
} else if (hbValue === 0) {
hbstate = 'level'
hbValue = '- 0'
} else if (hbValue > 0) {
hbstate = 'up'
hbValue = Math.abs(hbValue)
}
this.thisyear = data.thisyear
this.hbstate = hbstate
this.huanbi = hbValue
this.lastyear = data.lastyear
},
dealCase (data) {
const config = {}
const multiPie = new MultiPie('.case-resolved-chart', config)
multiPie.render(data)
}
}
}
</script>
<style scoped>
.diandongche {
position: absolute;
top: 220px;
left: 60px;
width: 680px;
height: 600px;
background: url(../assets/images/common/tip-title-bg.png) no-repeat top left;
}
.alarm-list {
position: absolute;
top: 120px;
left: 0px;
width: 100%;
height: 100%;
font-size: 0;
}
.alarm-list li {
width: 50%;
height: 190px;
overflow: hidden;
display: inline-block;
margin-bottom: 10px;
}
.alarm-list-name span {
font-size: 50px;
color: #b4c7f9;
position: relative;
display: inline-block;
}
.alarm-list-name span:after {
content: "件";
display: inline-block;
position: absolute;
top: 4px;
right: -80px;
text-align: center;
font-size: 30px;
line-height: 46px;
color: #8da5e4;
height: 46px;
width: 60px;
background: #0c3f87;
border: 1px solid #443cba;
}
.alarm-list li:nth-child(2) .alarm-list-name span:after {
display: none;
}
.alarm-list-value {
font-size: 60px;
color: #1aac4e;
position: relative;
display: inline-block;
margin-top:20px;
}
.alarm-list li:first-child .alarm-list-value {
font-size: 90px;
color: #44ff86;
}
.alarm-list li:nth-child(2) .alarm-list-value {
margin-top: 34px;
text-indent: 40px;
}
.alarm-list li:nth-child(2) .alarm-list-value[data-state="up"] {
color: #ff4444;
}
.alarm-list li:nth-child(2) .alarm-list-value[data-state="level"] {
color: #b4c7f9;
}
.alarm-list li:nth-child(2) .alarm-list-value[data-state="down"] {
color: #44ff86;
}
.alarm-list li:nth-child(2) .alarm-list-value:after {
content: "%";
display: inline-block;
position: absolute;
bottom: 4px;
right: -24px;
font-size: 30px;
}
.alarm-list li:nth-child(2) .alarm-list-value[data-state]:before {
content: "";
display: inline-block;
position: absolute;
width: 25px;
height: 26px;
top: 26px;
left: 0px;
}
.alarm-list li:nth-child(2) .alarm-list-value[data-state="up"]:before {
background: url(../assets/images/common/huanbi-up.png) no-repeat;
}
.alarm-list li:nth-child(2) .alarm-list-value[data-state="down"]:before {
background: url(../assets/images/common/huanbi-down.png) no-repeat;
}
.caseResolved {
position: absolute;
top: 830px;
left: 60px;
background: url(../assets/images/common/tip-title-bg.png) no-repeat top left;
}
.case-resolved-chart {
width: 900px;
height: 270px;
margin-top: 130px;
}
</style>
/src/data/fantasy/castle/left.js
import {
urlReg
} from '../../../assets/scripts/tool/utils'
import Mock from 'mockjs'
const data = () => {
Mock.mock(urlReg('/iot/overview/alarm'), {
'code': 1,
'msg': 'success',
'result': {
'diandongche': {
'lastyear': '@natural(1,2000)',
'thisyear': '@natural(1,2000)',
'huanbi': '@integer(-100,100)'
},
'caseResolved|3': [{
'name|+1': ['部门一', '部门二', '部门三'],
'value': '@natural(1,2000)'
}]
}
})
}
export default data
/src/assets/scripts/charts/multiPie.js
import * as d3 from 'd3'
export default class MultiPie {
/**
* 默认配置项
* @return {[Object]} [默认配置项]
*/
defaultSetting() {
return {
width: 900,
height: 270,
radius: [50, 66], // [innerRadius,outerRadius]
gap: 100, // 相邻两个图形的间距
margin: { // 多个图形布局:从左往右,竖直方向按容器高度居中放置,故只设置左侧距离left即可
left: 10
},
label: { // 名称文本样式
normal: {
fontSize: 32,
color: '#46aaff',
anchor: 'middle',
cursor: 'pointer',
top: 46 // 名称文本距离图案顶部的距离
},
emphasis: {
fontSize: 32,
color: '#74ffd3',
anchor: 'middle',
cursor: 'pointer',
top: 46 // 名称文本距离图案顶部的距离
}
},
itemStyle: {
label: { // value值文本样式
fontSize: 32,
color: '#46aaff',
anchor: 'middle',
cursor: 'pointer',
top: 10 // value值文本距离容器中线的偏移距离,默认放在饼图正中间
},
color: [ // 饼图填充色
['#4a8ce5', 'black'],
['#44ff86', 'black'],
['#dccc5c', 'black']
]
}
}
}
/**
* 初始化,创建容器
* @param {String} selector 图表容器,支持class或id
* @param {Object} option 配置项,控制图形样式
* @return {[type]} [description]
*/
constructor(selector, option = {}) {
const defaultSetting = this.defaultSetting()
this.config = Object.assign(defaultSetting, option)
const {
width,
height
} = this.config
// 创建svg
this.svg = d3.select(selector)
.append('svg')
.attr('width', width)
.attr('height', height)
}
/**
* 处理原始数据,获取pie布局转换后的数据
* @param {Array} data 原始数据
* @return {Array} dataset 转换后的数据
*/
getDataset(data) {
let dataset = []
const clockwisePie = d3.pie() // 顺时针,针对数据类型:[small,bigger]
const anticlockwisePie = d3.pie() // 逆时针,针对数据类型:[bigger,small]
.startAngle(0)
.endAngle(-2 * Math.PI)
// 求取总数:sum
let sum = 0
data.map(d => {
sum += parseInt(d.value, 10)
})
data.map((d) => {
let value = d.value
let rate = Math.max(Math.floor(value * 100 / sum), 1)
let rateData = [rate, 100 - rate]
let dealData = rate >= 50 ? clockwisePie(rateData) : anticlockwisePie(rateData)
dataset.push(dealData)
})
return dataset
}
/**
* 绘制图案底部的名称文本
* @param {Object} chart 包裹文本的外层g容器
* @param {Object} info 单组原始数据,包括name和value
* @return {[type]} [description]
*/
renderName(chart, info) {
const {
radius: [, outerRadius],
label: {
normal: {
fontSize: fontSizeNor,
color: colorNor,
anchor: anchorNor,
top: topNor,
cursor: cursorNor
},
emphasis: {
fontSize: fontSizeEmp,
color: colorEmp,
anchor: anchorEmp,
top: topEmp,
cursor: cursorEmp
}
}
} = this.config
chart.select('.pie-name')
.attr('font-size', fontSizeNor)
.attr('fill', colorNor)
.attr('text-anchor', anchorNor)
.attr('transform', `translate(0, ${outerRadius + topNor})`)
.attr('cursor', cursorNor)
.text(info.name)
.on('mouseover', function () {
d3.select(this)
.attr('font-size', fontSizeEmp)
.attr('fill', colorEmp)
.attr('text-anchor', anchorEmp)
.attr('transform', `translate(0, ${outerRadius + topEmp})`)
.attr('cursor', cursorEmp)
})
.on('mouseout', function () {
d3.select(this)
.attr('font-size', fontSizeNor)
.attr('fill', colorNor)
.attr('text-anchor', anchorNor)
.attr('transform', `translate(0, ${outerRadius + topNor})`)
.attr('cursor', cursorNor)
})
}
/**
* 绘制图案中间的value值文本
* @param {Object} chart 包裹文本的外层g容器
* @param {[type]} info 单组原始数据,包括name和value
* @return {[type]} [description]
*/
renderValue(chart, info) {
const {
itemStyle: {
label: {
fontSize,
color,
anchor,
cursor,
top
}
}
} = this.config
chart.select('.pie-value')
.attr('font-size', fontSize)
.attr('fill', color)
.attr('text-anchor', anchor)
.attr('transform', `translate(0,${top})`)
.attr('cursor', cursor)
.text(info.value)
}
/**
* 绘制单个Pie图案
* @param {Objec} chartName 单个图案的外层g容器
* @param {Array} pieData 绘制饼图的数据(已经过布局处理)
* @param {Object} info 该图案的原始数据,包括name和value
* @param {Array} color 填充饼图的两个颜色值
* @return {[type]} [description]
*/
creatPie(chartName, pieData, info, color) {
const {
radius: [innerRadius, outerRadius]
} = this.config
const arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
const chart = this.svg.select(chartName)
const update = chart.selectAll('path').data(pieData)
const enter = update.enter()
update.exit().remove()
// 绘制饼图图案
enter.append('path')
chart.selectAll('path').data(pieData)
.attr('fill', (d, i) => {
return color[i]
})
.attr('d', d => {
return arc(d)
})
// 绘制名称--name
enter.append('text').attr('class', 'pie-name')
this.renderName(chart, info)
// 绘制value值
enter.append('text').attr('class', 'pie-value')
this.renderValue(chart, info)
}
render(data) {
let dataset = this.getDataset(data)
const update = this.svg.selectAll('.item')
.data(dataset)
update.enter().append('g').attr('class', 'item')
update.exit().remove()
// 多个图形布局:从左往右,相邻图形间隔为配置项----config.gap
const {
height,
radius: [, R],
gap,
margin: {
left
},
itemStyle: {
color
}
} = this.config
this.svg.selectAll('.item').data(dataset)
.attr('transform', (d, i) => {
return `translate(${R + left + 2 * R * i + i * gap},${height / 2})`
})
.attr('class', (d, i) => {
return `item${i} item`
})
// 逐个绘制饼图
dataset.map((d, i) => {
this.creatPie(`.item${i}`, d, data[i], color[i])
})
}
}