微信小程序 ECharts 双重柱状图实现
场景: 目标 vs 实际完成的对比是常见的分析场景之一。最近遇到了这样一个需求:需要在一个柱状图中同时展示“目标值”和“实际完成值”,并附上完成百分比;
分析: 单一柱状图只能展示一组数据,而我们的需求是:
- 展示每个品类的目标值(计划)
- 叠加展示实际完成值(执行)
实现思路: 通过双重柱状图(又称分组柱状图或重叠柱状图),同一坐标系下两组柱体并排的样式
效果图

数据格式与结构
- 仅参考,我这里ratio结合其他方式计算得到的,如果你是通过目标、实际完成计算比率的,直接formatter 处理即可;
const mockData = [
{ name: '西兰花', target: 300, actual: 374, ratio: '124.7' },
{ name: '菠菜', target: 4800, actual: 4200, ratio: '87.5' },
]
- 例:如果你的数据源不一样,你也可以提前组合好数据;能实现需求就行;最终echarts需要是这样的;
const categories = ['西兰花', '菠菜', …];
const targetData = [300, 4800, …];
const actualData = [374, 4200, …];
const rateData = ['124.7%', '87.5%', …];
关键配置项
// 百分比
const BAR_SLOT = 100;
const slot = BAR_SLOT;
const chartRows = mockData;
series: [
{
name: '目标',
type: 'bar',
barWidth: 20,
barGap: '-100%',
barCategoryGap: '35%',
z: 1,
itemStyle: {
color: 'rgba(146, 197, 255, 0.9)',
borderRadius: [4, 4, 4, 4],
},
data: chartRows.map((row) => (slot)),
},
{
name: '实际完成',
type: 'bar',
barWidth: 20,
barGap: '-100%',
barCategoryGap: '35%',
z: 10,
itemStyle: {
// 渐变色
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [{
offset: 0,
color: '#96e6a1'
},
{
offset: 1,
color: '#d4fc79'
},
]),
borderRadius: [4, 4, 4, 4],
},
data: chartRows.map((item) => (item.radio)
)),
},
],
注意:
barGap 控制不同系列柱子的间距,可以让两组柱子重叠;
完成率标注在柱子上方,通过 formatter 回调,结合传入的数据,利用 dataIndex 索引取对应百分比。
formatter: ({
dataIndex
}) => {
const row = chartRows[dataIndex];
if (!row) return '';
return `${row.actual}万/${row.ratio}%`;
},
微信小程序中的集成要点
这里作为组件使用的;可以自行修改
使用 ec-canvas 组件
echarts-for-weixin
我这里通过引入方式,你也可以通过npm方式 npm install echarts-for-weixin
在页面 json 中引用组件
{
"usingComponents": {
"ec-canvas": "../../ec-canvas/ec-canvas"
}
}
WXML 结构
<view class="echartsBox">
<ec-canvas id="{{ chartId }}" canvas-id="{{ canvasId }}" ec="{{ ec }}"></ec-canvas>
</view>
完整配置
import * as echarts from '../../ec-canvas/echarts';
const BAR_SLOT = 100;
/** 超出用 dataZoom 在图表内滚动 */
const VISIBLE_ROW_COUNT = 12;
let componentRef = null;
// 初始化
function ecOnInit(canvas, width, height, dpr) {
const chart = echarts.init(canvas, null, {
width,
height,
devicePixelRatio: dpr,
});
canvas.setChart(chart);
if (componentRef) {
componentRef.chartInstance = chart;
const dataList = componentRef.getSortedChartData();
chart.setOption(componentRef.buildChartOption(dataList));
}
return chart;
}
Component({
properties: {
chartId: {
type: String,
value: 'chart-bar',
},
canvasId: {
type: String,
value: 'canvas-bar',
},
chartTitle: {
type: String,
value: '',
},
chartData: {
type: Array,
value: [],
observer(newVal) {
this.applyChartData(newVal);
},
}
},
data: {
dataList: [],
ec: {
onInit: ecOnInit,
},
},
lifetimes: {
attached() {
componentRef = this;
this.applyChartData(this.properties.chartData);
},
detached() {
if (componentRef === this) {
componentRef = null;
}
this.chartInstance = null;
},
},
methods: {
// 加载数据
applyChartData(list) {
const dataList = list || [];
this.setData({
dataList
})
this.refreshChart();
},
// 数据排序(不需要的去掉)
getSortedChartData() {
const list = this.data.dataList;
return list.sort((a, b) => parseFloat(b.ratio) – parseFloat(a.ratio));
},
// 构建图表
buildChartOption(data) {
const slot = BAR_SLOT;
// 是否显示滑动条
const showZoom = data.length > VISIBLE_ROW_COUNT;
const zoomEnd = showZoom ?
(VISIBLE_ROW_COUNT / data.length) * 100 :
100;
return {
title: {
text: this.properties.chartTitle,
left: 'center',
textStyle: {
fontWeight: 'normal',
fontSize: 13,
color: '#474747',
},
},
legend: {
data: ['目标', '实际完成'],
bottom: 12,
itemWidth: 12,
itemHeight: 12,
textStyle: {
fontSize: 11,
color: '#666'
},
},
grid: {
left: 8,
right: 48,
bottom: 30,
top: 30,
containLabel: true,
},
xAxis: {
type: 'value',
min: 0,
max: slot,
axisLabel: {
show: false
},
splitLine: {
show: false
},
},
yAxis: {
type: 'category',
inverse: true,
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
color: '#474747',
fontSize: 11
},
data: data.map((item) => item.name),
},
series: [{
name: '目标',
type: 'bar',
barWidth: 20,
barGap: '-100%',
barCategoryGap: '35%',
z: 1,
itemStyle: {
color: 'rgba(190, 190, 190, 0.9)',
borderRadius: [4, 4, 4, 4],
},
label: {
show: true,
position: 'right',
distance: 4,
fontSize: 11,
color: '#666',
formatter: (params) => {
const row = data[params.dataIndex];
return row ? `${row.target}万` : '';
},
},
data: data.map(() => slot),
},
{
name: '实际完成',
type: 'bar',
barWidth: 20,
barGap: '-100%',
barCategoryGap: '35%',
z: 10,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [{
offset: 0,
color: '#FFA500'
},
{
offset: 1,
color: '#FF4500'
},
]),
borderRadius: [4, 4, 4, 4],
},
label: {
show: true,
position: 'inside',
align: 'center',
verticalAlign: 'middle',
formatter: ({
dataIndex
}) => {
const row = data[dataIndex];
return `${row.actual}万/${row.ratio}%`;
},
fontSize: 11,
color: '#fff',
},
data: chartRows.map((item) => (item.radio)
},
],
// 滑动条
dataZoom: [{
show: showZoom,
type: 'slider',
realtime: true,
orient: 'vertical',
yAxisIndex: 0,
left: '96%',
bottom: '12%',
width: 6,
height: '82%',
start: 0,
end: zoomEnd,
minSpan: zoomEnd,
maxSpan: zoomEnd,
zoomOnMouseWheel: false,
moveOnMouseWheel: true,
moveOnMouseMove: true,
zoomLock: true,
brushSelect: false,
showDetail: false,
moveHandleStyle: {
opacity: 0
},
}],
};
},
refreshChart() {
const dataList = this.getSortedChartData();
if (this.chartInstance) {
this.chartInstance.setOption(this.buildChartOption(dataList), true);
}
},
},
});
使用组件
<diyEcBar
chartTitle="2026年出口蔬菜完成情况(按完成率从高到低)"
chartData="{{ chartData }}"
/>
{
"component": true,
"usingComponents": {
"diyEcBar": "../../diyEcBar/index",
}
}
希望这篇文章能帮到同样在移动端做数据可视化的你。如果遇到问题或想探讨更多图表类型,欢迎评论区交流!





