背景

在 uni-app 的内置组件和官方扩展组件中,是没有支持图表的组件的。通过内置组件-画布-canvas的页面内容,可以找到官方文档对图表使用的解释:

image-20241015203614690

一开始准备直接尝试使用uChart的,看到微信扫一扫就劝退了。反正我只需要基础的图表功能,也不太在乎性能,再加上之前在Vue项目写过echarts代码,对echarts官方的文档说明比较了解,就选择了另一个插件:echarts

这个插件主要就是为了让 uni-app 能兼容echarts,所有图表相关语法都是直接用echarts的,所以上手就比较简单些。

开始使用

在插件文档的代码演示中,找到Vue3版本的示例代码,直接拷贝,运行项目,图表正常展示。

现在就是需要自定义自己的图表样式了,我做的图表是一个饼图,也没什么样式要求,整个option的结构定义如下:

1
2
3
4
5
6
7
8
9
10
11
{
series: [{
type: 'pie',
data: data,
radius: '80%',
label: {
position: 'inside',
formatter: '{b}\n{d}%'
}
}]
}

所以只需要搞定data数据的异步加载和更新就可以了。

因为我做的功能是要求饼图能根据页面选择的日期加载指定日期的统计数据,所以饼图是需要刷新变化的,按照echarts的说法,可以直接对chart实例调用setOption方法,将整个option重新赋值就可以了。

但是,事与愿违,无论我怎么尝试,图表始终展示的都是第一次渲染的data数据。期间通过GPT、Google、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
53
54
55
56
57
58
59
60
61
62
63
64
65
以下是我在uni-app项目中,一个vue文件关于echarts的部分代码,在交互触发searchData()导致数据变化后,图表并没有发生变化。
<view style="width:100%; height:750rpx"><l-echart ref="chartRef"></l-echart></view>
const echarts = require('../../uni_modules/lime-echart/static/echarts.min');
// 支出分类数据。包括图表和分类排行的数据
const categoryData = reactive({
chartList: [],
categoryList: []
})

const categoryDataRef = ref({
chartList: [] // 确保 chartList 是响应式的
});
// 图表数据
const chartRef = ref(null)
// 监控 chartList 的变化
watch(() => categoryDataRef.value.chartList, (newVal, oldVal) => {
if (myChart.value) {
renderChart();
}
}, { deep: true }); // 确保深度监控
/**
* 查询数据
*/
function searchData() {
statistics.pageDto.currentMonth = ${currentMonth.value}-01
statistics.pageDto.isQueryLastMonth = compareLastMonth.value
Promise.all([loadTotalStatisticsData(), loadRankStatisticsData(), loadCategoryStatisticsData()])
.then(([result1, result2, result3]) => {
console.log("进入111");
// renderChart()
categoryDataRef.value.chartList = categoryData.chartList
})
.catch((error) => {
console.error('请求失败:', error);
});
}

const myChart = ref(null)

function renderChart() {
console.log("进入方法");
console.log(categoryData.chartList);
myChart.value.setOption({
series: [{
type: 'pie',
data: categoryDataRef.value.chartList,
radius: '80%',
label: {
position: 'inside',
formatter: '{b}\n{d}%'
}
}]
}, true)
}


onReady(() => {
// 组件能被调用必须是组件的节点已经被渲染到页面上
setTimeout(async () => {
if (!chartRef.value) return
myChart.value = await chartRef.value.init(echarts)

searchData()
}, 300)
})

GPT的答复中,给出了多方面的检查和调整思路:

  1. 图表实例的初始化与数据更新时机

myChart 可能未在数据变化时正确初始化。如果 searchData 在图表初始化之前触发,数据将不会被正确渲染。

searchData 触发前确保 myChart 已被初始化:

1
2
3
4
5
6
7
8
9
onReady(() => {
setTimeout(async () => {
if (!chartRef.value) return;
myChart.value = await chartRef.value.init(echarts);

// 确保图表实例化后才调用 searchData
searchData();
}, 300);
});
  1. 数据响应式处理

虽然你使用了 reactiveref,但是在 Promise.all 中直接更新 categoryDataRef.value.chartList 可能没有触发 watch。尝试通过 Vue.set 方式强制数据更新:

