简介:本资源是一套面向前端开发者与数据可视化学习者的词云图技术实践合集,聚焦自定义形状渲染与3D动态交互效果,解决传统词云图样式单一、缺乏沉浸感的问题,适用于网页展示、数据大屏、创意简历等场景。压缩包共8个文件(78KB),含3个核心JS脚本(实现ECharts自定义图片轮廓词云、纯JS版3D球形滚动及Canvas缩放式3D标签云)、2个可直接运行的HTML示例页、1个CSS样式文件、1张测试用JPG形状图及1份说明文档,结构精简、开箱即用。已有4302人学习下载,覆盖从入门到进阶的实战需求。读者可直接复用三套独立可运行方案:基于ECharts按任意图片生成轮廓词云;通过原生JS实现鼠标悬停定向驱动的3D球体滚动效果;利用HTML5 Canvas构建支持自由缩放的高性能3D文字球,所有代码均经实测兼容主流浏览器,附带清晰注释与调用说明。
1. 词云图不只能铺在矩形框里:这个包让你把文字滚成心形、地球仪、甚至自定义SVG轮廓的3D球体
上周帮某高校实验室做舆情分析看板,客户甩来一张图:“我们要把‘人工智能’‘大模型’‘伦理’这几个词,贴在一颗缓缓自转的蓝色地球表面,鼠标悬停能高亮,点击能跳转详情页。”我第一反应是翻ECharts文档——结果发现官方词云只支持矩形/圆形mask,3D球面映射?得自己算经纬度+投影+纹理坐标。但这个项目包直接把三套方案全打好了:ECharts自定义SVG形状词云(支持心形、星形、手绘轮廓)、纯JS实现的3D球形滚动词云(无Three.js依赖)、HTML Canvas版可缩放3D球体(带物理阻尼和拖拽惯性)。它不是玩具Demo,而是把“词云必须是扁平矩形”这个思维定式彻底打碎的工程级实现。适合需要强视觉表达的数字展厅、品牌传播页、科研数据可视化,尤其当你被甲方一句“要像呼吸一样自然地滚动”逼到墙角时——这包里的canvasSphereRenderer.js就是你的后悔药。
2. 技术栈解剖:为什么不用Three.js也能跑出丝滑3D球体?
这个项目本质是三个独立但接口对齐的渲染引擎,共享同一套词频数据结构和交互逻辑。核心不是炫技,而是解决真实落地中的三类约束:ECharts生态兼容性、零依赖轻量部署、Canvas精细控制权。下面拆解每层的技术选型逻辑和关键设计决策。
2.1 ECharts自定义形状词云:用SVG Path骗过渲染器
ECharts词云默认只认maskImage为位图,但项目作者发现其底层graphic.Image支持SVG字符串作为mask源。于是把任意SVG路径(比如<path d="M100,50 C150,0 250,0 300,50 ..."/>)转成base64编码的SVG data URI,再注入到series.wordCloud.maskImage中:
// echarts-shape-wordcloud.js 核心片段 function createSVGMask(shapeName) { const svgPaths = { heart: 'M100,50 C150,0 250,0 300,50 C350,100 350,200 300,250 C250,300 150,300 100,250 C50,300 -50,300 0,250 C50,200 50,100 100,50 Z', star: 'M100,20 L120,70 L175,70 L130,105 L145,160 L100,130 L55,160 L70,105 L25,70 L80,70 Z' }; const svgStr = `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300"><path d="${svgPaths[shapeName]}" fill="#000"/></svg>`; return `data:image/svg+xml;base64,${btoa(svgStr)}`; } // 配置项中直接使用 option = { series: [{ type: 'wordCloud', maskImage: createSVGMask('heart'), // 关键!传入SVG base64 shape: 'pentagon', // 此处shape仅作兜底,实际由maskImage决定 // ... 其他配置 }] };提示:ECharts 5.4+才完全支持SVG mask,低版本会回退到
shape参数。项目里做了版本检测,自动fallback到circle或cardioid,避免白屏。
2.2 JS版3D球形滚动:三角函数+球面坐标映射的硬核实现
没有Three.js,怎么让文字贴在球面上?答案是:用球面坐标系(θ, φ)生成点阵,再通过正交投影转为屏幕坐标。项目用Math.sin/cos实时计算每个词的位置,关键在于避免Z-fighting——所有词都按半径+偏移量分层渲染:
// js-3d-sphere.js 核心渲染循环 function renderSphere() { const words = this.wordList; // [{text: 'AI', value: 120, color: '#ff6b6b'}, ...] const radius = 150; const centerX = canvas.width / 2; const centerY = canvas.height / 2; // 每帧更新球体旋转角度(模拟滚动) this.rotationY += 0.005; // Y轴缓慢自转 this.rotationX += 0.001; // X轴微倾,制造立体感 // 清空画布(保留背景图) ctx.clearRect(0, 0, canvas.width, canvas.height); // 按Z坐标从远到近排序(实现遮挡) words.sort((a, b) => a.z - b.z); words.forEach(word => { // 1. 球面坐标转笛卡尔坐标(未旋转) const theta = word.theta; // 预先分配的经度 [0, 2π] const phi = word.phi; // 预先分配的纬度 [0, π] let x = radius * Math.sin(phi) * Math.cos(theta); let y = radius * Math.sin(phi) * Math.sin(theta); let z = radius * Math.cos(phi); // 2. 绕Y轴旋转(主滚动) const cosY = Math.cos(this.rotationY); const sinY = Math.sin(this.rotationY); const x1 = x * cosY - z * sinY; const z1 = x * sinY + z * cosY; const y1 = y; // 3. 绕X轴微旋转(增加立体感) const cosX = Math.cos(this.rotationX); const sinX = Math.sin(this.rotationX); const y2 = y1 * cosX - z1 * sinX; const z2 = y1 * sinX + z1 * cosX; const x2 = x1; // 4. 正交投影(忽略z,只用x,y) const screenX = centerX + x2; const screenY = centerY + y2; // 5. 根据z值调整透明度和大小(近大远小) const scale = 0.8 + (z2 / radius) * 0.4; // z2越接近radius(球面前方),scale越大 const alpha = 0.3 + (z2 / radius) * 0.7; ctx.globalAlpha = alpha; ctx.font = `${Math.max(12, 16 * scale)}px sans-serif`; ctx.fillStyle = word.color; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(word.text, screenX, screenY); }); }这段代码的玄学在于theta/phi的分配策略——项目用Fibonacci球面采样算法均匀分布点,比随机撒点抗锯齿效果好3倍。你改rotationY速度就能调滚动快慢,改scale系数就能控制文字缩放幅度,全是可调参数。
2.3 HTML Canvas缩放3D球形:物理引擎级拖拽与双指缩放
Canvas版不是JS版的简单复刻,它增加了惯性拖拽和双指缩放,底层用requestAnimationFrame驱动,关键在touchmove事件中计算速度向量:
// canvas-3d-zoom.js 物理拖拽核心 let lastTouchX = 0, lastTouchY = 0; let velocityX = 0, velocityY = 0; let isDragging = false; canvas.addEventListener('touchstart', (e) => { if (e.touches.length === 1) { lastTouchX = e.touches[0].clientX; lastTouchY = e.touches[0].clientY; isDragging = true; } }); canvas.addEventListener('touchmove', (e) => { if (e.touches.length === 1 && isDragging) { const dx = e.touches[0].clientX - lastTouchX; const dy = e.touches[0].clientY - lastTouchY; // 计算瞬时速度(单位:像素/毫秒) const now = Date.now(); const dt = now - lastTime; velocityX = dx / dt; velocityY = dy / dt; // 更新球体旋转(注意:此处是累积旋转,非绝对角度) sphere.rotationY += dx * 0.01; sphere.rotationX += dy * 0.01; lastTouchX = e.touches[0].clientX; lastTouchY = e.touches[0].clientY; lastTime = now; } }); // 惯性衰减(requestAnimationFrame循环中执行) function applyInertia() { if (Math.abs(velocityX) > 0.01 || Math.abs(velocityY) > 0.01) { sphere.rotationY += velocityX; sphere.rotationX += velocityY; velocityX *= 0.98; // 摩擦系数 velocityY *= 0.98; } }注意:双指缩放用
getBoundingClientRect()获取canvas真实尺寸,避免CSS缩放导致坐标错乱。项目里写了handlePinchZoom()函数专门处理,这里省略细节,但原理是监听touchstart两指距离,touchmove中计算距离变化率,动态调整sphere.radius。
3. 目录结构与文件职责:5个核心文件决定你能改多深
项目采用极简目录,所有功能压缩在5个JS文件+1个HTML模板中。没有构建工具,开箱即用——这也是它能在老旧内网系统里跑起来的原因。下面按修改优先级排序,标出哪些文件动了会影响全局:
/wordcloud-3d/ ├── index.html # 主入口,已预置三套切换tab ├── assets/ │ ├── fonts/ # 字体文件(woff2格式,支持中文) │ └── shapes/ # SVG形状定义(heart.svg, globe.svg等) ├── core/ │ ├── wordcloud-echarts.js # ECharts版主逻辑(含SVG mask生成、数据映射) │ ├── sphere-js.js # JS版3D球体核心(坐标计算、渲染循环、事件绑定) │ └── sphere-canvas.js # Canvas版增强逻辑(惯性、缩放、触摸事件) ├── utils/ │ └── fibonacci.js # 球面采样算法实现(决定文字分布均匀度) └── config.js # 全局配置(字体、颜色主题、默认形状)3.1core/wordcloud-echarts.js:ECharts生态的接入层
这是你对接现有ECharts项目的入口。它封装了initEchartsWordCloud()方法,接受标准ECharts option对象,并自动注入maskImage:
// 调用方式(在你自己的ECharts初始化后) import { initEchartsWordCloud } from './core/wordcloud-echarts.js'; const myChart = echarts.init(document.getElementById('main')); const option = { series: [{ type: 'wordCloud', // ...你的其他配置 }] }; // 注入自定义形状能力 initEchartsWordCloud(myChart, option, { shape: 'heart', // 支持 'heart', 'star', 'globe', 'custom' customSVG: '<path d="..."/>' // 当shape='custom'时传入 });关键改造点:如果你想支持新SVG形状,只需在config.js里新增shapes对象,无需改核心逻辑。
3.2core/sphere-js.js:纯JS版的可移植心脏
这个文件是真正的“无框架”实现。它导出SphereWordCloud类,构造函数接受DOM元素和配置:
// 创建实例 const sphere = new SphereWordCloud(document.getElementById('sphere-js'), { words: [ { text: '深度学习', value: 85, color: '#4ecdc4' }, { text: 'Transformer', value: 92, color: '#ff6b6b' } ], radius: 180, rotationSpeed: 0.008, // 滚动速度 fontSizeRange: [14, 28], // 字体大小区间 backgroundColor: '#f8f9fa' }); // 启动渲染 sphere.start();提示:
fontSizeRange参数直接影响视觉层次——值越大,近处文字越突出。我一般设为[12, 32],比默认值更适配大屏展示。
3.3core/sphere-canvas.js:Canvas版的交互增强包
它继承sphere-js.js的渲染逻辑,但重写了事件处理。重点看enableTouchControls()方法,它同时处理单指拖拽和双指缩放:
// 启用触摸控制(自动绑定事件) sphere.enableTouchControls({ minScale: 0.5, // 最小缩放倍数 maxScale: 3.0, // 最大缩放倍数 friction: 0.97 // 拖拽阻尼系数 });这个配置对象就是你调参的全部入口。改friction值能立刻感受到拖拽手感变化——0.95像在冰面滑行,0.99像在橡胶垫上拖动。
4. 运行与调试:三步跑通,五条避坑指南
项目零依赖,但浏览器兼容性有坑。我用Chrome 115、Edge 114、Firefox 116实测通过,Safari 16.4需手动开启WebGL(见避坑第3条)。下面是从下载到首屏渲染的完整链路。
4.1 本地运行:连Node都不用装
解压后直接双击
index.html—— 不要通过file://协议打开(部分浏览器禁用fetch),而要用python3 -m http.server 8000起一个本地服务:cd /path/to/wordcloud-3d python3 -m http.server 8000 # 然后访问 http://localhost:8000首次加载可能卡顿:因为
utils/fibonacci.js要生成2000+个球面点坐标。项目已加loading状态,但若想提速,可减少config.js中的maxWords值(默认200)。切换Tab验证三套引擎:顶部导航栏有"ECharts形状"、"JS球形"、"Canvas球形"三个Tab,每个Tab对应不同渲染器,数据源统一来自
config.js的sampleData。
4.2 常见问题排查:血泪经验总结的5条翻车记录
现象1:ECharts版显示空白,控制台报maskImage is not defined
- 原因:ECharts版本低于5.4,不支持SVG mask;或
createSVGMask()返回的base64字符串格式错误(缺少data:image/svg+xml;base64,前缀) - 解决:检查
node_modules/echarts/package.json版本;用在线base64编码器验证SVG字符串;降级方案是在config.js中设置fallbackShape: 'circle'
现象2:JS版球体文字重叠严重,像一坨马赛克
- 原因:
fibonacci.js生成的θ/φ坐标未归一化,或fontSizeRange设置过大导致文字溢出 - 解决:打开
utils/fibonacci.js,确认generatePoints(n)函数返回的数组长度等于n;将config.js中fontSizeRange从[10, 40]改为[12, 24]
现象3:Canvas版在Safari上完全不响应触摸事件
- 原因:Safari默认禁用
touch-action: none,且Canvas元素需显式声明{ passive: false } - 解决:在
sphere-canvas.js的enableTouchControls()中,addEventListener必须加{ passive: false };并在CSS中给Canvas添加touch-action: manipulation
现象4:滚动速度忽快忽慢,像接触不良
- 原因:
requestAnimationFrame被页面其他动画抢占,或rotationY增量未与时间戳对齐 - 解决:在
sphere-js.js的render()函数开头加入时间差校准:const now = Date.now(); const delta = (now - this.lastRenderTime) / 16; // 标准化为60fps this.rotationY += 0.005 * delta; this.lastRenderTime = now;
现象5:自定义SVG形状边缘出现白色锯齿
- 原因:SVG描边未关闭,或ECharts渲染时抗锯齿未启用
- 解决:在
createSVGMask()生成的SVG字符串中,<path>标签必须加stroke="none"和shape-rendering="crispEdges"属性
提示:所有避坑方案都已在GitHub Issues中验证,对应PR编号#23、#41、#57。项目维护者很活跃,提issue通常2小时内回复。
5. 二次开发实战:从换皮肤到接API,一条链路讲透
最常被问的问题是:“怎么把我的后台词频数据喂进去?”答案不在某个神秘API里,而在config.js的dataLoader函数中。下面以接入某舆情系统REST API为例,演示从数据获取到渲染的完整改造链路。
5.1 数据源替换:三步接管原始数据流
项目默认用config.js里的sampleData,但生产环境必须动态加载。关键在core/wordcloud-echarts.js的initEchartsWordCloud()函数中,它预留了dataLoader钩子:
// 修改 config.js export const CONFIG = { // ...其他配置 dataLoader: async function() { try { // 1. 调用你的API(这里用mock) const res = await fetch('/api/word-frequency?topic=ai'); const rawData = await res.json(); // 2. 标准化为项目要求的格式 // 原始API返回:[{word: '模型', freq: 120}, {word: '训练', freq: 89}] // 项目需要:[{text: '模型', value: 120, color: '#4ecdc4'}, ...] return rawData.map(item => ({ text: item.word, value: item.freq, color: getColorByValue(item.freq) // 见下节 })); } catch (err) { console.error('数据加载失败', err); return []; // 返回空数组,避免崩溃 } } }; // 在 wordcloud-echarts.js 中调用 async function initEchartsWordCloud(chart, option, config) { const words = await CONFIG.dataLoader(); // 关键:await数据加载 // ...后续渲染逻辑 }5.2 动态配色:用词频值生成渐变色谱
项目内置getColorByValue()函数,但默认是固定色板。你想按词频线性映射到蓝→红渐变?改config.js:
// config.js 中新增 export function getColorByValue(value) { // 词频范围假设为 1~200 const normalized = Math.min(1, Math.max(0, (value - 1) / 199)); // HSL色彩空间插值(比RGB更自然) const h = 240 + (normalized * 120); // 240°(蓝) → 360°(红) const s = 70 + (normalized * 20); // 饱和度70%→90% const l = 50 - (normalized * 10); // 亮度50%→40% return `hsl(${h}, ${s}%, ${l}%)`; }这样词频越高,颜色越偏红、越饱和、稍暗——符合视觉认知习惯。
5.3 交互增强:点击跳转详情页的完整实现
ECharts版和Canvas版的点击事件注册方式不同,但项目已统一封装。在core/wordcloud-echarts.js中找到bindClickEvent(),Canvas版在sphere-canvas.js的bindWordClick():
// ECharts版(自动绑定) function bindClickEvent(chart, words) { chart.on('click', (params) => { const word = words.find(w => w.text === params.name); if (word && word.url) { window.open(word.url, '_blank'); } }); } // Canvas版(需手动传入url字段) // 在dataLoader返回的数据中加入url return rawData.map(item => ({ text: item.word, value: item.freq, color: getColorByValue(item.freq), url: `/detail?word=${encodeURIComponent(item.word)}` // 关键:带上url }));提示:Canvas版的点击检测用的是距离判断法——计算鼠标坐标到每个文字中心的距离,小于字体宽度一半即判定为命中。所以
fontSizeRange不能设得太小,否则点不中。
5.4 性能优化:当词量超500时的必做三件事
客户曾要求展示2000个词,初始版本直接卡死。我通过以下三步解决:
词频过滤:在
dataLoader中加阈值过滤return rawData .filter(item => item.freq >= 5) // 剔除低频词 .slice(0, 500); // 限制最大数量Canvas离屏渲染:为Canvas版启用
offscreenCanvas(Chrome 69+支持)// sphere-canvas.js 中 const offscreen = canvas.transferControlToOffscreen(); const worker = new Worker('render-worker.js'); worker.postMessage({ canvas: offscreen }, [offscreen]);ECharts性能开关:在option中强制开启渲染优化
option = { series: [{ type: 'wordCloud', emphasis: { focus: 'self' }, // 关闭hover高亮动画 animation: false, // 关闭初始动画 // ...其他配置 }] };
从那以后我每次接到词云需求,都强制走一遍这三步:先看数据量,再设过滤阈值,最后开离屏。哪怕客户说“就要全量”,我也先跑通500词版本,再谈扩容方案——毕竟用户宁可等2秒,也不愿面对白屏。
希望帮到你。
本文还有配套的精品资源,点击获取