news 2026/10/14 2:44:31

uniapp下renderjs集成echarts堆叠柱状图完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
uniapp下renderjs集成echarts堆叠柱状图完整指南

1. 场景与方案:为什么要在uniapp里用echarts画堆叠柱状图

1.1 堆叠柱状图的业务价值

堆叠柱状图是数据可视化里非常常用的一种图形,它把不同类别的数据纵向叠加起来,既能看总量,也能看构成。比如说在一个管理后台里展示一周的销售额,你可以把“线上销售”“门店销售”“渠道分销”三个子项堆叠在同一根柱子上,一眼就能看出每天的销售额盘子有多大,三个渠道的占比如何。再比如网站流量分析里的新老访客叠加,订单来源构成统计,都是类似的场景。做uniapp跨端应用时,图表模块往往是逃不掉的需求,而堆叠柱状图又是其中出现频率很高的一种。

1.2 常见集成方案对比

我过去在实际项目里试过几种在uniapp中使用echarts的路子,这里先说结论:如果你的页面运行环境是H5端或者App的Vue页面,我很推荐用renderjs + echarts这套组合。理由很简单——renderjs允许我们在视图中直接操作DOM,而且可以和页面逻辑互通,不必像web-view那样套一层网页壳。实际用下来,页面渲染、交互响应、动画流畅度都还不错,也没有web-view那种“页面里套网页”的割裂感,更不需要去处理复杂的数据传递协议。

那为什么不用web-view?web-view本质上是在页面里放了一个浏览器页面,通信靠postMessage,图表需要的数据和事件都要走一层消息桥,代码维护起来比较繁琐,而且web-view在某些小程序平台会被限制。为什么不用纯粹的canvas自绘?那样工作量大,堆叠动画、legend、tooltip都要自己造轮子,不划算。还有一个uniapp生态里很流行的ucharts方案,它对小程序非常友好,但它是基于canvas封装的独立图表库,和echarts的option配置体系不同,如果你团队已经熟悉echarts,或者业务上要复用Web端现成的图表配置,那么echarts直接复用配置、降低心智负担的优势就体现出来了。

需要提醒的是,renderjs方案在微信小程序里是不通用的。如果你明确要上小程序,那么更稳妥的是使用小程序原生支持的echarts组件或ucharts。本文后续的所有做法,核心针对的是uniapp中的Vue页面运行在H5和App端的情况,这一点先划清楚,免得后面照搬踩坑。

2. 环境准备与基础集成步骤

2.1 安装依赖

既然是Vue页面里用echarts,第一步自然是把echarts装进项目。在uniapp项目根目录执行:

npm install echarts --save

如果你用的是HBuilderX创建的项目,没有package.json,建议先把项目初始化成npm管理模式,再执行安装命令。装完之后,在renderjs的script里就可以正常import了。这里我直接用了完整版的echarts包,简单省事,缺点是打出来的包会大一点。后面第5章我会讲怎么按需引入来瘦身。

2.2 页面模板结构

我们在pages目录下创建一个Vue页面,模板里只需要放一个普通的view作为图表容器:

<template> <view class="page"> <view id="stackChart" class="chart-container"></view> </view> </template>

注意这个容器不能是canvas,就是普通view。renderjs会用它内部创建的canvas去替换或者挂载在这个节点上。容器要给一个明确的高度,比如height: 600rpx,否则图表初始化时拿到的尺寸是0,啥也画不出来。宽度方面,建议直接用100%,再配合页面布局去撑开。

2.3 普通script中的数据处理

在同一个vue文件里,我们正常写一个<script>,用来放业务数据、请求后端、处理参数等。renderjs模块和普通script模块是分开的,普通script负责把数据“推”给renderjs。数据更新时,只需要修改普通script中对应的响应式变量,renderjs就能收到变化并重新绘制。

这里有一个关键点:renderjs中拿到的数据是最初通过props传入的,如果你在普通script里直接this.chartData = newData,renderjs里通过watch或updated监听来感知变化。为了让数据传递更稳定,我习惯把维度信息和系列数组合并成一个对象一次性传过去,避免散装变量造成时序问题。

