
机缘巧合我在整理开源项目收藏夹的时候又翻到了 Caveman 这个库。第一次看到它时我就被这个另类的数据可视化思路吸引了一个图表库不画折线也不画柱状而是把所有数据映射成一群手持棍棒的洞穴人数值越高人画得越大。这个点子乍看像恶搞细看却有一套严谨的视觉编码逻辑。前阵子我正好把一个内部运营报表改成了 Caveman 风格分享给团队之后反响意外地好今天就以我这次的实操过程为主线把 Caveman 的用法、原理以及坑位完整梳理一遍。Caveman 适合谁如果你做数据报表已经做到审美疲劳想在周报、数据大屏或者活动页里放一张一眼就能记住的图如果你想在小团队里快速把某个指标“画出”故事感又不想引入 D3 那样重的工程依赖或者你只是想在技术分享会上给大家留个印象——那 Caveman 是一张性价比极高的牌。下文所有代码示例都基于我在项目中实际跑通的版本和封装方式你可以直接抄作业也可以按自己的数据口径改造。1. 先说清楚Caveman 到底是个什么东西1.1 它解决的痛点日常开发里图表库的选择一般围绕 ECharts、Chart.js、D3 这三类打转。它们覆盖面广、社区成熟但也正因为太成熟产出图表的气质高度统一平滑的曲线、渐变色的柱状图、悬浮提示框。这些东西放在严肃的数据分析报告里没问题放在需要传播、需要让人停下来的场景里就差点意思。Caveman 解决的是“数据传达的注意力问题”。它把每个数据点画成一个洞穴壁画风格的小人人的体型、位置、数量都能编码数据。读者不需要看坐标轴也能瞬间理解“谁大谁小、谁多谁少”这种粗糙、原始、甚至有点笨拙的视觉语言反而在高信息过载的看板里制造出一种反差感让人愿意多看两眼。我当时的应用场景是一个面向运营团队的渠道质量周报。原来用 ECharts 画了一堆条形图大家看了就忘。换成 Caveman 之后同一个指标变成了“一排拿着棍子的小人在墙上排排站”谁的渠道贡献高对应的小人就又高又壮。运营同事看完直接截图发到群里这就是这个库的价值它让数据具备了某种被传播的气质。1.2 能用在哪不能用在哪Caveman 首先适合轻量级的、叙事性的数据呈现。比如活动战报页、团队 OKR 展示、个人博客的访问统计趣味化展示、内部系统的周边数据大屏。它也适合放在文章或 PPT 里作为插图和配图视觉冲击力远胜普通截图。但要说清楚的是它不是一个严肃的数据分析工具。坐标轴、网格线、精确的数值标注这些传统图表的基本配置在 Caveman 里都很弱。如果老板要看精确到小数点的转化率或者你需要做严格的趋势回归分析老老实实去用 ECharts别把 Caveman 硬套进不合适的位置。它的定位更接近“数据涂鸦”而不是“数据制图”。1.3 和主流图表库的对比我在选型的时候做了个小对比整理如下对比维度CavemanEChartsD3.js视觉风格洞穴壁画、涂鸦风、识别度极高现代商务风标准化程度高完全自由但需要自己实现一切上手成本很低一个核心方法就能出图中等配置项丰富但文档齐全高学习曲线陡峭定制能力中等适合配色与基础形状微调很强但配置复杂度同步上升极强理论上能画任何图形适合场景趣味看板、传播页、内部展示标准报表、管理后台、大屏复杂定制可视化、科研绘图体积与依赖轻量无复杂依赖较大核心库较大模块化使用稍好些这个小表格让你看明白Caveman 不是要和谁竞争它是在主流方案之外提供了一个独特的风格选项。2. 上手做一张洞穴壁画最小可运行示例2.1 环境准备与引入方式Caveman 的引入方式有两种我做项目时用的是 script 标签直接引入因为它不需要构建步骤对后端模板项目尤其友好。把下载好的 caveman.js 放进静态目录在页面底部引入即可script src/js/caveman.js/script如果你用的是前端工程化项目也可以通过打包器引入但需要注意排查模块导出的兼容性。就我的经验直接挂到 window 上是最稳的后面封装用起来也顺手。2.2 初始化图表与数据结构Caveman 的核心思想是把你准备好的数据“映射”成壁画。在跑通最小示例之前先理解它喜欢什么样的数据结构。我实际使用的简化结构是这样的var data { labels: [直接访问, 搜索跳转, 外链点击], values: [320, 210, 88] };labels 是每个洞穴人的名字values 是每个名字对应的数值。这个结构够简单、够直白不需要像 D3 那样先把数据绑定到 DOM 再慢慢推导比例尺。初始化也很简单var chart new Caveman({ container: document.getElementById(chart), data: data, width: 800, height: 320 }); chart.render();跑起来之后页面上会出现一个浅色纹理的背景上面站着一排洞穴人。体型根据 values 自动缩放。整个画面不需要你手动设置坐标轴范围Caveman 内部已经把数据归一化处理好了。第一次看到这张图的时候我愣了几秒然后笑了出来。这个反应本身就说明它成功了。2.3 options 核心参数逐项解读在你开始改样式之前先把高频参数吃透。我把项目里经常用到的配置项整理成一张速查表参数名作用我的建议container图表挂载的 DOM 容器只传 DOM 元素不要传选择器字符串data包含 labels 和 values 的数据对象单组数据最省心多组数据需要改造width / height画布整体尺寸按容器实际宽度计算别写死background背景色或背景纹理默认仿岩石纹理可以改成纯色padding画布内边距值太小洞穴人容易贴边animationSpeed入场动画速度建议适中太快会丢失仪式感showTooltip是否展示悬浮提示作展示用途时开启默认关闭colors自定义洞穴人和装饰的颜色可以配合品牌色做统一这些配置看着简单但每一项直接影响成图气质。我尤其建议你在 colors 和 background 上花一点时间因为默认的棕黄色调虽然地道但不一定适配你页面的整体视觉风格。3. 画面背后Caveman 的绘制原理拆解3.1 数据到形体的映射逻辑Caveman 看起来是个玩乐派库核心算法却很实在。它把数值映射大致分为三步归一化、对数压缩、绘图坐标换算。归一化很好理解把所有数值线性压到 0 到 1 之间的百分比这一步保证整个画布里的大只不大于天际、小只不小于芝麻。但我实际测试后发现如果你的数据里有极端值比如一个是 5一个是 10000线性归一化会导致 5 对应的小人几乎看不见。这时候 Caveman 内部会启动对数压缩逻辑拉开小数值之间的视觉差距同时避免大数值撑爆画布。做完归一化之后它把每个值映射成洞穴人的“体块大小”。在坐标系里每个穴居人底部中心点对齐在横向等距的位置上高度则由体块倍数决定。这个映射逻辑让我想起小时候玩的《帝国时代》村民采集资源的数量会改变角色动画里携带物资的视觉重量。Caveman 做的是同样的事用一个抽象的、可感知的视觉锚点去编码数字。3.2 canvas 绘制与动画的实现方式Caveman 底层是基于 Canvas 绘制的。这就意味着它本质上是在一块画布上不断重新绘制图形而不是像 DOM 图表那样靠多个元素堆叠。具体绘制过程是这样的第一次 render 时Caveman 会清空画布画上岩石纹理背景接着逐个绘制洞穴人的身体、头、武器和阴影。每个洞穴人其实是由若干圆形、椭圆和线条组合出来的所谓“人”并不是图片素材而是程序化生成的一组矢量图形。动画则依赖 requestAnimationFrame 实现。每一帧都会根据当前动画进度重新计算洞穴人的身高和位置从而形成从地面“长出来”的效果。我后来扒了一下源码发现这个动画循环写得相当克制并没有复杂的缓动库。它靠的是一个简单的插值函数把起始高度和最终高度平滑过渡。明白了这一点你就知道为什么把 animationSpeed 调到很高时会看着很突兀——因为它根本没有做花哨的回弹效果调太高了反而削弱了动画的叙事感。3.3 二次封装给洞穴人加上颜色和文字标签原版 Cavean 的展示信息比较克制只有悬浮提示能看到具体数值。我在实际项目里的做法是封装了一层让每个洞穴人脚下显示名称、头顶显示数值这样截图传播时信息才完整。我采用了这样的思路在 caveman.render() 完成后往容器里叠加一个透明定位的 HTML 覆盖层。覆盖层里根据坐标插入一排 span 标签。坐标计算可以通过回顾内部映射逻辑手动推算var chart new Caveman({ ... }); chart.render(); var itemWidth chart.width / chart.data.labels.length; chart.data.labels.forEach(function (label, index) { var labelEl document.createElement(span); labelEl.innerText label; labelEl.style.position absolute; labelEl.style.left index * itemWidth itemWidth / 2 px; labelEl.style.transform translateX(-50%); chart.container.appendChild(labelEl); });这里的关键是让 HTML 覆盖层定位基于同一个容器而不是页面全局坐标。我最初直接按页面坐标系计算滚动后全部错位后来改用容器作为偏移基准就解决了。这个封装能让图表在周报截图里信息自洽不需要额外配文字说明。4. 实战把一个真实业务指标做成洞穴壁画4.1 需求与数据准备我这次实战的背景是为团队做一个“渠道质量周报”。原始数据来自后端接口返回的是一个 JSON 数组里面包含渠道名、曝光量、点击量、转化率等字段。需求很朴素一眼看出哪个渠道整体贡献最高。为了方便 Caveman 使用我先把接口数据转换成了它需要的 labels values 结构。这里我选择用“加权贡献分”作为 values而不是单一曝光量。计算公式很简单var channels [ { name: 小红书, pv: 60000, uv: 3000, order: 45 }, { name: 公众号, pv: 20000, uv: 1800, order: 38 }, { name: 抖音, pv: 90000, uv: 4000, order: 20 } ]; var data { labels: channels.map(function (c) { return c.name; }), values: channels.map(function (c) { return c.pv * 0.3 c.uv * 0.5 c.order * 100; }) };加权逻辑可以根据业务自己定目的只是让单值能综合表达某个维度。如果你不需要综合维度直接用原始数值也是一样的效果。4.2 改造过程从原生 JSON 到贴合业务的数据格式真正接业务数据时你八成会遇到接口字段名不一致的问题。比如后端给你的是 exposure_count你要的是 labels 里的 channel_name。不要期待 Caveman 帮你做这件事写一个轻量的数据适配层是最佳实践。我封装了一个 convert 函数核心思路是把业务字段名映射成 Caveman 约定字段function toCavemanData(rawList, labelField, valueField) { return { labels: rawList.map(function (item) { return item[labelField]; }), values: rawList.map(function (item) { return Number(item[valueField]); }) }; } var cavemanData toCavemanData(apiData, channel_name, weighted_score);这一步看起来多余但在项目里价值很大。因为 Caveman 的数据结构固定你不可能为了每种接口形态都去改图表代码。适配层相当于在所有数据源和图表之间加了一个稳定的协议后端怎么变你只需改一行字段名映射。4.3 效果展示与性能表现改造完成后页面上渲染了 7 个洞穴人每个代表一个渠道体型差异很直观。我关了动画整图渲染时间实测在 20 毫秒左右。开启入场动画后页面加载过程会有半秒左右的趣味等待但不影响滚动页面。这里有一个经验想分享如果你的画布尺寸超过 1000 像素宽建议把洞穴人数量控制在 8 个以内。超过 8 个的时候横向排列的间距会被压缩洞穴人之间会产生重叠。尤其是当相邻两个值差距很大时大个子的手臂和小个子的头部容易“打架”视觉上显得有些凌乱。不是库的 bug而是画布内容密度的自然限制。5. 踩坑记录动画卡顿、白色背景、中文标签5.1 常见问题速查表在实际使用和给同事做技术支持的过程中我整理了这张问题速查表基本覆盖了 Caveman 最常见的雷区问题表现原因分析处理方法动画卡顿画布尺寸过大且同时渲染数量过多减小画布尺寸或用配置项关掉动画背景泛白无纹理覆盖层或父容器背景色覆盖了 canvas检查父级 CSS 背景色调成透明中文标签乱码或缺失字体渲染兼容问题或容器被遮挡在覆盖层样式中显式设置中文字体数值为 0 的项不显示0 值归一化后体型为最小可对 0 值做保底偏移初始化时报找不到容器container 传成了字符串而非 DOM改为 document.getElementById与前端框架配合异常框架虚拟 DOM 在渲染后重建节点在生命周期的最晚阶段初始化我遇到最多的是第一项设计稿里有个全屏大图看板Caveman 画布撑到 1920 宽还渲染了 12 个洞穴人。开启动画后低端办公笔记本上能明显感觉到绘制延迟。解决办法是砍掉动画、缩小画布把视觉重心放在洞穴人群像的排列上而不是动态过程。5.2 一个值得跨项目复用的小技巧Caveman 默认的海绵状岩石纹理和圆形小人在整体视觉里会显得有点“喧宾夺主”。我调整视觉风格的核心技巧是改 colors 配置和背景而不要试图去改它内部的人物结构算法——改结构代码意味着你脱离了库的维护线后续升级很麻烦。我把默认配色从棕黄色调改成灰蓝色调之后整个图的气质立刻从“搞笑洞穴”变成了“高级复古插画”。方法很简单配置 colors 数组时用灰度低饱和度颜色替换明快的黄褐色同时把 background 从纹理改成接近米白的纯色壁画感保留但不再抢页面焦点。如果你拿到一套品牌 VI 色板甚至可以按照品牌色把每个洞穴人的“衣服”颜色映射成不同渠道的分类色这样的图放在官网首页都不掉价。5.3 为什么要保留“丑”设计在开发过程中产品经理一度希望我把洞穴人做得更精致比如加上眼睛、嘴巴的动作或者把棍棒换成更有质感的 3D 模型。我劝住了。Caveman 的“粗糙感”恰恰是它的记忆点。当画面过于精致的时候它就和常规图标库没有区别了。数据可视化的首要目标是传达信息其次才是营造美学体验。洞穴人的粗线条和手绘感天然传递出一种“来自上古时代的朴素智慧”的心理暗示。用户看到它不会去纠结细节精度而是直接对数据对比产生直观印象。这个“丑”是有设计策略的丑。6. 我觉得值得单独说的几点经验6.1 封装成独立组件才能长期使用如果你准备在正式项目里引入 Caveman强烈建议第一时间把它封装成独立组件而不是在每个页面里复制粘贴初始化代码。我封装的组件对外只暴露三个参数data、width、theme。内部处理好适配层、中文标签覆盖层和自定义配色。这样以后任何页面想用洞穴人图表只需三行代码。封装成本不高但长远看能省下大量重复沟通时间。6.2 约定好 0 值和极端值的表现规则数据可视化项目里最容易被低估的是边界数据的处理。Caveman 对 0 值会渲染成很小的形体不是完全消失。但视觉上它很容易被解读为“这个渠道挂了”。我在封装里加了一个规则如果 values 中存在 0就在对应洞穴人脚下画一个小问号图标同时悬浮提示里明确显示“本期无数据”。这种规则要和业务方提前对齐否则他们会误把 0 当故障。6.3 适合和普通图表混合使用不要觉得用了 Caveman 就得把整个报表全部改成洞穴人风格。我的最终方案是整体看板继续用 ECharts只在最核心的“渠道贡献对比”区域放一个 Caveman。混合布局之下核心区域因为风格独特反而获得了更高的视觉权重。这种“少数派”打法比全盘洞穴化高级得多。6.4 没事翻翻源码收获比想象中大最后提一句Caveman 的源码并不复杂一个晚上就能读完。读它的收获不限于会用这个库你还能学到一套非常直白的 Canvas 动画封装思路如何在 requestAnimationFrame 循环里做插值、如何用纯几何图形组合出有辨识度的角色、如何用最少的配置项覆盖最多的场景。这些都是可以迁移到其他可视化项目里的底层能力。我后来自己写轻量图表时不少动画和坐标换算的灵感都来自它。如果你手头刚好有一个不要求严肃精度的内部项目不妨拿 Caveman 试试手。把一个平平无奇的指标变成一面画着原始人的墙打开页面的那一瞬间整个会议室的气氛都会不一样。