背景
我们经常在网上看到各种绚烂的数据展示仪表板,例如:
那我们如何实现这种效果了?
本文将讲叙如何利用vue-grid-layout和echarts制作数据可视化仪表板。
这是最终的效果图,
工具
vue-grid-layout:基于vue的栅格拖动布局组件。
echarts:一款用于图表可视化的插件,可以用来制作各种图表。
JavaScript:
代码
说明:以下代码只供参考,由于进行了一部分删减调整,直接拷贝肯定是不能够运行的。
<!DOCTYPE html>
<html>
<head>
<title>仪表板</title>
<script src="./js/vue.js" type="text/javascript"></script>
<script src="./js/vue-grid-layout.umd.min.js" type="text/javascript"></script>
<script src="./js/echarts.min.js"></script>
<script src="./js/chart-style.js"></script>
<script>
var board = new Vue({
el: '#dashboard',
data: {
editFlag: true, //是否是编辑态
layout: [], //初始化卡片数组
layoutMap: [], //卡片二维地图
resizeTimer: null,
layoutColNum: 12 //列
},
methods:{
layoutReadyEvent:function(){
this.initBoard();
this.layoutMap = this.genereatePlaneArr(this.layout);
},
// 当插件内容布局发生变化后 获取现在的二维地图树
layoutUpdatedEvent: function() {
// console.log("Updated");
this.layoutMap = this.genereatePlaneArr(this.layout);
},
//调整卡片大小
resizedEvent:function (index) {
if (this.resizeTimer) clearTimeout(this.resizeTimer);
var _this = this;
this.resizeTimer = setTimeout(function () {
var mychart = _this.layout[index]['myChart'];
mychart.resize();
}, 100);
},
//调整卡片大小
windowResizeEvent:function(){
if (this.resizeTimer) clearTimeout(this.resizeTimer);
var _this = this;
this.resizeTimer = setTimeout(function () {
for(var i = 0; i<_this.layout.length; i++){
var mychart = _this.layout[i]['myChart'];
mychart.resize();
}
}, 100);
},
//仪表板初始化
initBoard:function(){
for(var i = 0; i<this.layout.length; i++){
this.queryChartData(i, i*200);
}
},
//删除卡片
deleteChartUnit:function (index) {
this.layout.splice(index,1);
},
//修改卡片参数
editChartUnit:function(index){
// console.log(index);
this.layout[index].paramList = newParamList;
this.layout[index].showFlag = "0";
this.$nextTick(() => {this.queryChartData(index, 0);});
},
//添加卡片
addChartUnit:function(){
var item = {};
item.chartUnitCname = "图一";
item.chartType = "line";
item.chartData = [];
item.showFlag = '0';
item.x = 0;
item.y = 0;
item.w = 6;
item.h = 4;
item.i = uuid(32, 36);
var itemW = item.w;
var itemH = item.h;
var addItem = item;
if(this.layoutMap.length){
// console.log(this.layoutMap.length);
for(let r = 0 , rLen =this.layoutMap.length ; r < rLen; r++){
for(let c = 0; c <= (this.layoutColNum-itemW); c++){
let res = this.regionalTest(c, r, itemW,rLen>(r+itemH)?itemH:rLen-r );
if(res.result){
// 更新添加数据内容
addItem.x = res.x;
addItem.y = res.y;
c = this.layoutColNum+1;
r = rLen+1;
}else{
c = res.offsetX;
}
}
}
}
// 更新二维数组地图
for(let itemR = 0 ; itemR < itemH ; itemR++){
for(let itemC = 0 ; itemC < itemW ; itemC++){
// 如果没有该行,初始化
if(!this.layoutMap[addItem.y+itemR]){
this.layoutMap[addItem.y+itemR] = new Array(this.layoutColNum);
for(let i = 0 ;i < this.layoutColNum ; i++){
this.layoutMap[addItem.y+itemR][i] = 0;
}
}
// 标记点
this.layoutMap[addItem.y+itemR][addItem.x+itemC] = 1;
}
}
// console.log(this.layoutMap);
// 添加数据
this.layout.push(addItem);
this.$nextTick(() => {this.queryChartData(this.layout.length-1, 0);});
},
//获取后台数据
queryChartData : function (index, time) {
setTimeout( () => {
//防止同时请求对后台造成压力
this.layout[index].chartData = {"dataX":['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
"dataY":[150, 230, 224, 218, 135, 147, 260]};
this.drawChart(index);
}, time);
},
//绘图-echarts
drawChart:function (index) {
var elementId = this.layout[index].i;
var chartData = this.layout[index].chartData;
if(chartData.status == 1 ){
this.layout[index].showFlag = "1";
this.$nextTick(() => {
var chartType = this.layout[index].chartType;
var myChart = echarts.init(document.getElementById(elementId));
myChart.clear();
if(chartType === 'line'){
var option = {
xAxis: {
type: 'category',
data: chartData.dataX
},
yAxis: {
type: 'value'
},
series: [
{
type: 'line'
}
]
};
//设置样式
myChart.setOption(option);
//设置数据
myChart.setOption({
series: {
data: chartData.dataY
}
});
}
this.layout[index]['myChart'] = myChart;
})
}else {
this.layout[index].showFlag = "-1";
}
},
//卡片地图
regionalTest: function (x,y,w,h) {
// 定义返回 x,y 偏移 及 是否有空位置
let offsetX = 0,offsetY = 0,res = true;
// 按区域循环检测 二维数组地图
for(let r = 0; r < w ;r++){
for(let c = 0; c <= h ;c++){
let point = this.layoutMap[y+r]?this.layoutMap[y+r][x+c]:0;
// 如该点被占据 记录偏移值
if(point===1){
res = false;
offsetX = offsetX>(x+c)?offsetX:x+c;
offsetY = offsetY>(y+r)?offsetY:y+r;
}
}
}
return {
result: res,
offsetX: offsetX,
x: x,
y: y
};
},
//卡片地图
genereatePlaneArr: function (data) {
var map = [];
if(Array.isArray(data)){
for(var i = 0; i<data.length; i ++){
var one = data[i];
// 循环行
for(var r = one.y ; r < ( one.y + one.h ) ; r++){
// 循环列
for(var c = one.x ; c < ( one.x + one.w) ; c++){
// 检修当前行是否存在
if(!map[r]){
map[r] = new Array(this.layoutColNum);
for(let i = 0 ; i < this.layoutColNum ; i++){
map[r][i] = 0;
}
}
// 占据为1
map[r][c] = 1;
}
}
}
}
return map;
},
//比较卡片参数是否发生调整
compareParams: function (oldParamList, newParamList ) {
if(oldParamList.length == 0){
return true;
}else {
for (var i=0; i<oldParamList.length; i++){
if(oldParamList[i].value != newParamList[i].value){
return false;
}
}
return true;
}
}
}
});
//页面窗口改变图大小调整
window.onresize = function(){
board.windowResizeEvent();
};
//uuid
function uuid(len, radix) {
var chars = '0123456789abcdefghijklmnopqrstuvwxyz'.split( '');
var uuid = [], i;
radix = radix || chars.length;
if (len) {
// Compact form
for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix];
} else {
// rfc4122, version 4 form
var r;
// rfc4122 requires these characters
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-' ;
uuid[14] = '4' ;
// Fill in random data. At i==19 set the high bits of clock sequence as
// per rfc4122, sec. 4.1.5
for (i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | Math.random()*16;
uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
}
}
}
return uuid.join( '' );
}
</script>
<style>
.isEdit {
background: #efefef;
background-image: linear-gradient(0deg, #f8f8f8 10px, transparent 0.1em), linear-gradient(90deg, #f8f8f8 10px, transparent 0.1em);
background-size: calc(8.33333% - 2px) 100px;
background-position-y: 10px;
background-attachment: local, scroll;
overflow-y: scroll;
}
.dashboard_content {
align-self: stretch;
height: 0;
flex: 1 1 auto;
overflow-y: auto;
overflow-x: hidden;
position: relative;
}
.vue-grid-layout > div {
box-shadow: 1px 1px 5px #e3e3e3;
background: #FFF;
}
.card {
width: 100%;
height: 100%;
display: flex;
flex-flow: column nowrap;
}
.card-top {
margin-top: 10px;
font-size: 16px;
flex-basis: 20px;
display: flex;
}
.card-title {
float: left;
margin-left: 8px;
margin-right: 8px;
flex: 1;
}
.card-top-icon {
cursor: pointer;
width: 20px;
margin-right: 8px;
}
.card-title-content {
white-space: nowrap;
overflow: hidden;
display: inline-block;
text-overflow: ellipsis;
}
.card-body {
width: 100%;
flex: 1;
}
.loading {
width: 100px;
height: 50px;
margin: 0 auto;
margin-top: 100px;
}
.loading span {
display: inline-block;
width: 15px;
height: 15px;
margin-right: 5px;
border-radius: 50%;
background: lightblue;
-webkit-animation: load 1.04s ease infinite;
}
.loading span:last-child {
margin-right: 0px;
}
@-webkit-keyframes load {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.loading span:nth-child(1) {
-webkit-animation-delay: 0.13s;
}
.loading span:nth-child(2) {
-webkit-animation-delay: 0.26s;
}
.loading span:nth-child(3) {
-webkit-animation-delay: 0.39s;
}
.loading span:nth-child(4) {
-webkit-animation-delay: 0.52s;
}
.loading span:nth-child(5) {
-webkit-animation-delay: 0.65s;
}
</style>
</head>
<body>
<div style="width: 100%;height:100%;">
<div id="dashboard" style="height: 100%;display: none;" v-bind:class="{ isEdit: editFlag }">
<grid-layout
:layout.sync="layout"
:col-num="12"
:row-height="90"
:is-draggable="editFlag"
:is-resizable="editFlag"
:is-mirrored="false"
:vertical-compact="true"
:margin="[10, 10]"
:use-css-transforms="true"
@layout-updated="layoutUpdatedEvent"
@layout-ready="layoutReadyEvent">
<grid-item v-for="(item,index) in layout"
:x="item.x"
:y="item.y"
:w="item.w"
:h="item.h"
:i="item.i"
:max-w="12"
:key="item.i"
@resized="resizedEvent(index)">
<div class="card">
<div class="card-top">
<div class="card-title card-title-content"> {{item.chartUnitCname}}</div>
<div v-if="editFlag" class="card-top-icon" @click="editChartUnit(index)">
<i class="fa fa-cog" aria-hidden="true" title="编辑"
style="margin-right: 10px;"></i>
</div>
<div v-if="editFlag" class="card-top-icon" @click="deleteChartUnit(index)"
style='float:right;cursor:pointer'>
<i class="fa fa-trash" aria-hidden="true" title="删除"
style="margin-right: 10px;"></i>
</div>
</div>
<div class="card-body">
<div v-show="item.showFlag=='0'" class="loading">
<span></span><span></span><span></span><span></span><span></span></div>
<div v-show="item.showFlag=='1'" :id="item.i"
style="width: 100%;height: 100%"></div>
<div v-show="item.showFlag=='-1'" style="text-align: center;font-size: 14px;">
{{item.msg}}
</div>
</div>
</div>
</grid-item>
</grid-layout>
</div>
</div>
</body>
</html>
其它
卡片地图位置信息维护参考了网上前辈代码,找了好久没有找到该篇文章,就没贴文章链接了。。。