2.4 renderjs模块结构

renderjs是uniapp提供的一种特殊script语法,写法如下:

<script module="chartRenderer" lang="renderjs"> import * as echarts from 'echarts'; export default { data() { return { chart: null }; }, mounted() { this.initChart(); }, methods: { initChart() { // 拿到节点 const dom = document.getElementById('stackChart'); this.chart = echarts.init(dom); // 初始绘制使用普通script传入的数据 this.renderChart(this.chartData); }, renderChart(data) { if (!this.chart || !data) return; const option = buildOption(data); this.chart.setOption(option); }, buildOption(data) { // 具体配置 } }, watch: { chartData: { handler(newVal) { this.renderChart(newVal); }, deep: true } } }; </script>

这个模块和普通的script有不同的运行环境,它运行在视图层,可以直接操作DOM,也能访问页面传入的属性。普通script和renderjs之间,通过页面props或者触发事件通信。实际开发中,我一般会在普通script里把chartData塞进data,然后模板上通过props传值给renderjs模块对应的对象。

更具体的完整代码,我在第4章给出一个可以直接复制的整页demo。

3. 堆叠柱状图核心配置解析

3.1 堆叠原理:stack字段

堆叠柱状图的核心,在echarts的series配置里就是stack字段。多个系列的stack值设置成同一个字符串,它们就会被归为一组,按顺序纵向堆叠。比如下面的配置:

series: [ { name: '线上', type: 'bar', stack: 'total', data: [320, 302, 341, 374] }, { name: '门店', type: 'bar', stack: 'total', data: [120, 132, 151, 134] }, { name: '分销', type: 'bar', stack: 'total', data: [220, 182, 191, 234] } ]

三个系列的stack都是'total',那么在横轴上,每一根柱子会从下往上依次堆叠:先画“线上”,再在它上面画“门店”,最上面是“分销”。柱子总高度就是三个系列数据的和。如果你把某个系列的stack改成另一个值,它就会单独成柱,不再参与混叠。

理解这个字段很重要,因为很多人第一次画堆叠图,以为只要把type设为bar就能叠起来,结果画出来是三根柱子并排,就是因为没设置stack。

3.2 一个标准的堆叠柱状图option

一个完整可用的堆叠柱状图配置,我建议至少包含以下部分:

export function buildOption(data) { const { categories, seriesData } = data; return { color: ['#5470C6', '#91CC75', '#FAC858'], tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, legend: { data: seriesData.map(item => item.name) }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: { type: 'category', data: categories }, yAxis: { type: 'value' }, series: seriesData.map(item => ({ name: item.name, type: 'bar', stack: 'total', emphasis: { focus: 'series' }, barWidth: '40%', data: item.data })) }; }

tooltip的trigger设置为axis,配合axisPointer.type: 'shadow',鼠标悬停在一根柱子上时会显示整组堆叠数据,适合对比分析。barWidth统一设置,能保证三个系列宽度一致。containLabel: true可以让y轴刻度文字完整显示,不至于被截断。

3.3 从接口数据到echarts数据结构

后端给的原始数据往往不是直接能塞进echarts的,常见格式是一个对象数组:

[ { date: '2024-01-01', online: 320, store: 120, agent: 220 }, { date: '2024-01-02', online: 302, store: 132, agent: 182 } ]

所以我们需要在普通script里做一次转换,拆成echarts需要的categories和seriesData。我习惯写一个转换函数:

function transform(rawList) { const categories = rawList.map(item => item.date); const onlineData = rawList.map(item => item.online); const storeData = rawList.map(item => item.store); const agentData = rawList.map(item => item.agent); return { categories, seriesData: [ { name: '线上', data: onlineData }, { name: '门店', data: storeData }, { name: '分销', data: agentData } ] }; }

然后把转换结果赋给chartData。这样renderjs拿到的是已经完全适配echarts的数据结构,职责单一,也方便调试。

4. 从零复现:一个可运行的堆叠柱状图页面

4.1 完整代码demo

下面这个vue文件,是我在实际项目里精简后的通用版本,你可以直接复制到pages/stack-demo/stack-demo.vue跑一下。前提是项目里已经安装了echarts。

<template> <view class="page"> <view id="stackChart" class="chart-container"></view> </view> </template> <script> export default { data() { return { chartData: { categories: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], seriesData: [ { name: '线上', data: [320, 302, 341, 374, 390, 450, 420] }, { name: '门店', data: [120, 132, 101, 134, 90, 230, 210] }, { name: '分销', data: [220, 182, 191, 234, 290, 330, 310] } ] } }; } }; </script> <script module="chartRenderer" lang="renderjs"> import * as echarts from 'echarts'; function buildOption(data) { return { color: ['#5470C6', '#91CC75', '#FAC858'], tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, legend: { data: data.seriesData.map(item => item.name) }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: { type: 'category', data: data.categories }, yAxis: { type: 'value' }, series: data.seriesData.map(item => ({ name: item.name, type: 'bar', stack: 'total', emphasis: { focus: 'series' }, barWidth: '40%', data: item.data })) }; } export default { data() { return { chart: null }; }, mounted() { this.initChart(this.chartData); }, methods: { initChart(data) { const dom = document.getElementById('stackChart'); if (!dom) return; this.chart = echarts.init(dom); this.renderChart(data); this.bindResize(); }, renderChart(data) { if (!this.chart || !data) return; this.chart.setOption(buildOption(data)); }, bindResize() { if (typeof window === 'undefined' || !window.addEventListener) return; window.addEventListener('resize', this.handleResize, false); }, handleResize() { if (this.chart) { this.chart.resize(); } } }, watch: { chartData: { handler(newVal) { this.renderChart(newVal); }, deep: true } } }; </script> <style scoped> .page { padding: 30rpx; background-color: #fff; } .chart-container { width: 100%; height: 600rpx; } </style>

这个demo跑起来后,页面上应该会出现一组7根的堆叠柱状图,每根柱子上层是“线上”,中间是“门店”,下面是“分销”。鼠标悬浮可以看到单日三个渠道的详细数值和总和。

4.2 数据更新与自适应

实际项目里图表数据不可能写死。常见场景是进入页面后请求接口,拿到数据再渲染。这时候你只需要在普通script里改变chartData即可。比如在onLoad里模拟异步请求:

onLoad() { setTimeout(() => { this.chartData = { categories: ['一月', '二月', '三月', '四月'], seriesData: [ { name: '产品A', data: [150, 230, 224, 180] }, { name: '产品B', data: [80, 120, 90, 140] }, { name: '产品C', data: [60, 90, 110, 70] } ] }; }, 1000); }

renderjs里的watch会感知到chartData引用变化,然后重新调用renderChart。这里要注意,如果你想“增量更新”而不是整图重新setOption,也可以直接用this.chart.setOption(option),但那个option需要带上series索引,不如直接全量更新简单,数据量不大的场景性能差异可以忽略。

自适应方面,demo里已经监听了window.resize事件,设备横竖屏切换、浏览器窗口缩放时图表会跟着resize。如果你的页面使用了tabs切换,在tab内容从隐藏切换到显示时,容器可能从display:none变成显示,这时候容器宽度仍然是之前的,建议手动调用一次resize。可以从页面里通过事件通知renderjs执行resize,或者在切换动画结束后延迟触发。

4.3 踩过的坑:图表不显示、白屏、宽高为0

我第一次在renderjs里集成echarts时,最容易遇到的就是白屏。排查下来大多数原因集中在三点。

第一,容器没有高度。如果.chart-container只给了宽度,没有高度,或者高度是auto,echarts初始化时拿到的是0,自然渲染不出来。解决办法很简单,给明确的rpx高度,或者用计算属性动态设置。

第二,renderjs的mounted执行时机早于节点渲染。在某些情况下,mounted里执行document.getElementById('stackChart')可能会拿到null。我一般会在mounted里用一个小的定时器兜底:

this.$nextTick(() => { this.initChart(this.chartData); });

如果还不放心,就延迟100毫秒再初始化,基本能规避。

第三,初始化时机和尺寸变化冲突。如果页面使用了自定义导航栏、或内容区高度在页面加载后异步计算,那么图表初始化时容器尺寸可能还不是最终形态。这时候可以配合uni.createSelectorQuery()拿到实际节点信息再传给renderjs,或者在renderjs里先初始化,再利用resize事件修正尺寸。

5. 常见问题速查与优化建议

5.1 高频问题排查表

下面这张表是我在实际项目中遇到次数最多的几个问题,整理成速查格式,方便你按图索骥:

问题现象根本原因解决方案
图表区域空白容器无高度或初始高度为0给容器设置固定高度;或获取节点实际尺寸后再初始化
报错:Cannot read property 'getAttribute' of undefinedDOM节点还没渲染完成在$nextTick或setTimeout后初始化,并做节点判空
数据变了但图表不动普通script里的数据没有传到renderjs检查props绑定是否正确,renderjs的watch是否深度监听
图表宽度总是100px或塌陷容器父级是flex布局,且没有设置宽度给容器设置width: 100%,必要时给父级设置宽度
真机上触摸图例无响应或闪烁H5端点击事件延迟配置legend.type为普通图例,关闭不必要的动画
打包后包体积过大全量引入了echarts改用按需引入,只注册BarChart和必要组件

5.2 体积优化与按需加载

如果项目对首包体积敏感,全量echarts确实有点亏。echarts官方提供了按需引入的入口,renderjs里也支持这种写法。改写方式很简单,把import * as echarts from 'echarts'换成:

import * as echarts from 'echarts/core'; import { BarChart } from 'echarts/charts'; import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components'; import { CanvasRenderer } from 'echarts/renderers'; echarts.use([ BarChart, GridComponent, TooltipComponent, LegendComponent, CanvasRenderer ]);

注意renderjs运行环境本身是Webview渲染,所以使用CanvasRenderer没有问题。如果以后要跨端到小程序,那需要重新考虑渲染器。按需加载后,构建产物能小不少,配合Webpack或Vite的tree-shaking,效果比较明显。

5.3 跨端兼容提醒

renderjs只适用于App-Vue页面和H5页面,微信小程序、支付宝小程序等环境走不了这个方案。如果你的业务确实需要小程序端也展示同样的堆叠柱状图,建议直接使用uniapp生态里的ucharts,或者在小程序端单独引入echarts的小程序版本。我在一些混合项目中,会做一个图表组件,内部判断平台,H5/App用renderjs+echarts,小程序用ucharts,对外暴露同样的props和事件,业务层不用关心实现差异。这样做的成本不算高,但架构上会干净很多。

6. 一些提升体验的小技巧

6.1 自定义tooltip样式

堆叠柱状图的tooltip默认展示同一根柱子上所有系列的数据,但没有合计。你可以通过tooltip.formatter自定义内容,把总和加进去:

tooltip: { trigger: 'axis', formatter: function (params) { let total = 0; let html = params[0].axisValue + '</br>'; params.forEach(item => { total += item.value; html += item.marker + ' ' + item.seriesName + ':' + item.value + '</br>'; }); html += '合计:' + total; return html; } }

这样团队在分析数据时更直观,不用心算加总。如果需要展示百分比构成,也可以在此基础上按Math.round(item.value / total * 100)计算占比。

6.2 保持容器尺寸响应式

页面可能有横竖屏切换,或者左侧菜单折叠导致内容区宽度动态变化。遇到这类场景,只监听window.resize是不够的。我会使用ResizeObserver来观察容器DOM的尺寸变化,然后在回调中调用chart.resize(),实现更敏感的适配:

if (typeof ResizeObserver !== 'undefined') { this.observer = new ResizeObserver(() => { if (this.chart) { this.chart.resize(); } }); this.observer.observe(dom); }

注意页面卸载时记得调用observer.disconnect()和chart.dispose(),避免内存泄漏。renderjs模块里没有beforeDestroy钩子吗?和组件生命周期一致的,可以在beforeDestroy中处理一下。

6.3 loading状态与空数据

接口请求期间,可以先显示一个loading标识,有数据渲染图表,没数据就展示空态。我习惯把empty标志放在普通script里,模板上控制显示;同时给renderjs传入的chartData可以默认成空结构,保证即使没数据也不会初始化报错。


在实际项目里用renderjs+echarts这套方案已经跑了两年多,稳定性和灵活性都深得我心。尤其是在Vue页面需要复用Web端图表配置、做复杂交互时,echarts的成熟度确实能省不少自研成本。多花点心思在数据适配和生命周期时机上,后面几乎不会出什么大问题。如果你正在uniapp项目里纠结图表选型,可以先把这套方案跑通,再按业务需求去打磨细节。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 2:44:04

SVC与NuSVC分类实战:从原理到调参避坑全解析

简介&#xff1a;面向数据挖掘与机器学习初学者的支持向量机&#xff08;SVM&#xff09;分类模型学习包&#xff0c;涵盖 sklearn 中 SVC、NuSVC 与 LinearSVC 三种典型实现的原理讲解与可运行代码示例&#xff0c;附带线性核函数、支持向量、间隔、C 参数与 nu 参数等关键概念…

作者头像 李华
网站建设 2026/10/14 2:43:59

开题报告最大的骗局:你以为在“写”,其实是在“答”

毕夏AI官网&#xff1a;www.bixiaai.com&#xff0c;微信公众号搜一搜“毕夏AI官网”。 开题报告答辩现场&#xff0c;最怕听到的一句话是什么&#xff1f; “你这个研究问题&#xff0c;到底想问什么&#xff1f;” 你愣住了。你写了八千字的开题报告&#xff0c;引了四十…

作者头像 李华
网站建设 2026/10/14 2:42:55

Zemax单模光纤耦合仿真:模式匹配与物理光学传播设置解析

简介&#xff1a;Zemax模拟单模光纤的光束耦合是一份面向光学工程与Zemax使用者的专业范例文档&#xff0c;演示如何利用光学设计软件完成单模光纤耦合系统的搭建与优化。系统以SUSS MicroOptics FC-Q-250微透镜阵列耦合两根康宁SMF-28e光纤&#xff0c;物面到透镜距离与透镜到…

作者头像 李华
网站建设 2026/10/14 2:42:48

CNN特征图可视化实战:从张量处理到网格拼接的完整指南

1. 从一行文件名说起&#xff1a;show_featuremap.py 到底在解决什么问题第一次看到show_featuremap.py这个文件名&#xff0c;很多人会下意识觉得它只是个“小工具脚本”——毕竟名字里带show&#xff0c;听起来像是随手写来打印点东西的临时文件。但真正在卷积神经网络&#…

作者头像 李华
网站建设 2026/10/14 2:37:50

vue-devui Comment 评论组件:属性配置、插槽机制与自定义实战指南

前端UI组件设计系统 【免费下载链接】vue-devui 基于全新 DevUI Design 设计体系的 Vue3 组件库&#xff0c;面向研发工具的开源前端解决方案。 项目地址&#xff1a; https://gitcode.com/DevCloudFE/vue-devui 点击查看 免费下载 导读 Comment 是 vue-devui&#xff08;基于…

作者头像 李华
网站建设 2026/10/14 2:36:58

从黑名单到实时识别:深度学习钓鱼检测系统全栈落地

简介&#xff1a;一套基于深度学习的在线钓鱼页面检测系统完整项目源码与交付文件&#xff0c;分为 Chrome 插件前端、Django RESTful 后端与 TensorFlow 预测模块。后端从 URL 与页面中提取连接数量、外链比例、域名注册时间等特征&#xff0c;送入模型预测后将拦截或放行结论…

作者头像 李华