├── .gitignore ├── babel.config.js ├── public ├── bg.jpg ├── favicon.ico ├── index.html └── css │ └── normalize.min.css ├── src ├── assets │ └── logo.png ├── main.js ├── views │ ├── Home.vue │ ├── grid1.vue │ └── grid2.vue ├── router.js ├── libs │ └── util.js ├── App.vue └── components │ └── dashboard │ ├── charts │ ├── gauge │ │ └── index.vue │ ├── bar │ │ └── index.vue │ ├── scatter │ │ └── index.vue │ ├── line │ │ └── index.vue │ ├── dataset │ │ └── index.vue │ ├── pie │ │ └── index.vue │ ├── radar │ │ └── index.vue │ ├── graph │ │ └── index.vue │ └── bar2 │ │ └── index.vue │ ├── index.vue │ ├── index-grid2.vue │ └── index-grid.vue ├── package.json ├── LICENSE ├── vue.config.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /public/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MengFangui/vue-data-visualization/HEAD/public/bg.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MengFangui/vue-data-visualization/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MengFangui/vue-data-visualization/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import echarts from 'echarts' 5 | import { debounce } from './libs/util' 6 | Vue.prototype.$echarts = echarts 7 | Vue.prototype.$debounce = debounce 8 | 9 | Vue.config.productionTip = false 10 | 11 | new Vue({ 12 | router, 13 | render: h => h(App) 14 | }).$mount('#app') 15 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 23 | 24 | -------------------------------------------------------------------------------- /src/views/grid1.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 23 | 24 | -------------------------------------------------------------------------------- /src/views/grid2.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 23 | 24 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Home from './views/Home.vue' 4 | import grid1 from './views/grid1.vue' 5 | import grid2 from './views/grid2.vue' 6 | 7 | Vue.use(Router) 8 | 9 | export default new Router({ 10 | routes: [ 11 | { 12 | path: '/', 13 | name: 'home', 14 | component: Home 15 | }, 16 | { 17 | path: '/grid1', 18 | name: 'grid1', 19 | component: grid1 20 | }, 21 | { 22 | path: '/grid2', 23 | name: 'grid2', 24 | component: grid2 25 | } 26 | ] 27 | }) 28 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 数据可视化 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/libs/util.js: -------------------------------------------------------------------------------- 1 | // 函数防抖 2 | export function debounce(fn, wait, immediate) { 3 | let timer; 4 | return function () { 5 | if (timer) clearTimeout(timer); 6 | if (immediate) { 7 | // 如果已经执行过,不再执行 8 | var callNow = !timer; 9 | timer = setTimeout(() => { 10 | timer = null; 11 | }, wait) 12 | if (callNow) { 13 | fn.apply(this, arguments) 14 | } 15 | } else { 16 | timer = setTimeout(() => { 17 | fn.apply(this, arguments) 18 | }, wait); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-data-visualization", 3 | "version": "1.0.0", 4 | "private": false, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "core-js": "^2.6.5", 11 | "echarts": "^4.2.1", 12 | "vue": "^2.6.10", 13 | "vue-router": "^3.0.3" 14 | }, 15 | "devDependencies": { 16 | "@vue/cli-plugin-babel": "^3.10.0", 17 | "@vue/cli-service": "^3.10.0", 18 | "vue-template-compiler": "^2.6.10" 19 | }, 20 | "postcss": { 21 | "plugins": { 22 | "autoprefixer": {} 23 | } 24 | }, 25 | "browserslist": [ 26 | "> 1%", 27 | "last 2 versions" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 MengFangui 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const resolve = dir => { 4 | return path.join(__dirname, dir) 5 | } 6 | 7 | const BASE_URL = process.env.NODE_ENV === 'production' 8 | ? './' 9 | : './' 10 | 11 | module.exports = { 12 | baseUrl: BASE_URL, 13 | outputDir: 'dist', 14 | pages: { 15 | index: { 16 | // page 的入口 17 | entry: 'src/main.js', 18 | // 模板来源 19 | template: 'public/index.html', 20 | filename: 'index.html', 21 | title: "大屏可视化", 22 | // 在这个页面中包含的块,默认情况下会包含 23 | // 提取出来的通用 chunk 和 vendor chunk。 24 | chunks: ['chunk-vendors', 'chunk-common', 'index'] 25 | } 26 | }, 27 | lintOnSave: process.env.NODE_ENV === 'development', 28 | parallel: require('os').cpus().length > 1, 29 | chainWebpack: config => { 30 | config.resolve.alias 31 | .set('@', resolve('src')) 32 | .set('_c', resolve('src/components')) 33 | }, 34 | 35 | // 设为false打包时不生成.map文件 36 | productionSourceMap: false, 37 | devServer: { 38 | quiet: false, 39 | watchOptions: { 40 | poll: true 41 | }, 42 | // 在浏览器上全屏显示编译的errors或warnings。 43 | overlay: { 44 | warnings: false, 45 | errors: true 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/components/dashboard/charts/gauge/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 55 | -------------------------------------------------------------------------------- /src/components/dashboard/charts/bar/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 63 | -------------------------------------------------------------------------------- /public/css/normalize.min.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none} 2 | /*# sourceMappingURL=normalize.min.css.map */ -------------------------------------------------------------------------------- /src/components/dashboard/charts/scatter/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 73 | -------------------------------------------------------------------------------- /src/components/dashboard/charts/line/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 75 | -------------------------------------------------------------------------------- /src/components/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 34 | 35 | 55 | -------------------------------------------------------------------------------- /src/components/dashboard/index-grid2.vue: -------------------------------------------------------------------------------- 1 | 2 | 31 | 32 | 52 | -------------------------------------------------------------------------------- /src/components/dashboard/index-grid.vue: -------------------------------------------------------------------------------- 1 | 2 | 31 | 32 | 52 | -------------------------------------------------------------------------------- /src/components/dashboard/charts/dataset/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 73 | -------------------------------------------------------------------------------- /src/components/dashboard/charts/pie/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 77 | -------------------------------------------------------------------------------- /src/components/dashboard/charts/radar/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 81 | -------------------------------------------------------------------------------- /src/components/dashboard/charts/graph/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 139 | -------------------------------------------------------------------------------- /src/components/dashboard/charts/bar2/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 基于VUE、echarts和Grid的大屏数据可视化实现技术 2 | 3 | ## 简介 4 | 5 | 数据可视化技术是将把比较复杂、抽象的数据通过可视的技术以人们更易理解的形式展示出来,数据可视化技术促进了数据信息的传播和应用。 数据可视化技术是抽象数据的具象表达。 6 | 7 | 大屏数据可视化是以大屏为主要展示载体的数据可视化。目前市场上大屏设备有1280*768的笔记本,也有7680*4320的8K显示屏,设备分辨率宽泛。“面积大、炫酷动效、丰富色彩、可交互”是大屏数据可视化的特点。大屏数据可视化技术主要应用场景有:信息展示、数据分析和监控预警三类。 8 | 9 | 本文阐述基于VUE.js、echarts图表和Grid布局的大屏数据可视化技术。 10 | 11 | ## 技术栈 12 | 13 | * vue 14 | * echarts 15 | * Grid布局 16 | 17 | ## echarts图标库使用 18 | 19 | echarts官网:https://www.echartsjs.com/zh/index.html 20 | 21 | 1. echarts导入VUE项目 22 | 23 | ``` 24 | import echarts from 'echarts' 25 | Vue.prototype.$echarts = echarts 26 | ``` 27 | 28 | 2. echarts 使用性能优化 29 | 30 | 当window resize时,echart需要重新绘制。若window resize实时对echart重绘,页面会卡顿,页面性能会降低,因此需要考虑dom渲染的性能,加入函数防抖。 下面简单解释一下函数防抖和函数节流。 31 | 32 | ### 函数节流throttle 33 | 34 | 函数节流throttle通俗解释:假设你正在乘电梯上楼,当电梯门关闭之前发现有人也要乘电梯,礼貌起见,你会按下开门开关,然后等他进电梯; 但是,你是个没耐心的人,你最多只会等待电梯停留一分钟; 在这一分钟内,你会开门让别人进来,但是过了一分钟之后,你就会关门,让电梯上楼。 35 | 36 | 所以函数节流throttle的作用是,预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新的时间周期。 37 | 38 | 函数节流throttle应用:在指定时间,事件最多触发一次。 39 | 40 | ### 函数防抖debounce 41 | 42 | 函数防抖debounce通俗解释假设你正在乘电梯上楼,当电梯门关闭之前发现有人也要乘电梯,礼貌起见,你会按下开门开关,然后等他进电梯; 如果在电梯门关闭之前,又有人来了,你会继续开门; 这样一直进行下去,你可能需要等待几分钟,最终没人进电梯了,才会关闭电梯门,然后上楼。 43 | 44 | 所以函数防抖debounce的作用是,当调用动作触发一段时间后,才会执行该动作,若在这段时间间隔内又调用此动作则将重新计算时间间隔。 45 | 46 | 函数防抖debounce应用:百度首页的搜索按钮。 47 | 48 | 函数节流throttle和函数防抖debounce在函数式编程,如lodash库都有实现。 49 | 50 | 函数防抖debounce的封装: 51 | 52 | ``` 53 | // 函数防抖 54 | export function debounce(fn, wait, immediate) { 55 | let timer; 56 | return function () { 57 | if (timer) clearTimeout(timer); 58 | if (immediate) { 59 | // 如果已经执行过,不再执行 60 | var callNow = !timer; 61 | timer = setTimeout(() => { 62 | timer = null; 63 | }, wait) 64 | if (callNow) { 65 | fn.apply(this, arguments) 66 | } 67 | } else { 68 | timer = setTimeout(() => { 69 | fn.apply(this, arguments) 70 | }, wait); 71 | } 72 | } 73 | } 74 | ``` 75 | 76 | Vue项目main.js中引入函数防抖: 77 | 78 | ``` 79 | import { debounce } from './libs/util' 80 | Vue.prototype.$debounce = debounce 81 | 82 | ``` 83 | 84 | echarts图表组件初始化函数防抖: 85 | 86 | ``` 87 | mounted() { 88 | // 窗口改变时重新绘制 89 | window.addEventListener("resize", this.$debounce(myChart.resize, 500)); 90 | } 91 | ``` 92 | 93 | ## echarts 图表使用时细节优化处理 94 | 95 | 1. 图例区域太大导致遮挡住图表 96 | 97 | 在option中设置grid,主要设置top值。 98 | 99 | ``` 100 | grid: { 101 | left: "3%", 102 | right: "3%", 103 | top: "3%", 104 | containLabel: true 105 | }, 106 | ``` 107 | 108 | 2. 防止坐标轴标签显示空间不全 109 | 110 | 在option中设置interval和rotate: 111 | 112 | ``` 113 | axisLabel: { 114 | // 防止坐标轴标签显示空间不全 115 | rotate: -30 116 | }, 117 | // 防止坐标轴标签显示空间不全 118 | interval: 0 119 | ``` 120 | 121 | ## Grid布局 122 | 123 | 首先说明的是grid布局不是bootstrap框架,element ui等UI的栅格布局。目前CSS布局方式有table,浮动,定位,flex和grid布局。 124 | 125 | Grid布局说明:https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout 126 | 127 | Grid和flex布局十分相似,但却有极大的不同。Flex 布局是一维布局,即轴线布局,只能指定"项目"针对轴线的位置。Grid 布局则是将容器划分成"行"和"列",产生单元格,然后指定"项目所在"的单元格,因此是二维布局。 128 | 在大屏数据可视化技术需要解决屏幕的响应式布局。常见的响应式布局有媒体查询,flex布局、百分比布局和Grid布局。相比于媒体查询,flex布局和百分比布局,Grid布局在实现大屏数据可视化技术响应式布局更便捷。有一点需要注意的是Grid存在浏览器兼容,请参考:https://caniuse.com/#search=grid,即在IE浏览器不兼容,对于Edge、Fifefox和Chrome主流浏览器兼容性良好。 129 | 130 | 1. Grid布局三行三列布局核心配置示例 131 | 132 | ``` 133 | /* 网格布局 */ 134 | display: grid; 135 | grid-template-columns: 29% 40% 29%; 136 | grid-template-rows: 32.333% 33.333% 32.333%; 137 | /* 行与行的间隔(行间距) */ 138 | /* grid-row-gap: 20px; */ 139 | /* 列与列的间隔(列间距) */ 140 | /* grid-column-gap: 20px; */ 141 | /* 行间距和列间距均是1% */ 142 | grid-gap: 1% 1%; 143 | ``` 144 | 145 | 2. Grid布局中行合并示例配置 146 | 147 | html: 148 | 149 | ``` 150 | 151 |
152 | 153 |
154 | 155 | 156 |
157 |
158 | 159 |
160 |
161 | 162 |
163 |
164 | 165 |
166 |
167 | 168 |
169 |
170 | 171 |
172 |
173 | 174 |
175 |
176 | 177 |
178 | 179 |
180 | 181 | ``` 182 | css: 183 | 184 | ``` 185 | 186 | 212 | 213 | ``` 214 | 215 | 2. Grid布局中列合并示例配置 216 | 217 | html: 218 | 219 | ``` 220 | 221 |
222 | 223 |
224 | 225 | 226 |
227 |
228 | 229 |
230 |
231 | 232 |
233 |
234 | 235 |
236 |
237 | 238 |
239 |
240 | 241 |
242 |
243 | 244 |
245 |
246 | 247 |
248 | 249 |
250 | 251 | ``` 252 | css: 253 | 254 | ``` 255 | 256 | 282 | 283 | ``` 284 | 285 | 4. Grid布局左侧固定,右侧自适应布局示例配置 286 | 287 | ``` 288 | 289 | display: grid; 290 | /* 左侧固定,右侧自适应布局 */ 291 | grid-template-columns: 150px 1fr; 292 | /* grid-template-columns: 150px auto; */ 293 | 294 | ``` 295 | 296 | 5. Grid布局repeat属性 297 | 298 | 将页面水平和垂直方向均分为100分。 299 | 300 | ``` 301 | 302 | .grid { 303 | 304 | height: 100%; 305 | display: grid; 306 | grid-template-columns: repeat(100, 1%); 307 | grid-template-rows: repeat(100, 1%); 308 | 309 | } 310 | 311 | .item { 312 | 313 | border: 1px solid red; 314 | 315 | } 316 | 317 | .item-1 { 318 | /* item-1 dom元素在水平和垂直页面占据左上角的20% */ 319 | 320 | grid-column-start: 1; 321 | grid-column-end: 21; 322 | grid-row-start: 1; 323 | grid-row-end: 21; 324 | 325 | } 326 | 327 | ``` 328 | 329 | grid布局参考[grid](http://www.ruanyifeng.com/blog/2019/03/grid-layout-tutorial.html) 330 | 331 | ## VUe项目引入normalize 库初始化浏览器默认样式 332 | 333 | ``` 334 | 335 | 338 | ``` 339 | 340 | ## 项目代码:https://github.com/MengFangui/vue-data-visualization 341 | 342 | ## 效果图 343 | 344 | ![image](https://note.youdao.com/yws/public/resource/37560477869e49d436491028e210a537/xmlnote/4459220E3F6E42D9AFE912F447572348/75CCAAD6133F4D11BE615C78C3030AB5/16955) 345 | 346 | 347 | 348 | ![image](https://note.youdao.com/yws/public/resource/37560477869e49d436491028e210a537/xmlnote/4459220E3F6E42D9AFE912F447572348/1D407F8E78C4493EB1AD095501E8DAF8/16953) 349 | 350 | ![image](https://note.youdao.com/yws/public/resource/37560477869e49d436491028e210a537/xmlnote/4459220E3F6E42D9AFE912F447572348/347ED3EBEF9A4C9E9F1A1D9DA0B98A2E/16954) 351 | 352 | --------------------------------------------------------------------------------