echarts基础vue组件
# echarts基础vue组件
# 基础组件(Vue2)
组件特点:
- 自适应容器大小,始终撑满容器
- 彻底解决因为外层容器不可见导致 echarts 10px 的问题
- 将 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
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# vue3 + TypeScript 组件更新
- (建议父组件可以创建shallowRef的option,赋值的时候直接给整个option赋值)
- ts 按需引入
- 去除overflow: hidden 这样在组件外面也能看到tooltip,有需要自行添加即可
- 明确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
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
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跟随页面宽度响应式变化
- 除了容器大小响应式,同时加入fontSize响应式变化(最好是默认viewPortWidth为0,即关闭字体响应式变化,需要的话传入设计稿页面宽度)
- 采用的echarts的 get、set 来快捷操作option对象
- 提供了一个getObjectPaths函数来获取对象的所有属性的path
- 注意点,echarts普通合并模式,series这种数组,需要找到对应name、id所在的项,不然数据都是新增
- 这里默认的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
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
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
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
2
3
4
5
6
# bar类型高阶组件封装(Vue2)(没啥特别的,这个类型的组件多了,封装一下)
组件特点:
- 基于基础组件开发
- 将所有基础组件事件抛出
- 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
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
上次更新: 2023/06/01, 12:40:50