ECharts高级技巧:用custom系列绘制逼真圆柱体柱状图

发布时间:2026/10/14 2:32:02
ECharts高级技巧:用custom系列绘制逼真圆柱体柱状图
这些年做大屏可视化项目经常会遇到一种挺头疼的需求普通柱状图太平了客户想要“立体感”哪怕数据不加任何东西光看图表就想要那种高级大屏的质感。传统的做法要么换3D图表库要么用纯自己画的Canvas方案两个都太重。ECharts其实可以做得非常巧妙——用一个几乎不增加额外体积的方案实现带有圆柱体效果的立体柱状图效果好、兼容性稳、性能也扛得住。这篇内容就围绕“EChart绘制立体柱状图圆柱体”展开讲清楚实现原理、完整步骤、避坑经验适合正在做数据可视化大屏、想提升图表观感的开发者参考同样也适合刚接触ECharts不久但想玩点花样的前端新手。不用引入额外三维库纯配置加少量代码就能实现很实用。1. 实现思路为什么默认图表做不到“圆柱体”先解决一个关键问题ECharts内置的柱状图本质上是四四方方的矩形。你设置barBorderRadius可以把顶部变成圆角但它依然是若干条边组合出来的一个棱柱体不是真正的圆柱。如果你只是想要一个看上去是圆柱的柱子直接指望默认bar系列是做不到的必须换一条路。我最初接到这个需求时也考虑过引入echarts-gl。这个库确实能生成真正的圆柱体里面提供了bar3D系列配合三维坐标系可以模型化地生成任意立体形状。但实际用下来有几个问题体积比较大加载多出几百K对性能敏感型页面来说不划算坐标系变了原来的grid轴、tooltip交互、数据下钻等逻辑都要跟着调整部分情况下和大屏低版本浏览器的兼容会有问题移动端也不够稳。所以当团队里有人提出“要不要试试用渐变模拟圆柱体”时我特意花了一下午做了个原型。结论是用ECharts的custom系列配合横向线性渐变再加顶部椭圆阴影和底部的投影就能做出非常接近真实圆柱体效果的外观。这是目前在原生ECharts结构下最稳妥、最性价比高的方案。原理其实很简单就是我们看着一个圆柱体时视觉上的立体感来自两个方面圆柱体表面高光区域集中在侧面上端离光源近的一侧亮、远的一侧暗圆柱体底部有轻微反射阴影圆柱顶部看过去是一个椭圆椭圆内部有深浅层次。因此只要在柱体上构造一个“中间亮、左右暗”的渐变再加顶部椭圆和底部阴影大脑就会自动脑补出圆柱轮廓。这个原理做平面设计的朋友不陌生在CSS、SVG、Canvas里都能套用只不过我们需要把它落到ECharts的绘制机制里。2. 核心细节解析圆柱体视觉的三要素无论用什么方式去实现一个让人信服的立体柱状图都离不开三个核心要素。理解了这三个要素后面写代码只是把它们翻译成ECharts的配置而已。2.1 横向渐变圆柱的“皮肤”渐变是整个圆柱效果的关键。普通的bar系列也支持color渐变但它只会沿着柱子的高度方向纵向渐变顶部到尾部。而圆柱体的明暗变化应该是横向的——从左到右先亮、再暗、再亮中间偏左的位置模拟高光最右侧压暗模拟阴影。ECharts的自定义custom系列中我们有一个renderItem函数这个函数允许我们完全控制每个元素的绘制方式。使用echarts.graphic.LinearGradient可以构造任意方向的渐变针对圆柱体的特征我会设置const cylinderGradient new echarts.graphic.LinearGradient(0, 0, 1, 0, [ { offset: 0, color: #6ca0ff }, // 最左边缘稍暗 { offset: 0.25, color: #9bc0ff }, // 高光区域 { offset: 0.5, color: #4f87ff }, // 中间过渡 { offset: 0.82, color: #2f6fe0 }, // 右侧暗部 { offset: 1, color: #1f56b5 }, // 右边缘最深 ]);四个到五个色标基本够用。如果追求更平滑可以加到六个。但实际渲染时我发现色标太多反而在高分辨率大屏上会出现微弱的色带断层五个以内最稳。颜色一般选用同一色系的亮、中、深三个主色调高光区域不要太白否则会显得像塑料而不是金属质感太灰了又会显得脏需要反复调几次。2.2 顶部椭圆圆柱体的“盖子”一个圆柱体最明显的特征是俯视时看到顶部是一个椭圆。ECharts的自定义元素里可以使用ellipse图形。我在renderItem里会把顶部画成一个ellipse其长轴刚好等于柱体宽度短轴则根据视觉效果设为一个固定比例。这个比例没有绝对标准窄的柱子比如宽度20以下短轴可以设为宽度的0.3倍左右如果是宽柱40以上比例调整到0.25会更自然。理论上如果非要按透视角去严格算也可以但实际上不需要人眼对椭圆扁率的接受范围很宽只要方向对了效果就出来了。顶部椭圆内部也需要一个渐变颜色比侧面亮一些。由于顶部在光照下通常受光较强我一般会用侧面向导出的高光颜色再叠加一层透明度渐变从靠近光源的一侧偏亮到另一侧偏暗。2.3 底部阴影让柱子“站得住”一个元素如果底部没有影子看起来会像浮在平面上。普通bar系列也有itemStyle.shadowColor和shadowBlur但那只是给矩形加模糊阴影对于圆柱体来说我们需要的是椭圆形的接地阴影。做法很简单依然是画一个ellipse放在柱子的底部正中央颜色是深灰色或者黑色的半透明短轴比顶部椭圆稍微压扁一点然后让它稍微往左偏一些模拟光源在左上方的投影方向。我用的是rgba(0,0,0,0.15)、blur设置为46在小面积投影时既不会脏也不会太假。3. 实操过程用custom系列完成圆柱体柱状图下面给出一个可以直接运行参考的完整方案。我用的是custom系列先说明几个关键配置的含义再给完整代码。这个方案整体上分为三个部分构造数据坐标、编写renderItem绘制函数、输出图表配置。3.1 搭建坐标系圆柱柱状图本质还是柱状图定位坐标都用类目轴或时间轴作为横向定位数值轴作为高度定位。为了让绘制时有足够空间进行操作比如画阴影在柱子底部会稍微超出一点坐标范围我给grid设置了一些边距y轴留出10%的头部空间max避免顶部椭圆被裁剪掉。const option { grid: { left: 40, right: 40, top: 40, bottom: 30 }, xAxis: { type: category, data: [一月, 二月, 三月, 四月, 五月], axisTick: { show: false }, axisLine: { lineStyle: { color: #ccc } } }, yAxis: { type: value, splitLine: { lineStyle: { color: #eee, type: dashed } } }, series: [{ type: custom, renderItem: renderCylinder, data: [120, 200, 150, 260, 180], encode: { x: 0, y: 1 } }] };encode不一定必须但建议写上这样tooltip、legend以及一些数据筛选功能可以自动正常工作。X轴和Y轴的数据对应关系在renderItem里也可以通过api.value(0)和api.value(1)拿到后面代码会用到。3.2 编写renderItem绘制函数renderItem是整个方案的灵魂它会被ECharts在渲染每个数据点时自动调用我们需要返回一个或者多个绘制元素的分组。以下是完整代码function renderCylinder(params, api) { // 拿到类目索引和数值 const categoryIndex api.value(0); const value api.value(1); // 通过api获取柱体所在位置 const start api.coord([categoryIndex, 0]); // 底部中心坐标 const end api.coord([categoryIndex, value]); // 顶部中心坐标 // 计算柱体的宽度。这里用类目轴bandWidth来保证柱体撑满对应分组 const bandWidth api.size([1, 0])[0]; const barWidth bandWidth * 0.6; // 按需调整太宽或太窄都会影响立体观感 // 柱子高度 const height start[1] - end[1]; const left end[0] - barWidth / 2; const top end[1]; const bottom start[1]; // 横向渐变圆柱体侧面 const cylinderGradient new echarts.graphic.LinearGradient(0, 0, 1, 0, [ { offset: 0, color: #8ec1ff }, { offset: 0.3, color: #b5d8ff }, { offset: 0.5, color: #6ca3f0 }, { offset: 0.8, color: #2f69cc }, { offset: 1, color: #1b4e9e } ]); // 顶部椭圆 const capGradient new echarts.graphic.LinearGradient(0, 0, 1, 0, [ { offset: 0, color: #c9e2ff }, { offset: 0.5, color: #9cc2f6 }, { offset: 1, color: #5b8ad4 } ]); // 底部接地阴影参数 const shadowOffset barWidth * 0.08; return { type: group, children: [ // 1. 最底层的接地阴影椭圆形半透明深色 { type: ellipse, shape: { cx: left barWidth / 2 - shadowOffset, cy: bottom - 3, rx: barWidth / 2 shadowOffset, ry: Math.max(barWidth * 0.2, 5) }, style: { fill: rgba(0,0,0,0.18), blur: 6 }, silent: true }, // 2. 圆柱体主体矩形顶部圆角用横向渐变填充 { type: rect, shape: { x: left, y: top, width: barWidth, height: height }, style: { fill: cylinderGradient, r: [barWidth / 2, barWidth / 2, 0, 0] // 让顶部呈现半圆模拟圆柱侧面的顶边 }, textContent: { style: { text: api.value(1), fill: #fff, fontSize: 12 } }, textConfig: { position: insideTop, distance: 6 } }, // 3. 顶部椭圆盖 { type: ellipse, shape: { cx: left barWidth / 2, cy: top, rx: barWidth / 2, ry: Math.max(barWidth * 0.25, 4) }, style: { fill: capGradient, stroke: #7aa5df, lineWidth: 1 }, silent: true } ] }; }这段代码里有个小技巧矩形主体的r圆角半径设置成了barWidth / 2这意味着矩形上面两端是完全的半圆视觉上和顶部椭圆是衔接的。如果不设置这个半圆矩形顶端会和椭圆有一个明显的“缺口”圆柱感会大打折扣。textContent这块是给柱子加数据标签一般用白色和半透明底或者干脆不放。放标签时注意如果柱高很矮标签会和顶部椭圆挤在一起这时可以把distance调大或者直接用position: top。3.3 完整option组装与工具提示联动custom系列的tooltip默认是显示原始数据值如果想展示更丰富的指示器比如百分比、单位等需要额外配置formatter。我一般这样写tooltip: { trigger: axis, formatter: function(params) { const item params[0]; if (!item) return ; return strong${item.name}/strongbr/销售额span stylecolor:#2f6fe0;font-weight:bold;${item.value[1]}/span 万元; } }需要注意的是在custom系列里params[0].value的结构取决于你传入data的方式。如果直接传入[120, 200, 150, 260, 180]这种一维数组其value就是原始数值如果传入二维数组[[0, 120], [1, 200]]那value就是[categoryIndex, value]。我的renderItem里使用api.value(0)和api.value(1)来分别取是为了兼容不同data格式建议正式项目中都用二维数组方式传入语义更明确。4. 常见问题与排查技巧实录这个方案我调试过很多轮踩的坑也不少。下面把几个特别典型的问题列出来基本涵盖了大家自己写的时候最常遇到的麻烦。4.1 柱子变成了圆角矩形但不是圆柱这个问题十有八九是没画顶部椭圆或者椭圆的位置不对。顶部椭圆的作用是给圆柱“封顶”如果把顶部椭圆的cy设置到了柱体中间看起来就会像一个胶囊而不是柱体。正确做法是让cy精确等于柱子上边缘的Y坐标也就是代码里end[1]。另外矩形主体的顶部圆角r也很关键。如果只设置r: [20, 20, 0, 0]当柱子宽度变化时圆角不会跟着变化柱子又宽又矮时会产生奇怪的“枕头”形状。所以建议动态计算r: [barWidth / 2, barWidth / 2, 0, 0]这样无论柱宽怎么变化顶部永远是个完美的半圆。4.2 渐变方向总是竖着的很多人一写渐变就用LinearGradient(0, 0, 0, 1, ...)这是纵向渐变完全反了。圆柱体的明暗是横向变化的所以前四个参数必须是0, 0, 1, 0。第一个和第三个参数代表渐变起点和终点的X坐标比例第二个和第四个是Y坐标比例。如果你想要更加精确的渐变方向比如光源在左上方可以让终点稍微带一点Y方向偏移比如0, 0, 0.85, 0.15这样高光区域会略微倾斜接近真实光照方向。实测这个细节对整体质感提升很明显。4.3 底部阴影看起来很脏接地阴影是一个很微妙的东西半透明度、偏移量、模糊度都影响最终效果。我在调试中发现最容易出现的两个问题阴影太黑rgba(0,0,0,0.5)这种直接放弃通常0.12到0.2之间比较舒服阴影和柱子底部离得太近糊成一团需要让阴影的cy比柱底坐标低3到5像素同时让ry稍微大一点把阴影压扁、拉宽才会像真实接触投影。注意在ECharts的custom系列中blur字段在style里是被支持的但不同渲染模式下表现略有差异。如果在Canvas模式下发现blur没有效果可以检查是否用了SVG渲染器SVG模式下blur对fill的支持在部分版本中不一致建议统一使用Canvas模式。4.4 标签和tooltip位置对不上由于柱体绘制多了几个元素工具提示默认锚定中心点可能不准。另一种情况是点击高亮区域异常因为顶层椭圆会拦截鼠标事件。解决方法是把视觉辅助元素顶部椭圆、底部阴影设置silent: true这样鼠标事件只会响应矩形主体交互定位会准确很多。数据标签文字也会在交互时更自然。4.5 柱宽自适应问题类目轴分组时api.size([1, 0])[0]可以拿到一个类目的像素宽度。但要特别注意如果你在series.data里用的是普通一维数组这个方法是有效的但如果你在dataset模式下使用尺寸可能需要通过api.size([1, 0])不同传参来获取。我自己更推荐直接写data数组避免引入不必要的复杂度。还有一种情况是坐标轴被设置为log类型柱宽计算可能要换算一般不会拿来做柱状图这里不展开。5. 进阶优化给圆柱柱状图加上动效和大屏质感基础方案能够运行之后做一些锦上添花的优化能让整张图直接拥有“大屏定制”的感觉。5.1 入场生长动画ECharts的custom支持transition相关配置但默认样式的过渡在某些情况下不够平滑。我做过最简单有效的做法是让Y轴起始值从0开始配合animationDuration和animationEasing。animationDuration: 1000, animationEasing: cubicOut,如果希望柱子像从地里长出来一样可以给矩形主体的height做一个动态变化的transition在renderItem里返回时将height乘以一个随时间增长的比例。不过实操下来ECharts对custom返回元素的shape属性的动画支持还不够智能更稳妥的做法是放弃这种精细动画用整个系列的初始动画即可。起步滚动效果已经足够让人感觉精致了。5.2 多维度对比和叠加当你有两组数据需要对比时可以写两个custom系列然后通过barGap属性控制不同系列的柱子间距。custom系列没有默认的barGap需要手动计算偏移第二组柱子的x坐标在第一组的基础上加上barWidth / 2 gap / 2即可。实现上并不复杂。我通常会在series里增加一个seriesIndex字段区分柱组编号然后在renderCylinder里根据它计算偏移量。5.3 折线叠加的提示增强如果图表里同时还有一条趋势折线圆柱柱体就不建议每个柱子都显示数据标签否则视觉会很挤。把textContent去掉只保留tooltip即可。如果确实要在柱顶显示数值建议字号控制在10左右颜色使用和柱身主色同色系的深色比如柱身最亮色加深30%的透明度。一个意外的优化是当柱顶标签文字带有浅色描边时在所有背景下都可读性都很高。在textContent.style里加textBorderColor: rgba(0,0,0,0.2)和textBorderWidth: 1效果会立刻清爽很多。5.4 光影偏色处理不同主题下圆柱体的颜色并不是简单换个基础色就行的。比如蓝色系的柱身暗部颜色应当向紫色或者青色偏而不是单纯变黑。这样更有通透感。一个快捷做法把cylinderGradient的最后一个色标颜色从主色转成rgb后分别乘以0.20.3系数作为暗部。例如主色是rgb(65, 125, 240)暗部就使用rgb(20, 45, 100)高光部分则使用rgb(150, 185, 255)。6. 总结一下我在实操中的几个体会圆柱体柱状图做出来不难但想做得好看关键是别把三种视觉要素割裂开。横向渐变决定质感是立体感的核心顶部椭圆决定形态是圆柱辨识度的关键底部阴影决定存在感是“接地”的保障。三者缺一效果都会打折扣。另外在团队协作时这段绘图函数最好封装成一个独立模块暴露一个接收颜色数组、柱宽比例等参数的配置函数不要让业务方去直接改渐变数组。否则每次改颜色都要找半天维护成本很高。还可以把它封装成一个自定义系列的工厂函数后续即使换成深色主题只需要重新调用一次即可。最后分享一个小技巧调试圆柱体效果时建议把数据调成两个极端来预览——一个很矮的柱子比如值为10和一个很高的柱子比如值为1000这样能最快发现椭圆比例和柱宽适配的问题。如果这两个极端都能呈现自然效果那你的参数就调通了大半。我每次改完代码都是这么验证的比反复看效果图快得多。