小程序
echarts
在 uni-app 小程序中使用 ECharts,需要三个层次的适配:按需引入裁剪包体积、用 WxCanvas 桥接小程序 Canvas API、封装 Vue 组件统一管理初始化和触摸事件。
按需引入与类型配置
注册项目所需的图表类型(柱状图、折线图、饼图、雷达图)和组件(网格、提示框、图例等),导出组合后的 CustomEChartsOption 类型和 echarts 实例供全局使用。
import type {
BarSeriesOption,
LineSeriesOption,
PieSeriesOption,
RadarSeriesOption,
} from 'echarts/charts'
import type {
GraphicComponentOption,
GridComponentOption,
TitleComponentOption,
ToolboxComponentOption,
TooltipComponentOption,
} from 'echarts/components'
import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts'
import {
GraphicComponent,
GridComponent,
LegendComponent,
TitleComponent,
TooltipComponent,
} from 'echarts/components'
import * as echarts from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
echarts.use([
GridComponent,
BarChart,
CanvasRenderer,
TooltipComponent,
PieChart,
LegendComponent,
TitleComponent,
RadarChart,
GraphicComponent,
LineChart,
])
type EChartsBaseOption =
| TitleComponentOption
| ToolboxComponentOption
| TooltipComponentOption
| GridComponentOption
| LineSeriesOption
| BarSeriesOption
| PieSeriesOption
| RadarSeriesOption
export type CustomEChartsOption =
| echarts.ComposeOption<EChartsBaseOption>
| echarts.ComposeOption<GraphicComponentOption>
export { echarts }Canvas 适配器
ECharts 在 Web 端依赖标准 HTML Canvas API,小程序的 Canvas 接口与之不兼容。WxCanvas 将小程序 CanvasContext 包装成 ECharts 可识别的对象,同时将小程序触摸事件(touchStart/touchMove/touchEnd)映射为 ECharts 期望的鼠标事件(mousedown/mousemove/mouseup/click)。
import type { EChartsType } from 'echarts/core'
export default class WxCanvas {
ctx: UniApp.CanvasContext
canvasId: string
chart: EChartsType | null
isNew: boolean
canvasNode?: HTMLCanvasElement
event: Record<string, any> = {}
constructor(
ctx: UniApp.CanvasContext,
canvasId: string,
isNew: boolean,
canvasNode?: HTMLCanvasElement,
) {
this.ctx = ctx
this.canvasId = canvasId
this.chart = null
this.isNew = isNew
if (isNew) {
this.canvasNode = canvasNode
} else {
this._initStyle(ctx)
}
this._initEvent()
}
getContext(contextType: string) {
if (contextType === '2d') {
return this.ctx
}
}
setChart(chart: EChartsType) {
this.chart = chart
}
addEventListener() {
// noop
}
attachEvent() {
// noop
}
detachEvent() {
// noop
}
_initCanvas(zrender: any, ctx: any) {
zrender.util.getContext = function () {
return ctx
}
zrender.util.$override('measureText', (text: any, font: any) => {
ctx.font = font || '12px sans-serif'
return ctx.measureText(text)
})
}
_initStyle(ctx: any) {
ctx.createRadialGradient = () => {
// eslint-disable-next-line prefer-rest-params
return ctx.createCircularGradient(arguments)
}
}
_initEvent() {
this.event = {}
const eventNames = [
{
wxName: 'touchStart',
ecName: 'mousedown',
},
{
wxName: 'touchMove',
ecName: 'mousemove',
},
{
wxName: 'touchEnd',
ecName: 'mouseup',
},
{
wxName: 'touchEnd',
ecName: 'click',
},
]
eventNames.forEach((name) => {
this.event[name.wxName] = (e: any) => {
const touch = e.touches[0]
this.chart?.getZr().handler.dispatch(name.ecName as any, {
zrX: name.wxName === 'tap' ? touch.clientX : touch.x,
zrY: name.wxName === 'tap' ? touch.clientY : touch.y,
preventDefault: () => {},
stopImmediatePropagation: () => {},
stopPropagation: () => {},
})
}
})
}
set width(w) {
if (this.canvasNode) this.canvasNode.width = w
}
get width() {
if (this.canvasNode) return this.canvasNode?.width
return 0
}
set height(h) {
if (this.canvasNode) this.canvasNode.height = h
}
get height() {
if (this.canvasNode) return this.canvasNode.height
return 0
}
}组件类型导出
统一导出 EcCanvas 组件的实例类型和 CustomEChartsOption,供业务页面在 TypeScript 中使用。
import type { ComponentExposed } from 'vue-component-type-helpers'
import type EcCanvas from './ec-canvas.vue'
export type { CustomEChartsOption } from '@cloud/shared'
export type EcCanvasInstance = ComponentExposed<typeof EcCanvas>EcCanvas 组件
核心封装组件。在 onReady 生命周期中通过 SelectorQuery 获取 Canvas 节点,用 WxCanvas 桥接后调用 echarts.init 完成初始化。监听 option prop 变化自动调用 setOption 更新图表。对外暴露 getDataURL(导出图片)和 getEchartsInstance(获取原始实例)两个方法。
<script setup lang="ts">
import type { CustomEChartsOption } from './types'
import { WxCanvas, createBooleanProp, echarts, mergeDeep } from '@cloud/shared'
import { onReady } from '@dcloudio/uni-app'
import { getCurrentInstance, nextTick, ref, watch } from 'vue'
const props = defineProps({
canvasId: {
type: String,
default: () => 'ec-canvas',
},
option: {
type: Object as PropType<CustomEChartsOption>,
default: () => ({}),
},
disabled: createBooleanProp(),
})
type TouchCanvasEvent = UniHelper.CanvasOnTouchcancelEvent
const currentInstance = getCurrentInstance()
let canvasNode: HTMLCanvasElement | null = null
let chart: echarts.EChartsType | null = null
const isReady = ref(false)
function getOption() {
const option: CustomEChartsOption = {}
const optionMerge = props.option
return mergeDeep(option, optionMerge)
}
function init() {
echarts.registerPreprocessor((option) => {
if (option && option.series) {
if (option.series instanceof Array && option.series.length > 0) {
option.series.forEach((series) => {
series.progressive = 0
})
} else if (typeof option.series === 'object') {
;(option.series as { progressive: number }).progressive = 0
}
}
})
initEcharts()
}
async function initEcharts() {
// version >= 2.9.0:使用新的方式初始化
await nextTick()
isReady.value = true
const query = uni.createSelectorQuery().in(currentInstance)
query
.select(`#${props.canvasId}`)
.fields({ node: true, size: true } as any, () => {})
.exec((res) => {
if (chart) chart.dispose()
canvasNode = res[0].node as unknown as HTMLCanvasElement
const dpr = uni.getSystemInfoSync().pixelRatio
const canvasWidth = res[0].width
const canvasHeight = res[0].height
const ctx = canvasNode!.getContext(
'2d',
) as any as UniNamespace.CanvasContext
const canvas = new WxCanvas(ctx, props.canvasId, true, canvasNode)
echarts.setPlatformAPI({
createCanvas: () => canvas as unknown as HTMLCanvasElement,
loadImage: (src, onload, onerror) => {
if ((canvasNode as any)?.createImage) {
const image = (canvasNode as any).createImage()
image.onload = onload
image.onerror = onerror
image.src = src
return image
}
console.error(
'加载图片依赖 `Canvas.createImage()` API,要求小程序基础库版本在 2.7.0 及以上。',
)
// PENDING fallback?
},
})
chart = echarts.init(canvas as unknown as HTMLCanvasElement, null, {
width: canvasWidth,
height: canvasHeight,
devicePixelRatio: dpr, // new
})
nextTick(() => {
chart?.setOption(getOption())
})
canvas.setChart(chart as any)
})
}
function wrapTouch(event: TouchCanvasEvent): any {
return {
...event,
touches: event.touches.map((touch) => {
return {
...touch,
offsetX: touch.x,
offsetY: touch.y,
}
}),
}
}
function touchStart(e: TouchCanvasEvent) {
if (props.disabled) return
if (chart && e.touches.length > 0) {
const touch = e.touches[0]
const handler = chart.getZr().handler
handler.dispatch('mousedown', {
zrX: touch.x,
zrY: touch.y,
preventDefault: () => {},
stopImmediatePropagation: () => {},
stopPropagation: () => {},
})
handler.dispatch('mousemove', {
zrX: touch.x,
zrY: touch.y,
preventDefault: () => {},
stopImmediatePropagation: () => {},
stopPropagation: () => {},
})
handler.processGesture(wrapTouch(e), 'start')
}
}
function touchMove(e: TouchCanvasEvent) {
if (props.disabled) return
if (chart && e.touches.length > 0) {
const touch = e.touches[0]
const handler = chart.getZr().handler
handler.dispatch('mousemove', {
zrX: touch.x,
zrY: touch.y,
preventDefault: () => {},
stopImmediatePropagation: () => {},
stopPropagation: () => {},
})
handler.processGesture(wrapTouch(e), 'change')
}
}
function touchEnd(e: TouchCanvasEvent) {
if (props.disabled) return
if (chart) {
const touch = e.changedTouches ? e.changedTouches[0] : {}
const handler = chart.getZr().handler
handler.dispatch('mouseup', {
zrX: touch.x,
zrY: touch.y,
preventDefault: () => {},
stopImmediatePropagation: () => {},
stopPropagation: () => {},
})
handler.dispatch('click', {
zrX: touch.x,
zrY: touch.y,
preventDefault: () => {},
stopImmediatePropagation: () => {},
stopPropagation: () => {},
})
handler.processGesture(wrapTouch(e), 'end')
}
}
watch(
[() => props.option, () => isReady.value],
() => {
if (isReady.value && chart) {
chart.setOption(getOption())
}
},
{
deep: true,
},
)
defineExpose({
getDataURL: () => {
return new Promise<string>((resolve, reject) => {
uni
.canvasToTempFilePath({
canvasId: 'ec-canvas',
canvas: canvasNode as any,
width: canvasNode?.width,
height: canvasNode?.height,
})
.then((res) => {
return uni.getImageInfo({
src: res.tempFilePath,
})
})
.then((res) => {
resolve(res.path)
})
.catch((e) => {
reject(e)
throw e
})
})
},
getEchartsInstance() {
return chart
},
})
onReady(init)
</script>
<template>
<canvas
:id="canvasId"
type="2d"
class="ec-canvas"
:canvas-id="canvasId"
@touchstart="(e) => (!disabled ? touchStart(e) : undefined)"
@touchmove="(e) => (!disabled ? touchMove(e) : undefined)"
@touchend="(e) => (!disabled ? touchEnd(e) : undefined)"
></canvas>
</template>
<style>
.ec-canvas {
width: 100%;
height: 100%;
}
</style>