Wiidede's blog Wiidede's blog
  • 前端
  • Python
  • 算法
  • 生活
  • 其他
  • 分类
  • 标签
  • 归档
  • 关于我
  • 赞赏
  • 我的小站 (opens new window)
GitHub (opens new window)

Wiidede

小的的写前端
  • 前端
  • Python
  • 算法
  • 生活
  • 其他
  • 分类
  • 标签
  • 归档
  • 关于我
  • 赞赏
  • 我的小站 (opens new window)
GitHub (opens new window)
  • 整理一些css样式
  • vue隔代组件层层动态插槽并且附带数据
  • vue判断字符串是否溢出来显示弹窗、解决el-table tooltip 内过多导致无法显示,内容闪烁
  • 整理一些js写法
  • ElementUI timePicker 增加此刻按钮 引发的dom操作的学习
  • 毕业设计(水表识别)前端知识整理
  • html小知识
  • axios请求api然后下载文件
  • vue3+ts根据高度改变元素的透明度
  • vue3 + ElementPlus 换肤方案(Css变量)
  • Moment的一些使用方法
  • echarts基础vue组件
    • 基础组件(Vue2)
      • ResizeObserver(容器大小变化监听方案1)
      • element-resize-detector(容器大小变化坚挺方案2)
    • vue3 + TypeScript 组件更新
      • echarts.ts 参考在 TypeScript 中按需引入
      • echarts.vue
    • fontSize跟随页面宽度响应式变化
    • 一些echarts技巧
      • 增量更新
      • legend超出省略
      • echarts 使用数据集 在轴为 time 会无法显示
    • bar类型高阶组件封装(Vue2)(没啥特别的,这个类型的组件多了,封装一下)
  • element UI el-date-picker 年月日切换组件
  • 可以不选择的el-radio单选框
  • vue的小技巧总结
  • 全局动态权限判断(Vue指令)
  • vue-anchor 探索
  • Deep Dive with Evan You 笔记
  • 前端基础知识查漏补缺
  • WebPack 知识总结
  • 我写的一些可以日后参考的代码
  • 接口变化后,封装接口函数,改变返回内容
  • 项目组件整理
  • 前端框架设计想发
  • 全局进度条
  • 带有token的图片vue组件:authImg,使用axios下载图片
  • 前端npm包推荐
  • 给ElInputNumber添加prefix
  • ElPagination添加页数总数
  • el-tab做成chrome类似的tab样式
  • vue-grid-layout-组件配置
  • 项目数据字典封装
  • 图表组件响应式探索
  • ElementPlus表格table列自动合并composition
  • 简单的curd组件封装
  • ElementPlus表格自定义合计列composition
  • 一些处理表格数据composition api
  • div内容溢出后,内容向左悬浮,vue组件封装
  • 文本数字溢出后,按比例缩小,vue组件封装
  • 表格使用async-validator检验composition
  • ElementPlus Form一些简单的组件整合
  • arco-design快速使用tailwind的颜色、unocss动态颜色
  • 前端
wiidede
2021-12-27

echarts基础vue组件

# echarts基础vue组件

# 基础组件(Vue2)

组件特点:

  1. 自适应容器大小,始终撑满容器
  2. 彻底解决因为外层容器不可见导致 echarts 10px 的问题
  3. 将 echarts 事件以 vue 事件抛出

# ResizeObserver(容器大小变化监听方案1)

可以使用 ResizeObserver 这个 Web Api 来监听容器的大小变化,使用的时候注意下兼容性哦

<template>
  <div ref="echarts" class="echarts"/>
</template>

<script>
import * as echarts from 'echarts'
import {debounce} from 'lodash'