1
categoryDataRef.value.chartList = [...categoryData.chartList]; // 确保数据发生变化
  1. watch 的深度监听

watch 中使用了 { deep: true },但由于 categoryDataRef.value.chartList 是数组,Vue 有时不会捕捉到数组内部的变化。为了确保监控到变化,可以尝试:

1
2
3
4
5
watch(() => categoryDataRef.value.chartList.slice(), (newVal, oldVal) => {
if (myChart.value) {
renderChart();
}
}, { deep: true });

使用 slice() 方法创建新数组,触发 Vue 的响应式系统。

  1. setOption 强制更新

你已经使用了 setOption 的强制更新参数 true,这一点是正确的,可以保证即使数据小幅更新,图表也会重新渲染。

确保 renderChart 确实在数据变更后被正确调用,检查日志输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function renderChart() {
console.log("进入方法");
console.log(categoryDataRef.value.chartList);
myChart.value.setOption({
series: [{
type: 'pie',
data: categoryDataRef.value.chartList,
radius: '80%',
label: {
position: 'inside',
formatter: '{b}\n{d}%'
}
}]
}, true);
}

总结

可以尝试以下步骤:

  1. 确保 myChart 初始化在 searchData 调用之前。
  2. 通过 slice() 或者使用 Vue.set 强制触发 watch 的响应式更新。
  3. renderChart 中检查数据是否正确传递给 setOption

如果这些调整都没有效果,可以在 renderChart 内部打印日志以进一步调试数据更新的时机与流程。

---- 分段 ----

通过上述思路,我首先在 renderChart 方法中,增加了打印 option 的代码:

1
2
const currentOption = myChart.value.getOption();
console.log("setOption 后的 series.data:", currentOption.series[0].data);

通过打印发现,series的data数据确实没有更新,说明myChart.value.setOption没有生效。

第1点,图表实例的初始化与数据更新时机,这个其实只要自己代码逻辑清晰,数据更新时机是肯定没问题的,至于实例初始化问题,第一次渲染的数据图表都出来,所以初始化也没有问题。

第2点,认为数据可能没有触发watch,其实通过日志打印,是可以看到每次数据发生变化后,触发了watch方法的。之前我没有使用监听方法,而是在每次加载数据后手动调用renderChart方法,但总是出现一些莫名其妙的问题,于是就改成监听方式了。

第3点,watch的深度监听,这个跟第2点一样,是可以忽略的,因为打印了日志,说明能监听到数据变化。

然后我根据资料解释,一度怀疑是echarts没有触发更新,于是尝试了echarts的cleardisposenotMerge: true等方法,直到解决问题后才幡然醒悟,属性数据都没有发生变化,echarts图表肯定不会重新渲染,所以不存在第4点说的问题。

最后,我重整思绪,基本可以确定是myChart.value.setOption这段代码出现了问题,但是实在是想不通这样有什么不对,因为myChart确实就是echarts的对象实例啊,而且第一次加载的数据也渲染出来了。

整段代码中,其实都很好理解,因为都是vue或者echarts的语法内容,可以确定没问题,问题的关键还是在这个插件上,chartRef.value.init(echarts)方法,顾名思义就是初始化,但我没有看到任何关于这个init方法的解释,于是产生怀疑,init方法返回的对象可能并不是echarts实例对象。

于是,我又在插件文档上反复浏览,发现了下面这一段内容:

image-20241015211809598

文档说明init方法还有第二个参数的,第二个参数是回调函数,回调函数的参数才是chart实例。这是其一,还有一个很重要的观察点,既然这个方法都列在一起,说明它们的调用对象是同一个!!!而init方法的调用对象是chartRef.value,那么是不是说明setOption方法的调用对象也是chartRef.value,于是renderChart方法代码改成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function renderChart() {
const option = {
series: [{
type: 'pie',
data: categoryDataRef.value.chartList,
radius: '80%',
label: {
position: 'inside',
formatter: '{b}\n{d}%'
}
}]
}

chartRef.value.setOption(option)
}

再次启动项目,图表重新渲染成功!

所以~关键点就是setOption的调用对象搞错了。。。

但也留下了一个疑问,为什么第一次渲染时,使用myChart.value.setOption却可以。