恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
ECharts动态排名柱状图实现:自适应布局与平滑动画实战
首页
资讯中心
/
ECharts动态排名柱状图实现:自适应布局与平滑动画实战
ECharts动态排名柱状图实现:自适应布局与平滑动画实战
发布时间:2026/9/2 14:03:16
简介本资源是一套基于ECharts实现的动态排名柱状图完整前端工程面向Web开发初学者与数据可视化实践者解决多端适配下实时榜单展示的核心需求。项目通过定时刷新数据、自动重排名次、响应式布局等关键技术实现PC与手机端无缝切换的交互式图表体验适用于运营看板、赛事排名、销售实时监控等业务场景。压缩包共9个文件3个JS脚本含ECharts5核心库与配置逻辑、1个HTML主入口、1个CSS样式文件支持媒体查询、1个JSON模拟数据源、1个XLS原始数据表、1个EXE启动工具及1个说明文档整体6.87MB结构清晰、模块职责分明。已有3783人学习下载提供开箱即用的完整运行环境——含数据加载逻辑、自适应尺寸计算、动态排序算法封装及跨设备渲染调试要点助开发者快速掌握ECharts高级配置与响应式图表开发实战能力。1. 项目缘起为什么我们需要一个自适应的动态排名柱状图最近在做一个数据大屏项目后台实时推送着各个业务线的排名数据前端需要以一种足够“抓眼球”又清晰的方式展示出来。静态的柱状图太死板而ECharts的常规动态柱状图例子要么是固定宽高在手机上一团糟要么是只考虑了宽度自适应排名变化时的动画效果生硬柱子跳来跳去看得人眼花。这让我意识到一个真正能投入生产环境的“动态排名柱状图”远不止是调用个setOption那么简单。它需要同时解决三个核心痛点第一视觉表现力排名变化时柱子的高度、位置、标签要有一个流畅、符合直觉的过渡动画第二布局自适应必须能在从4K大屏到iPhone SE小屏的各种设备上都保持清晰可读的布局和比例第三性能与体验频繁的数据更新不能卡顿动画要顺滑交互要友好。网上搜了一圈虽然有很多ECharts的示例但要么只讲动态要么只讲自适应把两者结合并且处理好细节的完整方案很少。于是我决定自己动手从零开始封装一个。经过几轮迭代和真实项目的打磨现在这个组件已经非常稳定了。今天我就把这个支持动态排名更新、完美自适应PC与移动端、且包含完整源代码的实现方案分享出来你可以直接拿去用在你的项目里。2. 核心设计思路如何让柱状图“动”得优雅且“适应”得自然在动手写代码之前我们先厘清设计思路。一个动态排名的柱状图其核心交互是当新数据到来时旧数据的排名可能发生变化对应的柱子需要移动到新的位置新的排名索引并更新到新的高度新的数值。这个过程如果直接硬切会非常突兀。我们的目标是实现一个平滑的、带缓动效果的过渡动画让用户能清晰地追踪到某个具体数据项排名的上升或下降。同时自适应布局要求我们的图表容器和内部元素如坐标轴、标签、图形的尺寸能根据父容器的宽度动态计算而不是写死像素值。这里的关键在于利用ECharts强大的resize方法和配置项中的百分比、函数式定义。我的设计拆解为以下几个关键部分数据结构与状态管理我们需要维护两份核心数据一是当前正在屏幕上显示的currentData二是最新接收到的nextData。动画的本质就是让currentData状态逐步过渡到nextData状态。动画策略ECharts本身提供了图形元素graphic和系列数据series.data的过渡动画。但直接更新整个series.dataECharts会为每个数据项计算新旧值的差异并应用默认过渡这可能导致柱子交叉飞行逻辑混乱。更优的策略是我们预先计算好每个数据项从旧排名到新排名的位置索引变化然后利用ECharts的animationDelay动画延迟功能按序触发每个柱子的移动和变形动画形成一种“接力”或“波浪”式的视觉效果逻辑清晰且美观。自适应策略PC端屏幕宽我们可以展示更详细的标签、更复杂的图例移动端屏幕窄则需要精简元素、放大字体、调整布局。我将通过媒体查询CSS Media Queries监听容器宽度变化并据此生成两套不同的ECharts配置项option。当屏幕尺寸切换时调用myChart.resize()并更新对应的option。性能优化频繁的动画和重绘可能成为性能瓶颈。我会使用函数节流throttle来处理窗口resize事件并合理设置动画时长和缓动函数确保60fps的流畅体验。下面我们就进入具体的实现环节。3. 基础环境搭建与ECharts引入首先创建一个标准的HTML项目结构。我们将使用ES6语法并通过CDN引入ECharts。为了更好的开发体验我也会引入lodash的throttle函数用于性能优化。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title自适应动态排名柱状图/title script srchttps://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js/script script srchttps://cdn.jsdelivr.net/npm/lodash4.17.21/lodash.min.js/script style * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif; padding: 20px; background: #f5f7fa; min-height: 100vh; } #chart-container { width: 100%; max-width: 1200px; height: 600px; /* 初始高度将在JS中自适应 */ margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08); padding: 20px; transition: all 0.3s ease; } media (max-width: 768px) { body { padding: 10px; } #chart-container { height: 500px; /* 移动端调整高度 */ padding: 12px; border-radius: 8px; } } .controls { text-align: center; margin-top: 25px; } button { padding: 12px 28px; margin: 0 10px; border: none; border-radius: 6px; background: #4a6cf7; color: white; font-size: 16px; cursor: pointer; transition: background 0.2s; } button:hover { background: #3a56d8; } /style /head body div idchart-container/div div classcontrols button idbtn-update模拟数据更新/button button idbtn-reset重置数据/button /div script srcmain.js/script /body /html注意这里将图表的初始高度设为600px并在移动端媒体查询中调整为500px。在实际项目中更推荐使用calc(100vh - 某个固定值)或由父元素绝对定位来决定高度以实现真正的流体布局。本例为了演示清晰采用了固定值。接下来在main.js中我们开始编写核心逻辑。4. 图表初始化与自适应配置生成器图表初始化的第一步是获取DOM容器并初始化ECharts实例。紧接着我们需要一个核心函数getChartOption它根据当前的isMobile标志是否移动端来生成两套不同的配置。// main.js (function() { use strict; // 获取容器和初始化图表 const chartDom document.getElementById(chart-container); const myChart echarts.init(chartDom, null, { renderer: canvas, // 使用canvas渲染性能更好 useDirtyRect: true // 开启脏矩形渲染提升动画性能 }); // 判断是否为移动端的函数基于容器宽度更可靠 function isMobileMode() { // 通常以768px作为分界点可根据项目调整 return chartDom.clientWidth 768; } // 核心生成ECharts配置项的函数 function getChartOption(data, isMobile) { // 对数据进行排序确保排名顺序 const sortedData [...data].sort((a, b) b.value - a.value); const categories sortedData.map(item item.name); const values sortedData.map(item item.value); // 基础颜色系列 const colorList [#5470c6, #91cc75, #fac858, #ee6666, #73c0de, #3ba272, #fc8452, #9a60b4]; // 根据设备类型调整参数 const fontSize isMobile ? 14 : 16; const axisLabelRotate isMobile ? 45 : 0; // 移动端X轴标签旋转以防重叠 const gridTop isMobile ? 15% : 10%; const gridBottom isMobile ? 25% : 15%; // 移动端底部留更多空间给旋转的标签 const barWidth isMobile ? 60% : 70%; // 移动端柱子稍窄 const option { // 动画配置 animation: true, animationDuration: 1000, animationEasing: cubicOut, // 缓动函数使动画结尾更自然 animationThreshold: 2000, // 响应式布局网格 grid: { top: gridTop, left: isMobile ? 12% : 10%, // 移动端Y轴标签区域稍宽 right: 5%, bottom: gridBottom, containLabel: true // 确保坐标轴标签在grid内 }, // X轴类别轴显示排名名次 xAxis: { type: category, data: categories, axisLabel: { fontSize: fontSize, color: #666, rotate: axisLabelRotate, // 旋转角度 interval: 0, // 强制显示所有标签 margin: isMobile ? 20 : 10 // 标签与轴线的距离 }, axisTick: { alignWithLabel: true, length: 4 }, axisLine: { lineStyle: { color: #ddd } } }, // Y轴数值轴 yAxis: { type: value, name: 数值, nameTextStyle: { fontSize: fontSize, color: #999 }, axisLabel: { fontSize: fontSize, color: #666, formatter: {value} // 可改为 {value} 单位 }, axisLine: { show: true, lineStyle: { color: #ddd } }, splitLine: { lineStyle: { type: dashed, color: #eee } } }, // 数据系列 - 柱状图 series: [{ name: 排名数据, type: bar, data: values.map((value, index) ({ value: value, itemStyle: { color: colorList[index % colorList.length], borderRadius: [4, 4, 0, 0] // 柱子顶部圆角 }, // 每个柱子的标签配置 label: { show: true, position: top, fontSize: fontSize - 2, fontWeight: bold, color: #333, formatter: function(params) { // 显示数值和排名 const rank index 1; return {a|${rank}.} {b|${params.value}}; }, rich: { a: { color: #888, fontSize: fontSize - 4 }, b: { color: inherit // 继承柱子颜色 } } } })), barWidth: barWidth, // 关键启用渐进式动画并设置动画延迟函数 animationDelay: function (idx) { // 排名越靠前索引越小动画越早开始形成波浪效果 return idx * 100; // 每个柱子延迟100ms }, animationDelayUpdate: function (idx) { // 数据更新时的动画延迟同样策略 return idx * 100; } }], // 工具提示 tooltip: { trigger: axis, axisPointer: { type: shadow }, formatter: function (params) { const data params[0]; const rank data.dataIndex 1; return ${data.name}br/ 排名b第${rank}名/bbr/ 数值b stylecolor:${data.color};${data.value}/b; } }, // 在移动端隐藏图例PC端显示 legend: isMobile ? { show: false } : { data: [排名数据], top: 2%, textStyle: { fontSize: fontSize } }, // 数据区域缩放PC端可用移动端隐藏 dataZoom: isMobile ? [] : [{ type: inside, xAxisIndex: 0, start: 0, end: 100, zoomLock: true // 锁定缩放比例避免用户误操作破坏排名视图 }] }; return option; } // 初始模拟数据 let currentData [ { name: 产品A, value: 235 }, { name: 产品B, value: 187 }, { name: 产品C, value: 312 }, { name: 产品D, value: 278 }, { name: 产品E, value: 154 }, { name: 产品F, value: 198 }, { name: 产品G, value: 265 } ]; // 首次渲染 myChart.setOption(getChartOption(currentData, isMobileMode())); // 窗口大小改变时重绘图表 const handleResize _.throttle(function() { myChart.resize(); // 重要窗口大小变化可能导致设备类型切换需要重新setOption myChart.setOption(getChartOption(currentData, isMobileMode())); }, 300); // 300ms节流 window.addEventListener(resize, handleResize); // 模拟数据更新函数 window.simulateDataUpdate function() { // 深拷贝当前数据并模拟变化 const newData currentData.map(item ({ ...item, value: Math.max(50, item.value (Math.random() - 0.5) * 100) // 在原有值基础上随机增减 })); updateChartWithAnimation(newData); }; // 重置数据函数 window.resetData function() { const originalData [ { name: 产品A, value: 235 }, { name: 产品B, value: 187 }, { name: 产品C, value: 312 }, { name: 产品D, value: 278 }, { name: 产品E, value: 154 }, { name: 产品F, value: 198 }, { name: 产品G, value: 265 } ]; updateChartWithAnimation(originalData); }; // 为按钮绑定事件 document.getElementById(btn-update).addEventListener(click, window.simulateDataUpdate); document.getElementById(btn-reset).addEventListener(click, window.resetData); // 关键函数带动画的数据更新 function updateChartWithAnimation(nextData) { // 1. 计算新旧数据的排名映射 const oldSorted [...currentData].sort((a, b) b.value - a.value); const newSorted [...nextData].sort((a, b) b.value - a.value); // 创建一个从数据名到新排名字典 const nameToNewRankIndex {}; newSorted.forEach((item, index) { nameToNewRankIndex[item.name] index; }); // 2. 为了动画平滑我们需要构建一个按“旧排名”顺序排列但包含“新值”和“新排名目标位置”的数据结构 // 但ECharts的series.data顺序决定了X轴上的位置。所以我们直接设置最终排序后的数据。 // 动画的奥秘在于 animationDelayUpdate它让柱子按顺序旧排名顺序开始移动。 // 然而ECharts的默认过渡是基于数据索引的。如果“产品A”从第1名变成第3名 // 在数据数组中它的索引从0变成了2ECharts会让对应的图形从X轴0位置移动到2位置。 // 这正是我们想要的 // 3. 更新当前数据引用 currentData nextData; // 4. 获取最新的配置项包含新的排序数据 const newOption getChartOption(currentData, isMobileMode()); // 5. 关键步骤在设置新option前确保图表知道我们要进行过渡动画更新而不是完全替换 // ECharts的setOption在传入notMerge: false默认时会合并新旧option并触发过渡。 // 我们需要确保series.data的变化能被正确识别为“数据更新”。 myChart.setOption(newOption, { notMerge: false, // 合并模式触发过渡动画 lazyUpdate: false // 立即更新 }); // 6. 可选在控制台输出排名变化便于调试 console.log(数据已更新新排名); newSorted.forEach((item, idx) { const oldItem oldSorted.find(old old.name item.name); const oldRank oldSorted.indexOf(oldItem) 1; const newRank idx 1; const change oldRank - newRank; let changeSymbol ; if (change 0) changeSymbol ↑上升${change}名; else if (change 0) changeSymbol ↓下降${-change}名; else changeSymbol →持平; console.log(${newRank}. ${item.name}: ${item.value} (${changeSymbol})); }); } // 初始模拟一次更新展示动画效果 setTimeout(window.simulateDataUpdate, 2000); })();这段代码已经实现了一个基础的自适应动态排名柱状图。getChartOption函数根据isMobile标志返回两套不同的配置核心差异在于字体大小、布局间距、标签旋转和组件的显隐。updateChartWithAnimation函数处理数据更新的逻辑它计算新旧排名并依靠ECharts内置的数据驱动和动画延迟(animationDelayUpdate)来实现平滑过渡。5. 高级优化解决柱子跳跃与标签重叠问题上面的基础版本在大多数情况下工作良好但在两个极端场景下会有问题第一当某个数据项的数值剧烈变化导致排名跨越多位时例如从第7名直接跃升到第1名其对应的柱子会“飞越”其他柱子动画路径可能交叉视觉上稍显混乱。第二在移动端即使旋转了X轴标签当数据项名称很长或项数很多时仍然会发生严重的重叠。针对问题一柱子跳跃我们可以采用一种称为“关键帧补间”的策略来优化视觉体验。虽然ECharts不直接支持自定义图形运动路径但我们可以通过分阶段动画来模拟。思路是在瞬间更新数据到最终排序状态之前先让所有柱子“淡出”或缩小然后在新位置“淡入”或放大。但这会失去柱子连续移动的追踪感。另一种更优的、保留追踪感的方案是使用ECharts的自定义系列custom series或图形组件graphic来手动绘制柱子和控制动画但这复杂度陡增。对于大多数排名应用跨越多位的情况并不频繁且ECharts默认的基于索引的移动动画本身已能清晰表达“排名变化”这一核心信息。因此一个实用的优化点是调整动画缓动函数。将animationEasing从cubicOut改为elasticOut可以让柱子的移动带有轻微的弹性效果视觉上更柔和能一定程度上缓解“跳跃”的突兀感。// 在getChartOption函数的option中修改 animationEasing: isMobile ? cubicOut : elasticOut, // PC端用弹性动画移动端用标准缓动保持性能 animationDurationUpdate: 1200, // 更新动画时间稍长针对问题二标签重叠则需要更细致的策略。ECharts的axisLabel.interval可以控制标签显示间隔设为0是全部显示但这必然导致重叠。我们可以写一个函数根据容器宽度、字体大小和标签数量动态计算一个合理的interval值。// 在getChartOption函数内部计算xAxis.axisLabel之前添加 function calculateLabelInterval(categories, containerWidth, fontSize) { // 估算每个标签所占宽度假设字符平均宽度为fontSize * 0.6加上旋转后的额外空间 const avgCharCount categories.reduce((sum, name) sum name.length, 0) / categories.length; const estimatedLabelWidth avgCharCount * fontSize * 0.6 20; // 加上边距 const availableWidth containerWidth * 0.8; // 假设X轴占用80%容器宽度 const maxLabelsWithoutOverlap Math.floor(availableWidth / estimatedLabelWidth); if (categories.length maxLabelsWithoutOverlap) { return 0; // 全部显示 } else { // 计算需要间隔多少项显示一个标签 return Math.ceil(categories.length / maxLabelsWithoutOverlap) - 1; } } const labelInterval isMobile ? calculateLabelInterval(categories, chartDom.clientWidth, fontSize) : 0; // PC端默认全显示 // 在xAxis.axisLabel配置中使用 axisLabel: { fontSize: fontSize, color: #666, rotate: axisLabelRotate, interval: labelInterval, // 使用动态计算的间隔 margin: isMobile ? 20 : 10 },此外还可以考虑在移动端完全隐藏X轴标签转而通过tooltip触摸时显示来查看具体项名或者使用可滑动的数据区域缩放dataZoom让用户手动滑动查看被遮挡的标签。// 修改移动端的dataZoom配置即使之前隐藏了现在可以启用一个滑块 dataZoom: isMobile ? [{ type: slider, // 改为滑块类型 xAxisIndex: 0, start: 0, end: 100, bottom: 10%, // 放在底部 height: 20, showDetail: false // 移动端隐藏详情 }] : [{ type: inside, xAxisIndex: 0, start: 0, end: 100, zoomLock: true }]6. 性能调优与移动端专属适配在移动设备上性能和平滑度至关重要。除了之前提到的节流resize事件还有几个关键点1. 减少重绘区域与启用硬件加速确保图表的容器chart-container的CSS不包含会引起整个页面重排的属性如复杂的box-shadow动画。ECharts Canvas渲染本身性能很好但我们可以通过CSS为容器添加transform: translateZ(0)来触发GPU硬件加速提升动画流畅度。#chart-container { /* ... 其他样式 ... */ transform: translateZ(0); will-change: transform; }2. 简化移动端配置移动端应移除所有非必要的视觉元素。例如网格线可以更淡或取消Y轴轴线可以隐藏只保留刻度标签。// 在getChartOption函数中针对移动端调整yAxis yAxis: { // ... 其他配置 ... axisLine: { show: !isMobile // PC端显示移动端隐藏 }, splitLine: { lineStyle: { color: isMobile ? #f5f5f5 : #eee, // 移动端颜色更淡 type: isMobile ? dotted : dashed } } }3. 防抖处理数据更新如果数据源推送非常频繁如每秒多次直接调用updateChartWithAnimation会导致动画队列堆积图表卡死。我们需要对数据更新请求进行防抖debounce确保在上一次动画完成前不会开始新的动画。// 使用lodash的debounce const debouncedUpdate _.debounce(function(rawData) { // 注意这里传入的rawData可能是多次事件合并后的最新数据 updateChartWithAnimation(rawData); }, 500, { leading: false, trailing: true }); // 等待停止触发500ms后再执行 // 模拟一个高频数据源 let mockHighFreqDataSource setInterval(() { const tempData currentData.map(item ({ ...item, value: Math.max(10, item.value (Math.random() - 0.5) * 20) })); debouncedUpdate(tempData); // 使用防抖函数 }, 200); // 记得在页面卸载时清除 // clearInterval(mockHighFreqDataSource);4. 触摸交互优化移动端用户习惯用手势操作。我们可以启用ECharts的数据区域缩放dataZoom的拖拽和捏合功能并优化tooltip的显示。移动端的tooltip通常设置为‘item’触摸柱子显示比‘axis’触摸X轴位置显示更符合直觉。tooltip: { trigger: isMobile ? item : axis, // 移动端触发方式改为item // ... 其他配置 ... // 移动端下可以配置一个更大的触发区域和更长的显示延迟 confine: true, // 将tooltip限制在图表区域内 backgroundColor: rgba(255, 255, 255, 0.95), borderWidth: 0, textStyle: { fontSize: isMobile ? 14 : 12 } }7. 完整源代码整合与使用指南将以上所有优化点整合我们就得到了一个健壮的、可用于生产环境的自适应动态排名柱状图组件。为了方便使用我将其封装成了一个简单的类RankingBarChart。完整HTML文件 (index.html):!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title自适应动态排名柱状图 - 完整实现/title script srchttps://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js/script script srchttps://cdn.jsdelivr.net/npm/lodash4.17.21/lodash.min.js/script style * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: sans-serif; padding: 20px; background: #f0f2f5; min-height: 100vh; } .container { max-width: 1200px; margin: 0 auto; } .header { text-align: center; margin-bottom: 30px; } .header h1 { color: #333; margin-bottom: 10px; } .header p { color: #666; line-height: 1.6; } #chart-main { width: 100%; height: 600px; background: white; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.08); padding: 20px; transform: translateZ(0); will-change: transform; } media (max-width: 768px) { body { padding: 10px; } #chart-main { height: 500px; padding: 15px; border-radius: 8px; } .header h1 { font-size: 1.5em; } } .controls { text-align: center; margin-top: 25px; } .btn { padding: 12px 28px; margin: 0 10px; border: none; border-radius: 6px; background: #4a6cf7; color: white; font-size: 16px; cursor: pointer; transition: all 0.2s; } .btn:hover { background: #3a56d8; transform: translateY(-1px); } .btn-reset { background: #f0ad4e; } .btn-reset:hover { background: #ec971f; } .status { text-align: center; margin-top: 15px; color: #888; font-size: 0.9em; } /style /head body div classcontainer div classheader h1业务线实时排名看板/h1 p图表支持动态数据更新与平滑排名动画并自动适配PC与移动端显示。点击下方按钮模拟数据更新。/p /div div idchart-main/div div classcontrols button classbtn idbtn-update模拟实时更新/button button classbtn btn-reset idbtn-reset重置排名数据/button /div div classstatus idstatus-info就绪/div /div script srcranking-bar-chart.js/script script // 初始化图表 const chart new RankingBarChart(chart-main); chart.init(); // 绑定按钮事件 document.getElementById(btn-update).addEventListener(click, () { chart.simulateRandomUpdate(); document.getElementById(status-info).textContent 数据已更新于 ${new Date().toLocaleTimeString()}; }); document.getElementById(btn-reset).addEventListener(click, () { chart.resetToInitialData(); document.getElementById(status-info).textContent 数据已重置; }); // 可选模拟一个每5秒自动更新的数据源 // let autoUpdate setInterval(() chart.simulateRandomUpdate(), 5000); // 停止自动更新 clearInterval(autoUpdate); /script /body /html核心JavaScript类 (ranking-bar-chart.js):// ranking-bar-chart.js class RankingBarChart { constructor(containerId, options {}) { this.containerId containerId; this.container document.getElementById(containerId); if (!this.container) { throw new Error(容器元素 #${containerId} 未找到); } // 默认配置 this.defaultOptions { animationDuration: 1000, animationEasing: elasticOut, mobileBreakpoint: 768, colors: [#5470c6, #91cc75, #fac858, #ee6666, #73c0de, #3ba272, #fc8452, #9a60b4], ...options }; this.chart null; this.currentData []; this.isMobile false; this.resizeHandler null; } // 初始化图表 init(initialData) { if (!initialData) { initialData this._getDefaultData(); } this.currentData initialData; this.isMobile this._checkIsMobile(); // 初始化ECharts实例 this.chart echarts.init(this.container, null, { renderer: canvas, useDirtyRect: true }); // 设置初始配置 this.chart.setOption(this._getChartOption(this.currentData, this.isMobile)); // 绑定自适应事件 this.resizeHandler _.throttle(() { const newIsMobile this._checkIsMobile(); // 如果设备类型发生变化需要更新整个option if (newIsMobile ! this.isMobile) { this.isMobile newIsMobile; this.chart.setOption(this._getChartOption(this.currentData, this.isMobile)); } else { // 仅调整大小 this.chart.resize(); } }, 300); window.addEventListener(resize, this.resizeHandler); console.log(RankingBarChart 初始化完成); } // 更新图表数据带动画 updateData(newData) { if (!this.chart) { console.warn(图表未初始化请先调用 init() 方法); return; } // 计算并输出排名变化日志 this._logRankChange(this.currentData, newData); // 更新当前数据引用 this.currentData newData; // 获取新配置并设置触发ECharts过渡动画 const newOption this._getChartOption(this.currentData, this.isMobile); this.chart.setOption(newOption, { notMerge: false, lazyUpdate: false }); } // 模拟随机数据更新用于演示 simulateRandomUpdate() { const newData this.currentData.map(item ({ name: item.name, value: Math.max(10, item.value (Math.random() - 0.5) * 80) })); this.updateData(newData); } // 重置为初始数据 resetToInitialData() { this.updateData(this._getDefaultData()); } // 销毁图表清理事件 dispose() { if (this.resizeHandler) { window.removeEventListener(resize, this.resizeHandler); this.resizeHandler null; } if (this.chart) { this.chart.dispose(); this.chart null; } } // -------------------- 私有方法 -------------------- _checkIsMobile() { return this.container.clientWidth this.defaultOptions.mobileBreakpoint; } _getDefaultData() { return [ { name: 华东地区, value: 335 }, { name: 华北地区, value: 310 }, { name: 华南地区, value: 274 }, { name: 西南地区, value: 235 }, { name: 华中地区, value: 220 }, { name: 西北地区, value: 198 }, { name: 东北地区, value: 165 } ]; } _getChartOption(data, isMobile) { const sortedData [...data].sort((a, b) b.value - a.value); const categories sortedData.map(item item.name); const values sortedData.map(item item.value); const fontSize isMobile ? 14 : 16; const axisLabelRotate isMobile ? 45 : 0; const gridBottom isMobile ? 22% : 15%; const barWidth isMobile ? 55% : 65%; // 动态计算标签显示间隔移动端 const labelInterval isMobile ? this._calculateLabelInterval(categories, fontSize) : 0; return { animation: true, animationDuration: this.defaultOptions.animationDuration, animationEasing: this.defaultOptions.animationEasing, animationDurationUpdate: 1200, animationEasingUpdate: cubicOut, grid: { top: isMobile ? 15% : 12%, left: isMobile ? 13% : 11%, right: 5%, bottom: gridBottom, containLabel: true }, xAxis: { type: category, data: categories, axisLabel: { fontSize, color: #666, rotate: axisLabelRotate, interval: labelInterval, margin: isMobile ? 18 : 8 }, axisTick: { alignWithLabel: true, length: 4 }, axisLine: { lineStyle: { color: #ddd } } }, yAxis: { type: value, name: 销售额 (万), nameTextStyle: { fontSize, color: #999 }, axisLabel: { fontSize, color: #666, formatter: {value} }, axisLine: { show: !isMobile, lineStyle: { color: #ddd } }, splitLine: { lineStyle: { type: isMobile ? dotted : dashed, color: isMobile ? #f0f0f0 : #eee } } }, series: [{ name: 地区排名, type: bar, data: values.map((value, idx) ({ value, itemStyle: { color: this.defaultOptions.colors[idx % this.defaultOptions.colors.length], borderRadius: [4, 4, 0, 0] }, label: { show: true, position: top, fontSize: fontSize - 2, fontWeight: bold, formatter: (params) { const rank idx 1; return {rank|${rank}.} {val|${params.value}}; }, rich: { rank: { color: #888, fontSize: fontSize - 4 }, val: { color: inherit } } } })), barWidth, animationDelay: idx idx * 100, animationDelayUpdate: idx idx * 100 }], tooltip: { trigger: isMobile ? item : axis, axisPointer: { type: shadow }, confine: true, backgroundColor: rgba(255, 255, 255, 0.96), borderWidth: 0, textStyle: { fontSize: isMobile ? 14 : 13 }, formatter: (params) { const data Array.isArray(params) ? params[0] : params; const rank data.dataIndex 1; return div stylefont-weight:bold;margin-bottom:5px;${data.name}/div 排名b第${rank}名/bbr/ 数值b stylecolor:${data.color};${data.value.toLocaleString()}/b; } }, legend: isMobile ? { show: false } : { data: [地区排名], top: 3%, textStyle: { fontSize } }, dataZoom: isMobile ? [{ type: slider, xAxisIndex: 0, start: 0, end: 100, bottom: 8%, height: 20, showDetail: false, brushSelect: false }] : [{ type: inside, xAxisIndex: 0, start: 0, end: 100, zoomLock: true }] }; } _calculateLabelInterval(categories, fontSize) { if (!this.container) return 0; const avgLen categories.reduce((sum, name) sum name.length, 0) / categories.length; const estWidth avgLen * fontSize * 0.65 25; const availWidth this.container.clientWidth * 0.75; const maxLabels Math.floor(availWidth / estWidth); return categories.length maxLabels ? 0 : Math.ceil(categories.length / maxLabels) - 1; } _logRankChange(oldData, newData) { const oldSorted [...oldData].sort((a, b) b.value - a.value); const newSorted [...newData].sort((a, b) b.value - a.value); console.group(排名变化详情); newSorted.forEach((item, newIdx) { const oldItem oldSorted.find(old old.name item.name); const oldIdx oldSorted.indexOf(oldItem); if (oldIdx ! -1) { const change oldIdx - newIdx; let arrow →; if (change 0) arrow ↑${change}; else if (change 0) arrow ↓${change}; console.log(${newIdx 1}. ${item.name}: ${item.value} (${arrow})); } else { console.log(${newIdx 1}. ${item.name}: ${item.value} (新上榜)); } }); console.groupEnd(); } }使用指南引入依赖确保页面已通过CDN引入ECharts和lodash。创建容器在HTML中定义一个具有固定或自适应高度的div作为图表容器。初始化在JavaScript中实例化RankingBarChart类传入容器ID然后调用init()方法。你可以传入自定义的初始数据不传则使用默认数据。更新数据调用updateData(newDataArray)方法即可触发带动画的排名更新。newDataArray是一个对象数组每个对象需包含name和value属性。响应式图表会自动监听窗口大小变化并在PC/移动端布局间切换。你可以在实例化时通过options.mobileBreakpoint修改断点默认768px。清理在单页应用SPA或组件卸载时调用dispose()方法以移除事件监听和释放图表资源。这个组件封装了所有核心逻辑开箱即用。你可以通过修改_getChartOption方法中的配置项如颜色、字体、网格等来完全定制图表样式也可以通过继承该类来扩展更复杂的功能比如添加时间轴、多系列对比等。本文还有配套的精品资源点击获取