前言
这段时间一直在做可视化,在我的项目中有一部分是电力巡检的数据可视化。其中的数据看板比较简单,我将其单独抽离出来形成一个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: [
[140, 232, 101, 264, 90, 340, 250],
[120, 282, 111, 234, 220, 340, 310],
[320, 132, 201, 334, 190, 130, 220]
],//折线图数据
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: [
[14, 22, 100, 164, 200, 140, 250],
[120, 22, 111, 24, 220, 240, 310],
[10, 132, 201, 334, 190, 30, 220]
],//折线图数据
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: [
[104, 122, 200, 164, 20, 140, 250],
[220, 22, 111, 24, 120, 40, 10],
[130, 32, 201, 34, 190, 30, 200]
],//折线图数据
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="https://www.cnblogs.com/xi12/archive/2022/12/15/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配置,但麻烦的是效果需要自己仔细调节。