export default {
 name: 'EchartsDynamicResize',
 props: {
  options: {
   type: Object,
   default: () => ({})
  }
 },
 data() {
  return {
   chart: null,
   destroyFunc: null
  }
 },
 mounted() {
  this.chart = echarts.init(this.$refs.echarts)
  this.setOption(this.options)
  this.initChartEvent()
  this.makeEchartsResponsive()
 },
 beforeDestroy() {
  this.chart && this.chart.dispose()
  this.destroyFunc && this.destroyFunc()
 },
 methods: {
  getEchartsInstance() {
   return this.chart
  },
  setOption(option) {
   this.chart && this.chart.setOption(option)
  },
  // 注册echarts事件到Vue组件
  initChartEvent() {
   const emitEvents = ['mouseover', 'mouseout', 'click', 'legendselectchanged']
   emitEvents.forEach(eventName => {
    this.chart.on(eventName, (params) => {
     this.$emit(eventName, params, this.chart)
    })
   })
  },
  // echarts 容器大小自适应
  makeEchartsResponsive() {
   const targetNode = this.$refs.echarts
   const resizeDebounced = debounce(() => {
    this.chart.resize()
   }, 50)
   const resizeObserver = new ResizeObserver(() => {
    resizeDebounced()
   })
   resizeObserver.observe(targetNode)
   this.destroyFunc = () => {
    resizeObserver.disconnect()
   }
  }
 },
 watch: {
  options: {
   handler(val) {
    this.setOption(val)
   },
   deep: true
  }
 }
}
</script>

