前言
这段时间一直在做可视化,在我的项目中有一部分是电力巡检的数据可视化。其中的数据看板比较简单,我将其单独抽离出来形成一个demo,为保密demo中数据非真实数据。先看效果。
具体效果
链接相关
- 浏览链接:http://xisite.top/original/data-board/index.html#/
- 项目链接(觉得有用的记得star哦):https://gitee.com/xi1213/data-board
实现目标
- 可根据项目切换不同看板数据。
- 数据的展现形式包括:折线图,柱状图、饼图、环图、进度图、轮播图。
- 包含一个可控制的3d球体,球面打点具体数据。
具体实现
数据切换
没啥技术含量,demo数据我是写死,要使用的可以直接拿去替换为自己的接口数据。
projectPtions: [ | |
{ | |
value: ‘1’, | |
label: ‘四川项目’, | |
sphereData: [ | |
{ | |
position: [102, 30], | |
pointName: “成都”, | |
value: 31889355 | |
}, | |
{ | |
position: [102, 27],//经度,纬度 | |
pointName: “西昌”, | |
value: 13578453 | |
}, | |
{ | |
position: [107, 31],//经度,纬度 | |
pointName: “达州”, | |
value: 7854541453 | |
}, | |
], | |
msg: { | |
distance: 12245,//巡检距离 | |
towerNum: 85345,//杆塔数量 | |
defectNum: 208//缺陷数量 | |
}, | |
lineData: [ | |
[ | ],|
[ | ],|
[ | ]|
],//折线图数据 | |
pieData: [234, 124, 156, 178],//饼图数据 | |
ringData: [100, 120, 104, 140, 160],//环图数据 | |
histData: { | |
xAxisData: [‘成都’, ‘南充’, ‘宜宾’, ‘西昌’, ‘眉山’, ‘乐山’, ‘攀枝花’], | |
seriesData: [635, 880, 1165, 135, 342, 342, 524] | |
},//柱状图数据 | |
proData: [29, 67, 90],//进度图数据 | |
}, | |
{ | |
value: ‘2’, | |
label: ‘西藏项目’, | |
sphereData: [ | |
{ | |
position: [91.11, 29.97],//经度,纬度 | |
pointName: “拉萨”, | |
value: 78453 | |
}, | |
{ | |
position: [80, 32],//经度,纬度 | |
pointName: “阿里”, | |
value: 13578453 | |
}, | |
{ | |
position: [88, 29],//经度,纬度 | |
pointName: “日喀则”, | |
value: 7854541453 | |
}, | |
], | |
msg: { | |
distance: 20018,//巡检距离 | |
towerNum: 87624,//杆塔数量 | |
defectNum: 126189//缺陷数量 | |
}, | |
lineData: [ | |
[ | ],|
[ | ],|
[ | ]|
],//折线图数据 | |
pieData: [134, 154, 156, 198],//饼图数据 | |
ringData: [120, 180, 114, 120, 110],//环图数据 | |
histData: { | |
xAxisData: [‘拉萨’, ‘日喀则’, ‘昌都’, ‘林芝’, ‘山南’, ‘那曲’, ‘阿里’], | |
seriesData: [100, 280, 467, 956, 345, 111, 61] | |
},//柱状图数据 | |
proData: [69, 37, 50],//进度图数据 | |
}, | |
{ | |
value: ‘3’, | |
label: ‘浙江项目’, | |
sphereData: [ | |
{ | |
position: [119, 27],//经度,纬度 | |
pointName: “温州”, | |
value: 78453 | |
}, | |
{ | |
position: [120, 29],//经度,纬度 | |
pointName: “宁波”, | |
value: 13578453 | |
}, | |
{ | |
position: [120, 30],//经度,纬度 | |
pointName: “嘉兴”, | |
value: 7854541453 | |
}, | |
], | |
msg: { | |
distance: 18722,//巡检距离 | |
towerNum: 122334,//杆塔数量 | |
defectNum: 127895//缺陷数量 | |
}, | |
lineData: [ | |
[ | ],|
[ | ],|
[ | ]|
],//折线图数据 | |
pieData: [134, 174, 156, 108],//饼图数据 | |
ringData: [190, 110, 174, 130, 110],//环图数据 | |
histData: { | |
xAxisData: [‘杭州’, ‘宁波’, ‘温州’, ‘嘉兴’, ‘湖州’, ‘金华’, ‘舟山’], | |
seriesData: [1035, 100, 565, 435, 142, 842, 124] | |
},//柱状图数据 | |
proData: [89, 37, 60],//进度图数据 | |
}, | |
], |
数组中的每一个对象代表一个项目。
切换项目时直接使用element的el-select切换即可。由于图表组件是区分了组件的,每次切换数据时需要根据不同数据重绘图表。
折线图
图中可以看到一共只有九个图表。比较简单,直接使用echarts配置即可。这是折线图。
可能会感觉奇怪,折线图咋会这样呢?那是因为在配置中设置了areaStyle与smooth,使折线图变成了平滑的堆叠面积图,本质还是折线图。areaStyle中的color可以接受echarts.graphic.LinearGradient,使其具有渐变的颜色,LinearGradient的前四个参数分别为渐变色的起始点与终止点的x值与y值,后面的值为颜色值。
let option = { | |
color: [], | |
title: { | |
text: ‘项目执行情况’, | |
top: “5%”, | |
left: ‘center’, | |
textStyle: { | |
color: “#fff” | |
} | |
}, | |
tooltip: { | |
trigger: ‘axis’, | |
axisPointer: { | |
label: { | |
backgroundColor: ‘#6a7985’ | |
} | |
} | |
}, | |
grid: { | |
top: “20%”, | |
left: ‘3%’, | |
right: ‘4%’, | |
bottom: ‘3%’, | |
containLabel: true | |
}, | |
xAxis: [ | |
{ | |
type: ‘category’, | |
data: [], | |
axisLabel: { | |
color: “#fff”, | |
}, | |
axisLine: { | |
lineStyle: { | |
color: this.dataVColor[1] | |
} | |
} | |
} | |
], | |
yAxis: [ | |
{ | |
type: ‘value’, | |
axisLabel: { | |
color: “#fff”, | |
}, | |
axisLine: { | |
lineStyle: { | |
color: this.dataVColor[1] | |
} | |
}, | |
splitLine: { | |
show: true,//网格设置 | |
lineStyle: { | |
color: “#70707033”, | |
width: 1, | |
type: “dotted”,//虚线 | |
}, | |
}, | |
} | |
], | |
series: [] | |
}; | |
option.xAxis[0].data = chartData.xAxisData; | |
chartData.seriesData.forEach(s => { | |
option.color.unshift(this.dataVColor[1]);//注意颜色添加的顺序 | |
option.series.push( | |
{ | |
animationDuration: 3000,//动画时间 | |
animationEasing: “cubicInOut”,//动画类型 | |
name: s.name, | |
type: ‘line’, | |
smooth: true, | |
stack: ‘Total’, | |
lineStyle: { | |
width: 1 | |
}, | |
showSymbol: false, | |
areaStyle: { | |
opacity: 0.8, | |
//使用线性渐变颜色(x1,y1,x2,y2,渐变数组) | |
color: new echarts.graphic.LinearGradient(1, 1, 1, 0, [ | |
{ | |
offset: 0, | |
color: this.dataVColor[0] | |
}, | |
{ | |
offset: 1, | |
color: “#fff” | |
} | |
]) | |
}, | |
emphasis: { | |
focus: ‘series’ | |
}, | |
data: s.data | |
} | |
) | |
}); | |
await (option && this.lineChart.setOption(option));//设置数据 |
饼图
饼图我一样在itemStyle的color中设置了渐变色。饼图的尺寸是通过series中的radius来控制的,位置是center来控制的。
let option = { | |
title: { | |
text: ‘任务类型占比’, | |
top: “5%”, | |
left: ‘center’, | |
textStyle: { | |
color: “#fff” | |
} | |
}, | |
tooltip: { | |
trigger: ‘item’ | |
}, | |
series: [ | |
{ | |
type: ‘pie’, | |
animationDuration: 3000, | |
radius:”60%”, | |
animationEasing: “cubicInOut”, | |
center: [“50%”, “60%”],//饼图位置 | |
label: { | |
color: “#fff” | |
}, | |
emphasis: { | |
label: { | |
show: true, | |
fontSize: ’20’, | |
fontWeight: ‘bold’ | |
} | |
}, | |
data: [], | |
} | |
] | |
}; | |
chartData.seriesData.forEach(s => { | |
option.series[0].data.push( | |
{ | |
value: s.value, | |
name: s.name, | |
itemStyle: { | |
color: new echarts.graphic.LinearGradient(1, 1, 1, 0, [ | |
{ | |
offset: 0, | |
color: this.dataVColor[0] | |
}, | |
{ | |
offset: 1, | |
color: “#fff” | |
} | |
]) | |
} | |
} | |
) | |
}); | |
await (option && this.pieChart.setOption(option));//设置数据 |
环图
环图其实就是饼图的变形。将series中的radius设置为两个元素的数组即可,数值为内外环的半径比。
let option = { | |
title: { | |
text: ‘缺陷类型’, | |
top: “5%”, | |
left: ‘center’, | |
textStyle: { | |
color: “#fff” | |
} | |
}, | |
tooltip: { | |
trigger: ‘item’ | |
}, | |
series: [ | |
{ | |
type: ‘pie’, | |
animationDuration: 3000, | |
animationEasing: “cubicInOut”, | |
radius: [‘30%’, ‘60%’],//内外环半径比 | |
center: [“50%”, “60%”],//饼图位置 | |
label: { | |
color: “#fff” | |
}, | |
emphasis: { | |
label: { | |
show: true, | |
fontSize: ’20’, | |
fontWeight: ‘bold’ | |
} | |
}, | |
data: [] | |
} | |
] | |
}; | |
chartData.seriesData.forEach(s => { | |
option.series[0].data.push( | |
{ | |
value: s.value, | |
name: s.name, | |
itemStyle: { | |
color: new echarts.graphic.LinearGradient(1, 1, 1, 0, [ | |
{ | |
offset: 0, | |
color: this.dataVColor[0] | |
}, | |
{ | |
offset: 1, | |
color: “#fff” | |
} | |
]) | |
} | |
} | |
) | |
}); | |
await (option && this.ringChart.setOption(option));//设置数据 |
柱状图
柱状图也一样设置了渐变色。每个柱子后面的阴影是通过series中的showBackground设置的。
let option = { | |
title: { | |
text: ‘缺陷分布’, | |
top: “5%”, | |
left: ‘center’, | |
textStyle: { | |
color: “#fff” | |
} | |
}, | |
tooltip: { | |
trigger: ‘item’ | |
}, | |
grid: { | |
left: ‘3%’, | |
top: “20%”, | |
right: ‘4%’, | |
bottom: ‘3%’, | |
containLabel: true | |
}, | |
xAxis: { | |
type: ‘category’, | |
data: [], | |
axisLabel: { | |
color: “#fff”, | |
interval: 0, | |
rotate: 20, | |
}, | |
}, | |
yAxis: { | |
type: ‘value’, | |
axisLabel: { | |
color: “#fff”, | |
}, | |
splitLine: { | |
show: true,//网格设置 | |
lineStyle: { | |
color: “#70707033”, | |
width: 1, | |
type: “dotted”,//虚线 | |
}, | |
}, | |
}, | |
series: [ | |
{ | |
type: ‘bar’, | |
animationDuration: 3000, | |
animationEasing: “cubicInOut”, | |
showBackground: true, | |
label: { | |
color: “#fff” | |
}, | |
data: [], | |
color: new echarts.graphic.LinearGradient(0, 0, 1, 1, [ | |
{ | |
offset: 1, | |
color: this.dataVColor[0] | |
}, | |
{ | |
offset: 0, | |
color: “#ffffff” | |
} | |
]) | |
} | |
] | |
}; | |
option.xAxis.data = chartData.xAxisData; | |
chartData.seriesData.forEach(s => { | |
option.series[0].data.push(s); | |
}); | |
await (option && this.histogramChart.setOption(option));//设置数据 |
关系图
本来想用3d力导向图插件3d-force-graph的,但后面发现echarts自己也有类似的功能graph,直接设置series的layout即可,它有三种值:none(无任何布局),circular(环形布局)、force(力引导布局)。我用了circular,只有点,没有连线。
let option = { | |
title: { | |
text: ‘巡检工作待办’, | |
top: “1%”, | |
left: ‘center’, | |
textStyle: { | |
color: “#fff” | |
} | |
}, | |
// tooltip: { | |
// trigger: ‘item’ | |
// }, | |
series: [{ | |
type: ‘graph’, | |
layout: ‘circular’,//环形布局 | |
scaleLimit: { | |
min: .5,//缩放限制 | |
max: 2 | |
}, | |
zoom: .7, | |
roam: false, | |
label: { | |
normal: { | |
color: “#fff”, | |
show: true, | |
position: ‘inside’, | |
fontSize: 14, | |
fontStyle: ‘900’, | |
} | |
}, | |
data: [] | |
}] | |
}; | |
chartData.seriesData.forEach(s => { | |
option.series[0].data.push( | |
{ | |
name: s.name, | |
value: s.value, | |
symbolSize: Math.round((s.value / maxSymbolSize) * 100),//尺寸 | |
draggable: true,//允许拖拽 | |
itemStyle: { | |
color: new echarts.graphic.LinearGradient(0, 0, 1, 1, [ | |
{ | |
offset: 0, | |
color: this.dataVColor[0] | |
}, | |
{ | |
offset: 1, | |
color: “#fff” | |
} | |
]) | |
} | |
} | |
) | |
}); | |
await (option && this.atlasChart.setOption(option));//设置数据 |
轮播图
这是直接用了element的走马灯组件,自己添加图片即可。
<el-carousel :height=“carouselHeight” indicator-position=“outside” arrow=“never” :autoplay=“true” | |
:interval=“2000”> | |
<el-carousel-item v-for=“item in defectImgList” :key=“item.name”> | |
<img :src=“item.img” fit=“fill”> | |
</el-carousel-item> | |
</el-carousel> |
进度仪表图
这是具体配置:
let option = { | |
title: { | |
text: ‘\n{a|’ + chartData.name + ‘}’, | |
x: ‘center’, | |
y: ‘65%’, | |
bottom: “0”, | |
textStyle: { | |
color: “#ffffff”, | |
rich: { | |
a: { | |
fontSize: 15, | |
fontWeight: 900 | |
}, | |
} | |
} | |
}, | |
series: [ | |
{ | |
type: ‘gauge’, | |
radius: ‘86%’,//仪表盘半径 | |
center: [‘50%’, ‘45%’],//仪表盘位置 | |
splitNumber: 5, | |
animationDuration: 3000, | |
animationEasing: “cubicInOut”, | |
axisLine: { | |
lineStyle: { | |
width: 15, | |
color: [ | |
[1, new echarts.graphic.LinearGradient(1, 1, 0, 1, [ | |
{ | |
offset: 0, | |
color: this.dataVColor[1] | |
}, | |
{ | |
offset: 1, | |
color: “#aaa” | |
} | |
])] | |
] | |
} | |
}, | |
//指针 | |
pointer: { | |
width: 3, | |
length: ‘70%’, | |
}, | |
//小刻度 | |
axisTick: { | |
length: 5, | |
lineStyle: { | |
color: ‘#fff’, | |
width: 1 | |
} | |
}, | |
//大刻度 | |
splitLine: { | |
show: false, | |
length: 10, | |
lineStyle: { | |
color: ‘#fff’, | |
width: 2 | |
} | |
}, | |
//刻度标签 | |
axisLabel: { | |
color: ‘#fff’, | |
distance: 5, | |
fontSize: 8, | |
fontWeight: 900, | |
}, | |
detail: { | |
valueAnimation: false, | |
formatter: ‘{value}%’, | |
color: ‘#fff’, | |
fontSize: 15, | |
fontWeight: 900, | |
padding: [30, 0, 0, 0] | |
}, | |
data: [ | |
{ | |
value: chartData.value | |
} | |
] | |
} | |
] | |
}; | |
await (option && this.progressChart.setOption(option));//设置数据 |
图表信息框动画
图表信息框自己显示轮播,其实是利用的echartsAutoTooltip.js这个东西,东西不大,这是他的源码:
export const autoToolTip = (chart, chartOption, options) => { | |
var defaultOptions = { | |
interval: 2000, | |
loopSeries: false, | |
seriesIndex: 0, | |
updateData: null, | |
}; | |
if (!chart || !chartOption) { | |
return {}; | |
} | |
var dataIndex = 0; // 数据索引,初始化为-1,是为了判断是否是第一次执行 | |
var seriesIndex = 0; // 系列索引 | |
var timeTicket = 0; | |
var seriesLen = chartOption.series.length; // 系列个数 | |
var dataLen = 0; // 某个系列数据个数 | |
var chartType; // 系列类型 | |
var first = true; | |
// 不循环series时seriesIndex指定显示tooltip的系列,不指定默认为0,指定多个则默认为第一个 | |
// 循环series时seriesIndex指定循环的series,不指定则从0开始循环所有series,指定单个则相当于不循环,指定多个 | |
// 要不要添加开始series索引和开始的data索引? | |
if (options) { | |
options.interval = options.interval || defaultOptions.interval; | |
options.loopSeries = options.loopSeries || defaultOptions.loopSeries; | |
options.seriesIndex = options.seriesIndex || defaultOptions.seriesIndex; | |
options.updateData = options.updateData || defaultOptions.updateData; | |
} else { | |
options = defaultOptions; | |
} | |
// 如果设置的seriesIndex无效,则默认为0 | |
if (options.seriesIndex < 0 || options.seriesIndex >= seriesLen) { | |
seriesIndex = 0; | |
} else { | |
seriesIndex = options.seriesIndex; | |
} | |
function autoShowTip() { | |
function showTip() { | |
// 判断是否更新数据 | |
if ( | |
dataIndex === 0 && | |
!first && | |
typeof options.updateData === “function” | |
) { | |
options.updateData(); | |
chart.setOption(chartOption); | |
} | |
var series = chartOption.series; | |
chartType = series[seriesIndex].type; // 系列类型 | |
dataLen = series[seriesIndex].data.length; // 某个系列的数据个数 | |
var tipParams = { seriesIndex: seriesIndex }; | |
switch (chartType) { | |
case “map”: | |
case “pie”: | |
case “chord”: | |
tipParams.name = series[seriesIndex].data[dataIndex].name; | |
break; | |
case “radar”: // 雷达图 | |
tipParams.seriesIndex = seriesIndex; | |
tipParams.dataIndex = dataIndex; | |
break; | |
default: | |
tipParams.dataIndex = dataIndex; | |
break; | |
} | |
if ( | |
chartType === “pie” ||//饼图 | |
chartType === “radar” || | |
chartType === “map” || | |
chartType === “scatter” || | |
chartType === “line” ||//折线图 | |
chartType === “bar” ||//柱状图 | |
chartType === “graph” | |
) { | |
// 取消之前高亮的图形 | |
chart.dispatchAction({ | |
type: “downplay”, | |
seriesIndex: options.loopSeries | |
? seriesIndex === 0 | |
? seriesLen – 1 | |
: seriesIndex – 1 | |
: seriesIndex, | |
dataIndex: dataIndex === 0 ? dataLen – 1 : dataIndex – 1, | |
}); | |
// 高亮当前图形 | |
chart.dispatchAction({ | |
type: “highlight”, | |
seriesIndex: seriesIndex, | |
dataIndex: dataIndex, | |
}); | |
} | |
// 显示 tooltip | |
tipParams.type = “showTip”; | |
chart.dispatchAction(tipParams); | |
dataIndex = (dataIndex + 1) % dataLen; | |
if (options.loopSeries && dataIndex === 0 && !first) { | |
// 数据索引归0表示当前系列数据已经循环完 | |
seriesIndex = (seriesIndex + 1) % seriesLen; | |
} | |
first = false; | |
} | |
showTip(); | |
timeTicket = setInterval(showTip, options.interval); | |
} | |
// 关闭轮播 | |
function stopAutoShow() { | |
if (timeTicket) { | |
clearInterval(timeTicket); | |
timeTicket = 0; | |
if ( | |
chartType === “pie” || | |
chartType === “radar” || | |
chartType === “map” || | |
chartType === “scatter” || | |
chartType === “line” || | |
chartType === “bar” || | |
chartType === “graph” | |
) { | |
// 取消高亮的图形 | |
chart.dispatchAction({ | |
type: “downplay”, | |
seriesIndex: options.loopSeries | |
? seriesIndex === 0 | |
? seriesLen – 1 | |
: seriesIndex – 1 | |
: seriesIndex, | |
dataIndex: dataIndex === 0 ? dataLen – 1 : dataIndex – 1, | |
}); | |
} | |
} | |
} | |
var zRender = chart.getZr(); | |
function zRenderMouseMove(param) { | |
if (param.event) { | |
// 阻止canvas上的鼠标移动事件冒泡 | |
param.event.cancelBubble = true; | |
} | |
stopAutoShow(); | |
} | |
// 离开echarts图时恢复自动轮播 | |
function zRenderGlobalOut() { | |
if (!timeTicket) { | |
autoShowTip(); | |
} | |
} | |
// 鼠标在echarts图上时停止轮播 | |
chart.on(“mousemove”, stopAutoShow); | |
zRender.on(“mousemove”, zRenderMouseMove); | |
zRender.on(“globalout”, zRenderGlobalOut); | |
autoShowTip(); | |
return { | |
clearLoop: function () { | |
if (timeTicket) { | |
clearInterval(timeTicket); | |
timeTicket = 0; | |
} | |
chart.off(“mousemove”, stopAutoShow); | |
zRender.off(“mousemove”, zRenderMouseMove); | |
zRender.off(“globalout”, zRenderGlobalOut); | |
}, | |
}; | |
}; |
球体实现
球体是用了three.js来实现的,具体可以看我之前的疫情可视化文章(https://www.cnblogs.com/xi12/p/16690119.html),实现原理是一样,直接创建宇宙、绘制球体、球面打点,一气呵成。
数值动画
这几个数值是有递增动画的,我项目整体风格使用的dataV(http://datav.jiaminghi.com/guide/)实现的,dataV里面也有数值增加动画。但我没用那个,可以利用vue的数据响应式很方便即可实现。
<!–数字增加动画组件–> | |
<template> | |
<span class=“num-span” :data-time=“time” :data-value=“value”>{{ addNum }}</span> | |
</template> | |
<script> | |
export default { | |
props: { | |
//动画时间 | |
time: { | |
type: Number, | |
default: 2 | |
}, | |
//停止时的值 | |
value: { | |
type: Number, | |
default: 0 | |
}, | |
//千位的逗号 | |
thousandSign: { | |
type: Boolean, | |
default: () => false | |
} | |
}, | |
data() { | |
return { | |
oldValue: 0, | |
addNum: 0,//响应式的数值 | |
}; | |
}, | |
watch: { | |
value(val) { | |
this.oldValue = 0; | |
this.addNum = 0;//响应式的数值 | |
this.startAnimation();//值改变时开始动画 | |
} | |
}, | |
mounted() { | |
this.startAnimation(); | |
}, | |
methods: { | |
startAnimation() { | |
let value = this.value – this.oldValue; | |
let step = (value * 10) / (this.time * 100); | |
let current = 0; | |
let start = this.oldValue; | |
//定时器 | |
let t = setInterval(() => { | |
start += step; | |
if (start > value) { | |
clearInterval(t); | |
start = value; | |
t = null; | |
} | |
if (current === start) { | |
return; | |
} | |
current = Math.floor(start);//取整 | |
this.oldValue = current; | |
if (this.thousandSign) { | |
this.addNum = current.toString().replace(/(\d)(?=(?:\d{3}[+]?)+$)/g, ‘$1,’);//添加千位符 | |
} else { | |
this.addNum = current.toString();//无千位符 | |
} | |
}, 10) | |
} | |
}, | |
}; | |
</script> | |
<style scoped lang=‘scss’> | |
.num-span { | |
/*开启gpu加速*/ | |
transform: translateZ(0); | |
} | |
</style> |
特效背景
我比较懒,背景可不是我自己写的,我直接一个iframe,把别人代码一扔,他就出来了⊙﹏⊙∥。源码在我项目的这个路径下:
背景颜色可以通过Victor.js文件下的Victor方法里的diffuse值调节。
结语
感觉可视化项目难度不大(当然这只是对于我这种只会用轮子的懒人加缝合怪来说),无非就是熟练利用echarts配置,但麻烦的是效果需要自己仔细调节。