背景
我们经常在网上看到各种绚烂的数据展示仪表板,例如:
那我们如何实现这种效果了?
本文将讲叙如何利用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>
复制
其它
卡片地图位置信息维护参考了网上前辈代码,找了好久没有找到该篇文章,就没贴文章链接了。。。