<style scoped>
.echarts {
  width: 100%;
  height: 100%;
  overflow: hidden;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

# element-resize-detector(容器大小变化坚挺方案2)

也可以选择这个库来监听元素的大小变化,这里只是改变监听方案,其他是和之前的一样的

import elementResizeDetector from 'element-resize-detector'

const data = {
  makeEchartsResponsive() {
   if (!this.$refs.echarts) {
    return
   }
   // element-resize-detector
   const erd = elementResizeDetector()
   const targetNode = this.$refs.echarts
   const resizeDebounced = debounce(() => {
    this.myChart.resize()
   }, 50)
   erd.listenTo(targetNode, resizeDebounced)
   this.destroyFunc = () => {
    erd.removeListener(targetNode, resizeDebounced)
   }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# vue3 + TypeScript 组件更新

  1. (建议父组件可以创建shallowRef的option,赋值的时候直接给整个option赋值)
  2. ts 按需引入
  3. 去除overflow: hidden 这样在组件外面也能看到tooltip,有需要自行添加即可
  4. 明确emit事件名,有需要自行添加其他事件

# echarts.ts 参考在 TypeScript 中按需引入 (opens new window)

后续可以通过这个组件拿到 echarts 和 option 类型,需要增加也在这里增加

import * as echarts from 'echarts/core'
import {
  BarChart,
  // 系列类型的定义后缀都为 SeriesOption
  BarSeriesOption,
  LineChart,
  LineSeriesOption
} from 'echarts/charts'
import {
  TitleComponent,
  // 组件类型的定义后缀都为 ComponentOption
  TitleComponentOption,
  TooltipComponent,
  TooltipComponentOption,
  GridComponent,
  GridComponentOption,
  // 数据集组件
  DatasetComponent,
  DatasetComponentOption,
  // 内置数据转换器组件 (filter, sort)
  TransformComponent
} from 'echarts/components'
import { LabelLayout, UniversalTransition } from 'echarts/features'
import { CanvasRenderer } from 'echarts/renderers'

// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
type ECOption = echarts.ComposeOption<
  | BarSeriesOption
  | LineSeriesOption
  | TitleComponentOption
  | TooltipComponentOption
  | GridComponentOption
  | DatasetComponentOption
>

// 注册必须的组件
echarts.use([
  TitleComponent,
  TooltipComponent,
  GridComponent,
  DatasetComponent,
  TransformComponent,
  BarChart,
  LineChart,
  LabelLayout,
  UniversalTransition,
  CanvasRenderer
])

export type { ECOption }

export default echarts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

# echarts.vue

<template>
  <div ref="echartsRef" class="echarts" />
</template>

<script lang="ts" setup>
import { debounce } from 'lodash-es'
import echarts from './echarts'
import type { ECOption } from './echarts'

const props = defineProps<{
  option: ECOption
}>()

type EventNames = 'mouseover' | 'mouseout' | 'click' | 'legendselectchanged'
interface IEmit {
  (event: 'click', params: any, chart: ReturnType<typeof echarts.init>): void
  (event: 'resize', params: { width: number, height: number }, chart: ReturnType<typeof echarts.init>): void
}
const emit = defineEmits<IEmit>()

let chart: ReturnType<typeof echarts.init>
const echartsRef = ref<HTMLDivElement>()
let destroyFunc: () => void

const getEchartsInstance = () => chart
const setOption = (option?: ECOption) => {
  option && chart!.setOption({ ...option })
}
const initChartEvent = () => {
  chart!.on('click' as string, (params) => {
    emit('click', params, chart!)
  })
}
const makeEchartsResponsive = () => {
  const targetNode = echartsRef.value!
  const resizeDebounced = debounce(() => {
    chart!.resize()
  }, 50)
  const resizeObserver = new ResizeObserver((targets) => {
    resizeDebounced()
    const [target] = targets
    emit('resize', { width: target.contentRect.width, height: target.contentRect.height }, chart!)
  })
  resizeObserver.observe(targetNode)
  destroyFunc = () => {
    resizeObserver.disconnect()
  }
}

onMounted(() => {
  chart = echarts.init(echartsRef.value!)
  setOption(props.option)
  initChartEvent()
  makeEchartsResponsive()
})

onUnmounted(() => {
  chart && chart.dispose()
  destroyFunc && destroyFunc()
})

watch(
  () => props.option,
  (option) => {
    setOption(option)
  },
  { deep: true },
)

defineExpose({
  getEchartsInstance,
})
</script>

<style scoped>
.echarts {
  width: 100%;
  height: 100%;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

# fontSize跟随页面宽度响应式变化

  1. 除了容器大小响应式,同时加入fontSize响应式变化(最好是默认viewPortWidth为0,即关闭字体响应式变化,需要的话传入设计稿页面宽度)
  2. 采用的echarts的 get、set 来快捷操作option对象
  3. 提供了一个getObjectPaths函数来获取对象的所有属性的path
  4. 注意点,echarts普通合并模式,series这种数组,需要找到对应name、id所在的项,不然数据都是新增
  5. 这里默认的setOption没用采用合并模式,需要传入整个图表的配置
<template>
  <div ref="echartsRef" class="echarts" />
</template>

<script setup>
import { debounce, get, set, cloneDeep } from 'lodash'
import * as echarts from 'echarts'
import {
  ref, onMounted, onUnmounted, watch,
} from 'vue'
import { getObjectPaths } from '@/utils/utils'

const props = defineProps({
  option: {
    type: Object,
    required: true,
  },
  viewPortWidth: {
    type: Number,
    default: 1920, // 0 关闭自适应
  },
})

const emit = defineEmits(['resize', 'mouseover', 'mouseout', 'click', 'legendselectchanged'])

let chart
const echartsRef = ref()
let destroyFunc

const defaultResizingOption = {} // 自适应配置(echarts 普通合并模式,需要用到name、id)
const fontSizePaths = new Set() // fontSize 需要的 key

const getEchartsInstance = () => chart
const setOption = (option) => {
  option && chart.setOption({ ...option }, true)
  // 这里没有走echarts合并模式,所以就不传参数,直接拿到echarts实例的option。
  // 如果是echarts合并模式,需要传入option,并且要想办法拿到echarts默认的option。
  collectFontSizePath()
}

// 注册echarts事件到Vue组件
const initChartEvent = () => {
  const emitEvents = ['mouseover', 'mouseout', 'click', 'legendselectchanged']
  emitEvents.forEach((eventName) => {
    chart.on(eventName, (params) => {
      emit(eventName, params, chart)
    })
  })
}

// echarts 容器大小自适应
const makeChartsResponsive = () => {
  const targetNode = echartsRef.value
  const resizeDebounced = debounce(() => {
    chart.resize()
    setFontSize() // resize的时候也要重新设置字体大小
  }, 50)
  const resizeObserver = new ResizeObserver((targets) => {
    resizeDebounced()
    const [target] = targets
    const { width, height } = target.contentRect
    emit('resize', { width, height }, chart)
  })
  resizeObserver.observe(targetNode)
  destroyFunc = () => {
    resizeObserver.disconnect()
  }
}

// 拿到option中所有的fontSize(包括基础的id、name)
const collectFontSizePath = (option) => {
  option === undefined && (option = chart.getOption())
  const pathList = getObjectPaths(option)
  const optionPathList = pathList.filter((path) => !path.includes('data'))
  const basePathList = optionPathList.filter((path) => path.endsWith('.name') || path.endsWith('id'))
  const fontSizePathList = optionPathList.filter((path) => path.endsWith('.fontSize'))
  basePathList.forEach((path) => set(defaultResizingOption, path, get(option, path)))
  fontSizePathList.forEach((path) => {
    let fontSize = get(option, path)
    if (typeof fontSize === 'string' && fontSize.endsWith('px')) {
      fontSize = Number(fontSize.replace('px', '')) || 12
    }
    set(defaultResizingOption, path, fontSize)
    fontSizePaths.add(path)
  })
  setFontSize()
}

// 遍历收集到的fontSize,根据页面宽度重新设置字体大小
const setFontSize = () => {
  if (!props.viewPortWidth) return
  const width = document.body.clientWidth
  const ratio = width / props.viewPortWidth
  const fontSizeOption = cloneDeep(defaultResizingOption)
  fontSizePaths.forEach((path) => {
    const fontSize = get(defaultResizingOption, path)
    const newFontSize = Number((ratio * fontSize).toFixed(5)) // 精度
    set(fontSizeOption, path, newFontSize)
  })
  chart.setOption(fontSizeOption)
}

onMounted(() => {
  chart = echarts.init(echartsRef.value)
  setOption(props.option)
  initChartEvent()
  makeChartsResponsive()
})

onUnmounted(() => {
  chart && chart.dispose()
  destroyFunc && destroyFunc()
})

watch(
  () => props.option,
  (option) => {
    setOption(option)
  },
  // { deep: true }, // 如果默认不走echarts合并模式,就不需要deep了
)

defineExpose({
  getEchartsInstance,
})
</script>

<style scoped>
.echarts {
  width: 100%;
  height: 100%;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133

取到对象所有的path

const getObjectPaths = (object) => {
  const paths = []
  const getPaths = (obj, path) => {
    if (obj && typeof obj === 'object') {
      Object.keys(obj).forEach((key) => {
        getPaths(obj[key], path ? `${path}.${key}` : key)
      })
    } else {
      paths.push(path)
    }
  }
  getPaths(object, '')
  return paths
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 一些echarts技巧

# 增量更新

setOption默认会合并option

# legend超出省略

通过这个配置可以看看 echarts 的 format 有什么功能

import {format as echartsFormat} from 'echarts'

const legend = {
 formatter: function (name) {
  return echartsFormat.truncateText(name, 80, undefined, '…', undefined)
 },
 tooltip: {
  show: true
 }
}
1
2
3
4
5
6
7
8
9
10

# echarts 使用数据集 在轴为 time 会无法显示

echarts 使用数据集 在轴为 time 会无法显示,可以在每个 serise 加上 encode 指定维度

const series = {
 encode: {
  x: 'time',
  y: 'dimension_name'
 }
}
1
2
3
4
5
6

# bar类型高阶组件封装(Vue2)(没啥特别的,这个类型的组件多了,封装一下)

组件特点:

  1. 基于基础组件开发
  2. 将所有基础组件事件抛出
  3. axisLabel坐标轴标签太长隐藏,hover坐标轴标签显示tooltip
<template>
  <echarts-base :id="id"
                :options="baseOptions"
                ref="echarts"
                :class-name="className"
                @mouseover="mouserOver"
                @mouseout="mouserOut"
                v-on="$listeners"
  />
</template>

<script>
import echartsBase from '/@/components/echarts'

export default {
 name: 'barChart',
 components: {
  echartsBase
 },
 props: {
  id: {
   type: String,
   required: true,
   default: ''
  },
  className: {
   type: String,
   default: ''
  },
  options: {
   type: Object,
   default: () => ({})
  }
 },
 data() {
  return {
   baseOptions: {
    tooltip: {
     trigger: 'axis',
     axisPointer: {
      type: 'shadow'
     },
     confine: true,
     formatter: undefined
    },
    grid: {
     left: 90,
     bottom: 18,
     right: 30,
     top: 18
    },
    color: [
     // '#1890FF'
     {
      type: 'linear',
      colorStops: [{
       offset: 0, color: '#1890FF' // 0% 处的颜色
      }, {
       offset: 1, color: '#5AC8FF' // 100% 处的颜色
      }]
     },
     {
      type: 'linear',
      colorStops: [{
       offset: 0, color: '#E6E6E6' // 0% 处的颜色
      }, {
       offset: 1, color: '#CCCCCC' // 100% 处的颜色
      }]
     },
     {
      type: 'linear',
      colorStops: [{
       offset: 0, color: '#FFB726' // 0% 处的颜色
      }, {
       offset: 1, color: '#FFD659' // 100% 处的颜色
      }]
     },
     {
      type: 'linear',
      colorStops: [{
       offset: 0, color: '#22D5E6' // 0% 处的颜色
      }, {
       offset: 1, color: '#49BAF2' // 100% 处的颜色
      }]
     },
     {
      type: 'linear',
      colorStops: [{
       offset: 0, color: '#81FBB8' // 0% 处的颜色
      }, {
       offset: 1, color: '#45D885' // 100% 处的颜色
      }]
     },
     {
      type: 'linear',
      colorStops: [{
       offset: 0, color: '#D8795A' // 0% 处的颜色
      }, {
       offset: 1, color: '#F8B28E' // 100% 处的颜色
      }]
     }
    ],
    dataZoom: [
     {
      type: 'inside',
      yAxisIndex: 0
     }
    ],
    xAxis: {
     type: 'value',
     axisLabel: {
      show: false
     },
     axisLine: {
      lineStyle: {
       color: '#D9D9D9'
      }
     },
     axisTick: {
      show: false
     },
     splitLine: {
      lineStyle: {
       type: 'dashed',
       color: '#E8E8E8'
      }
     }
    },
    yAxis: {
     type: 'category',
     axisPointer: {
      type: 'shadow'
     },
     axisLabel: {
      textStyle: {
       color: '#666666',
       fontSize: 12
      },
      formatter: (params) => {
       if (params.length > 5) {
        return params.substring(0, 5) + '...'
       } else {
        return params
       }
      }
     },
     axisLine: {
      lineStyle: {
       color: '#E8E8E8',
       type: 'dashed'
      }
     },
     axisTick: {
      show: false
     },
     splitLine: {
      show: false
     },
     triggerEvent: true
    },
    series: [{
     type: 'bar',
     cursor: 'default',
     barMaxWidth: 25,
     emphasis: {
      focus: 'series'
     }
    }],
    dataset: {source: []} // 使用数据集
   }
  }
 },
 methods: {
  mouserOver(params, chart) {
   if (params.componentType === 'yAxis') {
    if (params.value.length <= 5) {
     return
    }
    let offsetX = params.event.offsetX + 10
    let offsetY = params.event.offsetY + 10
    chart.setOption({
     tooltip: {
      formatter: () => params.value,
      confine: true,
      alwaysShowContent: true
     }
    })
    chart.dispatchAction({
     type: 'showTip',
     seriesIndex: 0,
     dataIndex: 0,
     position: [offsetX, offsetY]
    })
   } else if (params.componentType === 'series') {
    chart.setOption({
     tooltip: {...this.baseOptions.tooltip}
    })
    chart.dispatchAction({
     type: 'showTip',
     seriesIndex: params.seriesIndex,
     dataIndex: params.dataIndex
    })
   }
  },
  mouserOut(params, chart) {
   if (params.componentType === 'yAxis') {
    this.yLabel = ''
    chart.setOption({tooltip: {
     ...this.baseOptions.tooltip,
     alwaysShowContent: false
    }})
   }
  }
 },
 watch: {
  options: {
   handler: function (val) {
    Object.assign(this.baseOptions, val)
   },
   deep: true
  }
 }
}
</script>

<style scoped>

</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
#echarts#Vue#Vue组件#前端
上次更新: 2023/06/01, 12:40:50

← Moment的一些使用方法 element UI el-date-picker 年月日切换组件→

Theme by Vdoing | Copyright © 2021-2023 Wiidede | Website use MIT License | Article content & logo use CC-BY-SA-4.0 License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式