ECharts是著名的开源JavaScript图表库,支持20+图表类型,常用的有折线图、柱状图和饼图,这里是基于vue2对其进行封装的一种实践。在此之前建议通读以下篇章:
封装的原因:
- 直接使用存在冗余,与业务逻辑耦合,增加复杂度。
使用ECharts的基本步骤是echarts.init(dom)生成实例,然后setOption,即可渲染出不同的图表,在图表容器销毁之后,调用 echartsInstance.dispose 销毁实例。这是一个完整的步骤,但在真实的业务环境中,一个页面会有很多种不同的图表,此时我们可能需要去关注随业务变化带来的图表的创建、setOption和销毁,并会在代码中存在大量的option,实际上这会增加组件或页面的复杂度,会存在代码冗余的问题,图表的展示逻辑与业务逻辑耦合也会增加复杂度;
- 直接使用粒度较大,不方便复用。
封装的思路或需求
- ECharts 按需导入,减少打包体积
- 屏幕尺寸发生变化自动缩放
- 自带loading
- 无数据缺省设计为插槽(待确认)或默认缺省提示
- 使用ref代替id的dom(防止id重复性)
- 保持简单、可读性与扩展性,不过度封装
- 将不同页面的不同样式的option公共配置抽离为文件,具体使用时,仅更新必要的数据,做到基本的样式与数据分离配置(动态配置与静态配置分离)。
- 封装组件的权衡:开放性 & 可维护性 & 适合真实需求 > 性能
目录结构、代码与示例
不同数据粒度的图表更新:
图表实例新建和销毁
kotlin
this.chart = this.$echarts.init(this.$refs.chart, "", {
renderer: this.type
}) //创建一个 ECharts 实例
this.chart.setOption(this.chartOption) //设置图表实例的配置项以及数据,万能接口,
//所有参数和数据的修改都可以通过 setOption 完成,ECharts 会合并新的参数和数据,然后刷新图表
kotlin
this.chart.dispose() //销毁实例,销毁后实例无法再被使用。
图表类型改变 & 数据改变 (实例不用销毁)
kotlin
this.chart.clear() //清空当前实例,会移除实例中所有的组件和图表。
this.chart.setOption(this.chartOption) //更新视图
图表类型不变 & 仅改变展示数据 或 其他组件(legend、tooltip等)(可能随特定数据改变,尽可能在配置函数内实现则后续无需更新)
kotlin
// 方式1: 修改整体option后重新setOption
this.chart.setOption(this.chartOption)
// 方式2: 只需给setOption传递要修改的属性
this.chart.setOption({xAxis: {type: "category",data: [],}})
//传入一个option,其中仅添加需要修改的属性,ECharts会与之前的进行自动合并,更多参数见文档
所有数据不变,所在节点宽高变化,此时重新渲染,仅改变图表尺寸
kotlin
this.chart.resize() //场景1.改变图表**尺寸,在容器大小发生改变时需要手动调用。
//场景2.初始隐藏的标签在初始化图表的时候因为获取不到容器的实际高宽,可能会绘制失败,
//因此在切换到该标签页时需要手动调用 resize** 方法获取正确的高宽并且刷新画布
渲染图表的过程
全量更新
-
组装option
- 通过函数获取样式配置
- 通过接口获取数据,添加option中的数据字段
-
组件内部监听option修改自动setOption
增量更新
-
通过接口获取变化的数据,
-
判断option是否存在
- 存在则不需要再通过函数获取样式配置
- 直接更新需要改变的数据字段
-
组件内部监听option修改自动setOption
增量更新的另一种思路:
-
通过接口获取变化的数据
-
判断option是否存在
- 组装变化的option
- 通过组件的setOption来直接渲染更新图表
图表的三种状态:
-
加载
初始化图表时,我们有 无数据的只有样式配置的option,此时已经可以渲染出来,但实际图表应该展示loading,实现方式:
a. 传参数,表示是否业务数据已就绪,可以使用ECharts自带的loading或者组件(自己写的或组件库提供的); 如果使用组件,问题在于如何让ECharts不渲染,当ECharts实例被初始化但无option时,会占位但不会渲染canvas,还有一种方式就是封装一个更小的chart组件,但如果更新数据时都需要v-if,就会引起ECharts实例的频繁创建和释放,这里的问题在于如何在chart内部既不会频繁创建EChart实例也能够支持通过loading组件的方式实现加载状态
-
展示
-
无数据缺省展示
目录结构
安装: npm i echarts -S
npm i resize-detector -S
less
|- components
|- chart-view //图表组件
|- index.vue
|- options //所有页面图表配置,以页面作为文件夹
|- sales-statistics
|- chart1-bar.js //具体某个图表配置,详见注1
|- chart2-pie.js
|- plugins
|- charts.js //按需导入
|- view
|- sales
|- sales-statistics.vue //销售统计页面
|- main.js //vue主文件
注1: 这里个人命名规则为'chart序号-图表类型.js': 如sales-statistics/chart1-pie,就表示销售统计页面的第一个图表,类型为柱状图。 序号规则一般为从左往右、从上到下,方便快速按页面和位置找到对应图表配置文件。
相关代码
按需导入echarts
javascript
// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口
import * as echarts from "echarts/core"
// 引入常用的图表,图表后缀都为 Chart
import { BarChart, LineChart, ScatterChart, PieChart } from "echarts/charts"
// 引入提示框,标题,直角坐标系,数据集,内置数据转换器组件,组件后缀都为 Component
import {
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
LegendComponent,
DataZoomComponent
} from "echarts/components"
// 标签自动布局,全局过渡动画等特性
import { LabelLayout, UniversalTransition } from "echarts/features"
// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
import { CanvasRenderer } from "echarts/renderers"
// 注册必须的组件
echarts.use([
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
LegendComponent,
DataZoomComponent,
BarChart,
PieChart,
LineChart,
ScatterChart,
LabelLayout,
UniversalTransition,
CanvasRenderer
])
// 导出 echarts
export default echarts
挂载全局方法,注册全局组件
javascript
import Vue from "vue"
import echarts from "./plugins/charts"
Vue.prototype.$echarts = echarts
import ChartView from "@/components/chart-view/index.vue"
Vue.component(ChartView.name, ChartView) //注册全局组件
new Vue({
router,
store,
render: h => h(App)
}).$mount("#app")
组件实现 与 使用示例
xml
<template>
<div class="chart">
<div ref="chart" :class="{ 'opacity-0': loading || !hasData }" :style="{ height: height, width: width }" />
<div v-if="loading" class="loading-box">
<!-- 1使用相关组件库的loading -->
<van-loading />
<!-- 2使用自定义的Loading -->
<!-- <div class="loading"></div> -->
</div>
<div v-if="!loading && !hasData" class="empty-box">
<slot v-if="$slots.default"></slot>
<div v-else class="empty-tip">
<img src="" alt="" class="empty-bg" />
<div class="tip">暂无数据</div>
</div>
</div>
</div>
</template>
<script>
import { Loading } from "vant"
import { addListener, removeListener } from "resize-detector" //兼容性更好的resize监听
import { debounce } from "./utils"
export default {
name: "ChartView",
components: { VanLoading: Loading },
props: {
width: {
type: String,
default: "100%"
},
height: {
type: String,
default: "350px"
},
autoResize: {
type: Boolean,
default: true
},
chartOption: {
type: Object,
required: true
},
type: {
type: String,
default: "canvas"
},
hasData: {
type: Boolean,
default: true
},
loading: {
type: Boolean,
default: false
},
flushClear:{
type: Boolean,
default: true
}
},
data() {
return {
chart: null
}
},
watch: {
chartOption: {
deep: true,
handler(newVal) {
this.setOptions(newVal)
}
}
},
mounted() {
this.initChart()
if (this.autoResize) {
addListener(this.$el, this.resizeHandler)
}
},
beforeDestroy() {
if (!this.chart) {
return
}
if (this.autoResize) {
removeListener(this.$el, this.resizeHandler)
}
this.chart.dispose()
this.chart = null
},
methods: {
resizeHandler: debounce(function() {
//this存在
this.chart.resize()
}),
initChart() {
this.chart = this.$echarts.init(this.$refs.chart, "", {
renderer: this.type
})
this.chart.setOption(this.chartOption)
this.chart.on("click", this.handleClick)
},
handleClick(params) {
this.$emit("click", params)
},
setOptions(option) {
this.flushClear && this.clearChart()
this.resizeHandler()
if (this.chart) {
this.chart.setOption(option)
}
},
refresh() {
this.setOptions(this.chartOption)
},
clearChart() {
this.chart && this.chart.clear()
},
setOption(option){
if (this.chart) {
this.chart.setOption(option)
}
}
}
}
</script>
<style scope lang="scss">
.chart {
height: 100%;
position: relative;
}
.loading-box {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.loading {
width: 30px;
height: 30px;
border: 2px solid #000;
border-top-color: transparent;
border-radius: 100%;
animation: circle infinite 0.75s linear;
}
@keyframes circle {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
}
.empty-box {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.opacity-0 {
opacity: 0;
}
</style>
css
export const getChart1BarOption = rawData => {
const defaultConfig = {
tooltip: {
trigger: "item"
},
color: "#FAD632",
grid: {
top: 0,
bottom: 30,
left: 80
},
yAxis: {
type: "category",
data: [],
axisTick: {
show: false
},
axisLabel: {
color: "#999"
},
axisLine: {
lineStyle: {
color: "#ccc"
}
}
},
xAxis: {
type: "value",
axisLabel: {
color: "#999",
formatter: function(value) {
if (value % 1 !== 0) {
return ""
}
return value
}
},
splitLine: {
lineStyle: {
color: "#eee"
}
}
},
series: [
{
data: [],
type: "bar",
barWidth: "9px"
}
]
}
return defaultConfig
}
xml
<template>
<div class="page-sales-statistics">
<chart-view
:chart-option="chart1_option"
:has-data="chart1_has_data"
:auto-resize="true"
class="chart1-bar"
height="100%"
>
</chart-view>
</div>
</template>
<script>
import {getChart1BarOption} from '@/components/options/chart1-bar'
export default {
name: "SalesStatistics",
data() {
return {
chart1_has_data: false
chart1_option: null,
}
},
mounted(){
this.updateChart1Data()
},
methods:{
// 更新图表1数据
updateChart1Data(){
try {
const res = setTimeout(()=>{
return [{key:'北京',value:10},{key:'上海',value:9},{key:'广州',value:8}]
},)
this.chart_has_data = true;
// 数组处理与组装,在逻辑上尽量动静分离,静态样式和动态数据分离。
this.chart1_option = getChart1BarOption() || this.chart1_option
this.chart1_option.series[0].data = res.map(i => i.value / 100)
this.chart1_option.yAxis.data = res.map(i => i.key)
// 数据量很庞大时结合组件方式,采用增量更新
this.$nextTick(() => {
this.$refs.chart3bar.setOption({
series: [
{
data: res.map(i => i.value / 100),
type: "bar",
barWidth: "9px"
}
]
})
})
} catch (error) {
console.log('请求异常', error);
}
}
}
}
</script>
提示
- 通过v-if,达到组件级的渲染粒度控制;(1)
- 组件默认每次重新渲染为:图表类型改变 & 数据改变 (2)
- 通过flushClear参数和调用组件setOption达到上述3中方式1和方式2的两种渲染粒度控制;
- resize后自动setOption,以达到4的渲染粒度控制
相关规则
- 通常情况下,在每次调用 setOption 方法之前不需要调用 clear 方法。只有在需要清空现有图表或切换图表类型时,才需要使用 clear 方法。
- 示例网站见参考索引23
参考
- 在项目中引入 ECharts - 入门篇 - Handbook - Apache ECharts
- 大屏图表,ECharts 从"熟练"到入门 - 掘金
- 图表容器及大小 - 概念篇 - Handbook - Apache ECharts
- Documentation - Apache ECharts
- vue项目中封装echarts的比较优雅的方式 - 掘金
- 数据大屏:聊聊常见可视化大屏的产品实现 | 人人都是产品经理
- 数据可视化在移动端的应用 | 人人都是产品经理
- www.youtube.com/watch?v=C3M...
- 这样封装echarts超级简单好用 - 掘金
- vue3中echarts组件的最佳封装形式 - 掘金
- Echarts 在 Vue 中的最佳实践 - 掘金
- 在vue项目中封装echarts的正确姿势_vue中封装echarts组件mixin_JessicaLilyAn的博客-CSDN博客
- Vue项目中全局引入和按需引入Echarts - 掘金
- 我不允许有人不会封装 ECharts - 掘金
- Vue项目中如何优雅地封装Echarts - 掘金
- GitHub - vueblocks/ve-charts: 📈 ECharts 4.x for Vue.js 2.x. | 📈 ECharts 5.x for Vue.js 3.x in next version.
- echarts 快速入门(教学篇) - 掘金
- echarts性能优化 - 掘金
- echarts 使用小结
- React数据大屏的应用实践
- 移动端数据可视化图表_林深不见路和鹿-站酷ZCOOL
- 一文和你介绍数据可视化:目的、设计、流程及注意事项 | 人人都是产品经理
- 全网echarts案例资源大总结和echarts的高效使用技巧(细节版) - 掘金
- Echarts图表管理方式总结 - 掘金
- 整理echarts的一些常用配置 - 掘金
- Vue实用echarts组件封装 - dev-zuo 技术日常