一个做运营分析的朋友前阵子发来一张截图,说他在 Excel 里做了个一维热图,一百多个数据点,结果整条图几乎全糊成深红色,只在两头零星冒出几个白格子。他把原表也发过来了,我一看就明白了——数据里有两个异常值,一个比正常水平高出十几倍,一个低到接近零。Excel 的色阶默认按区域内的最大值和最小值来分配颜色,这两个点把整个色标区间撑得极宽,中间那九十多个真正有信息量的值全被挤到同一段色带里,自然就糊成一片。
这就是一维热图在 Excel 里最常见的翻车方式。所谓一维热图,说白了就是把一列数值按大小映射成一条颜色带——数值低的地方浅,数值高的地方深,一眼扫过去就能看出哪一段密集、哪一段突然跳变、哪几个点是孤立的异常。它比折线图更适合看"分布形态"而不是"变化趋势",在传感器读数扫描、单变量时间序列状态标记、基因表达条带、问卷得分分布这类场景里都很好用。Excel 做这件事的门槛极低,不需要装任何插件,条件格式点几下就出来了。但门槛低不等于坑少,从色阶断点的设置到长序列的排布,再到最后导出成图片,每一步都有能让人返工重做的细节。下面这些内容,是我自己反复折腾过几十遍之后沉淀下来的做法,从数据准备一直到模板固化,尽量把每一步"为什么要这么干"讲透。
1. 一维热图在 Excel 里到底在做什么映射
1.1 从"一列数值"到"一条颜色带"的本质
先把底层逻辑说清楚,后面所有的操作才有依据。一维热图做的事情,本质上是三步:归一化、离散化、上色。归一化是把原始数值压到 0 到 1 这个区间;离散化是把连续的 0 到 1 切成若干段;上色是给每一段分配一个颜色值。这三步里,Excel 的条件格式自动帮你做完了全部,你只需要选一个色阶样式。但问题恰恰在这里——自动完成意味着你看不见中间过程,而中间过程里藏着你必须控制的东西。
举个具体的例子。假设你有 100 个测量值,范围从 3.2 到 88.7,中位数 42.1。归一化公式是(x - min) / (max - min),那么 3.2 映射到 0,88.7 映射到 1,42.1 映射到 0.454。如果你的色标是"深蓝—白—深红"三色发散型,中间白点设在 0.5,那中位数 42.1 其实是被归到偏蓝那一边的。人在看图的时候会下意识觉得"白色是中性的、红色是高、蓝色是低",如果白色没有落在数据的重心上,整张图的直觉判断就会偏。这就是为什么后面要专门讲"中点必须手动钉死"。
归一化还有一个隐性假设:数值的间距是有意义的。如果你的数据是线性分布的,这条假设成立;但如果数据里有几个大值把范围撑开,线性归一化就会让大量小值挤在 0 附近。这时候你会看到"糊成一片"的现象,而问题不在色阶,在归一化方式本身。
1.2 三种能落地的形态与各自的适用边界
在 Excel 里做一维热图,实际有三条技术路线,各自的成本和效果差别很大,选错路线会白费功夫。
第一种是单元格填充色,也就是条件格式色阶。把每个数据点占一个单元格,单元格的底色按数值上色。这是最正宗、最接近"热图"本意的做法,色块是纯色方块,边界清晰,可以精确控制每个格子的尺寸。缺点是它依赖单元格,数据量大的时候要占很多行列,而且单元格本身有边框、有网格线,视觉上需要额外处理。适合数据点在几十到几千这个量级,且需要精确配色对比的场景。
第二种是数据条,也就是条件格式里的"数据条"功能。它会在单元格内画一个横向条,条的长度对应数值。严格说这不算热图,更像是一个内嵌条形图。但它有几个天然优势:不用管单元格尺寸,一行一个数据点,纵向排列非常紧凑;支持实心填充和渐变填充;负值可以用不同颜色区分。适合"看相对大小"比"看颜色深浅"更重要的场合,比如看排名、看占比。
第三种是字符热图,用REPT函数生成一串重复字符来模拟条形。这听起来很土,但在某些环境下特别有用——比如你需要把结果贴进纯文本的邮件、聊天窗口,或者贴进不支持富文本的工单系统里。字符热图是纯文本,走到哪里都不会丢格式。
| 形态 | 实现方式 | 优点 | 局限 | 适合场景 |
|---|---|---|---|---|
| 单元格填充色 | 条件格式→色阶 | 色块纯正、可精确控色、能配图例 | 占行列、需调尺寸 | 精确对比、需要配图例的正式图 |
| 数据条 | 条件格式→数据条 | 排布紧凑、自动处理负值 | 本质是条形图,不是颜色映射 | 看相对大小、排名类数据 |
| 字符热图 | REPT 函数 | 纯文本、到哪都不丢 | 精度受字体和字符宽度限制 | 纯文本环境、快速预览 |
选哪条路线,取决于你最终这张图要去哪儿。要进正式报告、要给别人看,走第一条;自己快速浏览、看个大概,第二条最快;要塞进纯文本渠道,第三条。
1.3 什么时候该用 Excel,什么时候该换工具
这里说句实在话。Excel 做一维热图的上限大概在几千个数据点,再往上就会遇到明显的性能墙和操作墙。条件格式本身很快,几千个单元格瞬时就能渲染完;但如果你要折行排布、要配图例、要调单元格尺寸,手工操作的量会随数据量线性增长。到了一万个点以上,用 Python 的 matplotlib 或者 R 的 ggplot2 几行代码就能出图,还能自动生成严格对齐的色标尺。
那 Excel 的价值在哪儿?在于它是活的。你改一个数据,颜色立刻跟着变;你把新数据粘进去,图自动更新。这个特性在"反复迭代看效果"的场景下是压倒性的优势。我在做数据清洗的时候,经常一边改数据一边盯着热图看,哪个点突变了一眼就能看出来,比看数字表格快得多。另外 Excel 的协作成本低,你把文件发出去,对方打开就能改,不用装环境、不用跑脚本。
所以我的判断标准很简单:如果这张图需要反复更新、需要给别人改,用 Excel;如果是一次性出图、数据量大、要求出版级精度,换工具。两者不冲突,很多时候是先在 Excel 里把数据摸清楚,再拿出去用别的工具出终稿。
2. 数据侧的准备:色阶被离群值绑架的根因
2.1 自动色阶为什么会把整条图压成一片
回到开头那个朋友的问题。他的数据里有两个异常值,一个 380,一个 0.3,其余九十多个值都在 20 到 60 之间。Excel 的色阶默认最小值和最大值类型都是"最低值"和"最高值",也就是取区域内的实际最小最大值。于是色标区间变成了 0.3 到 380,宽度接近 380。而中间那九十多个值只占了 20 到 60 这一小段,换算成比例就是 0.052 到 0.157,全部挤在色标最浅的那 15% 区间里。三色刻度的中间色(白色)落在 190 附近,这些值根本够不着,所以整条图看起来都是同一档浅色,肉眼分辨不出差异。
这个现象有个专门的叫法,叫动态范围压缩。本质上,色阶的分辨能力是固定的(人眼能稳定区分的色阶大概在 20 到 30 档),数据范围一撑大,每一档覆盖的数值跨度就变大,细节自然丢失。异常值的存在让动态范围被人为撑大了,压缩就发生了。
解决思路有三条,各有代价。第一条是把异常值切掉,用截断后的范围做色标;第二条是改归一化方式,用对数或者百分位;第三条是不动数据,改色阶的断点类型。我个人最常用的是第三条,因为它完全不动原始数据,可逆、可控,出问题了好回退。
提醒一句:不管你用哪种方法,都不要直接删掉异常值所在的整行数据。热图的价值之一恰恰是让异常显形,你把异常删了,图是好看了,但信息也丢了。
2.2 三种归一化方案的取舍与公式写法
如果决定在数据侧处理,有三种归一化方案值得了解,它们的适用条件完全不同。
Min-Max 归一化是最基础的,公式是(x - min) / (max - min)。优点是线性、保序、可逆;缺点是完全受极值支配,一个离群点就能毁掉整条色带。它适合数据本身分布比较均匀、没有明显离群点的场合。
Z-Score 标准化的公式是(x - mean) / stdev,把数据变成均值 0、标准差 1 的分布。它比 Min-Max 稳健一些,因为均值和标准差受单个极值的影响比极值本身小。但它的输出范围是不固定的,可能出现 -4 到 +4 这样的跨度,映射到色阶时需要再定一个截断范围,通常取 ±2 或 ±3 个标准差。
分位数截断是我最推荐的。做法是先算出数据的第 2 百分位和第 98 百分位(或者第 5 和第 95),用这两个值作为色标的上下界,超出这个范围的值一律按端点色处理。这样既保留了离群点"存在"这个信息(它会显示成最深的颜色),又不会被它的具体数值撑坏整个色标。
| 方案 | 公式 | 抗离群能力 | 可逆性 | 适用场景 |
|---|---|---|---|---|
| Min-Max | (x-min)/(max-min) | 弱 | 强 | 分布均匀、无离群点 |
| Z-Score | (x-mean)/stdev | 中 | 中 | 需要跨数据集比较 |
| 分位数截断 | 按 2%/98% 分位定界 | 强 | 弱(丢了极端值信息) | 有明确离群点、关心主体分布 |
在 Excel 里算分位数,用PERCENTILE.INC或者老版本的PERCENTILE。比如:
=PERCENTILE.INC($A$2:$A$101, 0.02) =PERCENTILE.INC($A$2:$A$101, 0.98)算出来之后,你其实不需要真的去改数据,只需要把这两个值记下来,填到色阶规则的"数字"型断点里。
2.3 用辅助列保留原始值,避免"改完就回不去"
如果你确实需要一个归一化后的辅助列(比如要拿去别处做计算),务必遵守一条铁律:原始数据列单独放在一边,绝不覆盖。我见过太多次这样的操作——直接在原列上做归一化,做完发现色阶还是不对,想回退,但原始值已经没了,只能重新导数据。
比较稳妥的排布是这样的:A 列放序号或时间标记,B 列放原始值,C 列放归一化后的值。条件格式应用在 C 列上,但图例、标注、报告中引用的数字全部来自 B 列。这样即使归一化逻辑改了,原始值随时能重算。
更讲究一点,可以给 B 列加一个"是否参与色标计算"的标记列,用IF判断把离群点排除在MIN和MAX的计算之外,但又保留在显示区域里。这样色标按主体数据算,离群点显示为端色,两全其美:
=MIN(IF($D$2:$D$101="主",$B$2:$B$101))这是数组公式,在较新的 Excel 版本里直接回车即可,老版本需要按 Ctrl+Shift+Enter。用这个方式得到的最小值,填进色阶断点,比MIN整个范围要稳健得多。
3. 条件格式色阶:把色标钉死而不是交给自动计算
3.1 三色刻度的中点为什么必须手动钉死
打开条件格式的色阶规则编辑器,会看到最小值、中点、最大值三行,每行都有一个"类型"下拉和一个"值"输入框。问题就出在这个"类型"上——默认情况下,最小值类型是"最低值",最大值类型是"最高值",中点类型是"百分比"且值为 50。
这里的"百分比"是个特别容易误解的选项。它指的不是中位数,而是"最小值到最大值之间线性位置在 50% 处对应的那个数值"。用前面的例子,最小值 3.2、最大值 88.7,50% 位置对应的值是 45.95,而真正的中位数是 42.1。两者差了将近 4,在某些数据上这个差距会更大。更极端的例子是数据严重右偏的时候,中位数可能只有最大值的十分之一,但"50% 百分比"永远落在正中,色标的中点就会严重偏高,导致绝大部分数据被压在偏冷的一端。
所以我的做法是:把中点类型改成"数字",值填实际的中位数。这样色标的"中性色"就落在数据的真实重心上,观察者看到白色的时候,知道那就是"典型值"。这个调整看起来很细小,但对图的直觉正确性影响很大。
中位数用MEDIAN算:
=MEDIAN($A$2:$A$101)如果你的数据量特别大,或者想更稳健,也可以用前面说的分位数截断后的中点,即(P2 + P98) / 2,这样和图例的上下一体感更好。
3.2 用"数字"型断点做一套可复用的色标
这是整篇文章里我认为最值得记住的一个技巧。条件格式的色阶,如果断点类型是"最低值/最高值",那它会在每次数据变化时重新计算区域内的极值。这个行为听起来是好事,实际上是麻烦的根源——只要有一个新数据超出原来的范围,整个色标就会重新缩放,昨天做的图和今天做的图颜色完全对不上,没法横向比较。
解决办法是把最小值和最大值的类型都改成"数字",填上你算好的固定值。这样色标被"钉死"了,数据在范围内怎么变,颜色映射关系都不变。这带来两个直接好处:第一,同一批图之间的颜色可比;第二,你可以把同一条规则应用到一个独立的"图例区域",让图例和数据严格同标准。
具体操作路径是:选中数据区域 → 开始 → 条件格式 → 新建规则 → 格式样式选"三色刻度" → 最小值类型选"数字"填 3.2 → 中点类型选"数字"填 42.1 → 最大值类型选"数字"填 88.7 → 三处颜色分别选深蓝、白、深红。
这里有个容易踩的小坑:如果你只改一处断点而忘了改另外两处,比如只把最大值改成了数字,最小值还是"最低值",那么当数据里出现比原来更小的值时,最小值会自动下探,色标整体仍然会漂移。三处必须全部改成数字型。
3.3 配一条和数据严格同标准的色标尺
有了钉死的断点,就能做真正的图例了。做法是造一列辅助数据,从最小值到最大值均匀取若干个点,然后应用一模一样的色阶规则。
假设要做一个 20 段的色标尺,放在 C 列第 2 行到第 21 行:
=$B$2+ (ROW()-2) * ($B$3-$B$2)/19其中 B2 是最小值、B3 是最大值。这样 C2 到 C21 就是 20 个均匀分布的值。然后选中 C2:C21,新建一条三色刻度规则,断点数值和数据区完全一致,颜色也完全一致。
关键在于:这条规则必须单独建,不要和数据区共用同一条规则。虽然理论上条件格式的"应用于"可以填多个不连续区域,但共用规则会让两个区域共享同一套断点计算逻辑(如果用了自动类型),而且后续改一处会牵连另一处。分开建两条一模一样但不关联的规则,维护起来清晰得多。
做完图例之后,把中间那些辅助数值的字号调成白色,或者把字体颜色改成和底色一样,这样你看到的就是一条纯净的色带,旁边再配上几档标注数字(最小值、中位数、最大值)。这个效果和正经绘图软件出的色标尺基本没差别。
3.4 单元格尺寸与边框的视觉校准
色块好不好看,尺寸占一半功劳。Excel 默认的单元格是长方形,做成热图会显得横向拉长,失去"色块"的感觉。要做出接近正方形的色块,需要同时调行高和列宽。
列宽的单位是字符数,行高的单位是磅,两者不是同一套度量,换算还跟当前字体和显示 DPI 有关。一个经验起点是:列宽设 2.14,行高设 14.4,在 100% 缩放、默认字体下视觉上比较接近正方形。但这不是绝对标准,你的屏幕 DPI 和字体设置不同,实际效果会有偏差。
调整的方法有两种。精细调整靠鼠标拖拽列宽边线,拖的时候 Excel 会显示像素提示,你可以对照着把行和列都调成相同的像素值(比如都调到 20 像素)。批量调整靠"开始 → 格式 → 行高/列宽",直接输入数值。我一般先用批量设个基准值,再挑一两个格子微调,调到看起来舒服为止。
边框处理上,热图默认带网格线,看起来会很碎。有两个办法:一是给数据区加一圈浅灰细边框作为"画框",内部网格线全部清掉;二是在"视图"里把网格线整个关掉,然后给色块之间加 0.5 磅的白色细边框模拟间隙。后者做出来的效果更像正经的数据可视化,色块之间有呼吸感。如果数据量特别大(比如几千个点),白色边框会让每个格子显得变小,这时候干脆不要边框,让色块连成一片,反而更能看出色的连续变化。
4. 长序列的折行排布
4.1 单行超过两百列之后的问题
一维热图最自然的排布是横向一行——左边是起点,右边是终点,像一条色带。这个布局在 100 列以内非常好用,一眼看到全貌。但一旦超过 200 列,问题就来了。
第一是显示问题。即使你把列宽压到 1,200 列也要占满一整个超宽屏。滚动查看的时候,你只能看到局部,失去了"全貌"这个最重要的价值。第二是操作问题。横向滚动的时候行标题(如果有的话)会跟着滚出去,你不知道当前看的是哪一行。第三是打印问题。横向超过一定宽度以后,打印到 A4 纸上会被强行缩放到看不清。
所以对于长序列,我的建议是超过 150 个点就折行。折成多行,每行放固定的点数,比如 50 个点一行,1000 个点就是 20 行。这样整体形状是个接近正方形的块,一屏能看全,颜色分布的整体形态一目了然。
4.2 INDEX + 行列号折行的具体写法
折行的核心公式是INDEX配合ROW()和COLUMN()算出原始序列中的位置。假设原始数据在 A2:A1001(共 1000 个点),你想折成每行 20 个,从 D2 开始铺开。
在 D2 输入:
=INDEX($A$2:$A$1001, (ROW()-2)*20 + COLUMN()-4 + 1)拆开看:COLUMN()-4在 D 列时等于 0,往右拖到 W 列时等于 19,对应每行内的第几个位置。ROW()-2在第二行时等于 0,往下一行等于 1,乘以 20 就是行偏移量。两者相加再加 1,就得到了 1 到 1000 之间的序号。
把这个公式向右填充到 W2,再选中 D2:W2 向下填充到第 51 行(20 列 × 50 行 = 1000 个点)。这样 1000 个点就被规整地铺成了一个 50 × 20 的方块。
实际使用的时候有两个细节要注意。第一个是越界保护。如果你的数据点不是正好 20 的整数倍,最后一行会有多余的格子,INDEX会返回错误值,条件格式会把这些错误值当成 0 处理,显示出不该有的颜色。加一层IFERROR或者IF判断:
=IF((ROW()-2)*20+COLUMN()-4+1 > COUNTA($A$2:$A$1001), "", INDEX($A$2:$A$1001, (ROW()-2)*20+COLUMN()-4+1))返回空字符串的格子,条件格式不会上色,视觉上就是留白,看起来很正常。
第二个是公式的引用方式。$A$2:$A$1001必须是绝对引用,否则向下填充的时候范围会跟着下移,取到的数据就全错了。这一点和普通的填充公式区别很大,容易疏忽。
4.3 折行边界处的连续性怎么保持
折行之后有个视觉问题:第一行的最后一个点和第二行的第一个点,在原始序列里是相邻的,但在图上它们一个在最右边、一个在最左边,视觉上被拉开了。看的人如果不知道这是折行排列,可能会误以为这两个点之间没有关系。
解决方式有三种。最简单的是在每行末尾留一个空白列作为分隔,让每行明显是一个独立的片段。这样视觉上就不会误读了。第二种是在每行末尾加个行首序号标注,比如在 W 列右边加一列,写上这一行对应原始序列的起止范围,看的人一眼就知道顺序。第三种是蛇形排列,也就是第一行从左到右、第二行从右到左,这样行末和行首在空间上是挨着的,连续性保持得最好。
蛇形排列的公式要稍微改一下,判断当前行是奇数行还是偶数行:
=IF(MOD(ROW()-2,2)=0, INDEX($A$2:$A$1001,(ROW()-2)*20+COLUMN()-4+1), INDEX($A$2:$A$1001,(ROW()-2)*20+(21-(COLUMN()-4))))奇偶判断加上方向反转。蛇形排列在视觉连续性上确实更好,但对阅读者来说有个认知负担——你得知道第二行是反着读的。如果这张图要给别人看,我更倾向于用第一种加空白分隔的方案,简单直白,不容易误读。
5. 绕开条件格式的两条路:字符热图与 VBA 填色
5.1 REPT 字符条能解决什么
条件格式虽然方便,但有两个场景它搞不定:一是要把结果贴进纯文本环境,二是需要在没有 Excel 的机器上展示。这两种情况,REPT函数做的字符热图就是唯一解。
基本公式是这样:
=REPT("■", ROUND((A2-MIN($A$2:$A$101))/(MAX($A$2:$A$101)-MIN($A$2:$A$101))*40,0))REPT的作用是把指定字符重复指定次数。这里用 40 作为最大字符数,把归一化后的比例(0 到 1)乘以 40,四舍五入,就得到了要重复的次数。最小值那个格子重复 0 次(空),最大值重复 40 次(满)。
这个方案有几个实在的坑要提前知道。第一,当值等于最小值时,REPT返回空字符串,那一行看起来像"漏了",视觉上很突兀。解决办法是在外面套个MAX(1, ...),保证至少有一个字符:
=REPT("■", MAX(1, ROUND((A2-MIN($A$2:$A$101))/(MAX($A$2:$A$101)-MIN($A$2:$A$101))*40,0)))第二,字符宽度依赖字体。必须用等宽字体(比如 Consolas、Courier New,或者中文的黑体在特定设置下),否则每个字符的宽度不一样,条形长度会失真,看起来歪歪扭扭。第三,最大字符数不宜太大,超过 60 个字符在大多数屏幕上会折行,一折行就全乱了。我一般用 40 到 50 之间。
字符热图还有一个变体,用不同"密度"的字符来表示深浅,比如░▒▓█这四个方块字符,按数值区间分成四档,用CHOOSE或者IFS挑一个。这样做出来的效果更像灰度热图,比单纯的长度条更有"热图感"。但四档的分辨率太粗,实际用起来我更倾向于用长度条,信息量更足。
5.2 VBA 批量填色的性能与插值坑
条件格式有一个绕不过去的限制:它的配色方案是预设的几十种,你没法自由指定任意颜色组合。如果你的配色有严格要求(比如必须贴合某个品牌色,或者要用特定的科研配色方案),那就得上 VBA。
VBA 填色的基本思路是遍历每个单元格,算出它对应的颜色,写进Interior.Color。核心代码如下:
Function LerpColor(c1 As Long, c2 As Long, t As Double) As Long Dim r1 As Long, g1 As Long, b1 As Long Dim r2 As Long, g2 As Long, b2 As Long r1 = c1 Mod 256: g1 = (c1 \ 256) Mod 256: b1 = (c1 \ 65536) Mod 256 r2 = c2 Mod 256: g2 = (c2 \ 256) Mod 256: b2 = (c2 \ 65536) Mod 256 LerpColor = RGB( _ CInt(r1 + (r2 - r1) * t), _ CInt(g1 + (g2 - g1) * t), _ CInt(b1 + (b2 - b1) * t)) End Function这里有个容易写错的地方:Excel 的Color属性是 BGR 排列而不是 RGB,取值的时候红色在最低字节、蓝色在最高字节。Mod 256取到红色,\ 256 Mod 256取到绿色,\ 65536 Mod 256取到蓝色。运算优先级上,整除\比Mod高,所以必须加括号,不然算出来的绿色通道全是错的——这个错误特别隐蔽,因为颜色看起来"有变化",只是不对,很难一眼发现。
主循环大概是这样:
Sub PaintStrip() Dim rng As Range, c As Range Dim vMin As Double, vMax As Double, vMid As Double, t As Double Set rng = Range("B2:B101") vMin = Application.WorksheetFunction.Min(rng) vMax = Application.WorksheetFunction.Max(rng) vMid = Application.WorksheetFunction.Median(rng) Application.ScreenUpdating = False For Each c In rng If c.Value >= vMid Then t = (c.Value - vMid) / (vMax - vMid) c.Interior.Color = LerpColor(RGB(255, 255, 255), RGB(180, 20, 40), t) Else t = (vMid - c.Value) / (vMid - vMin) c.Interior.Color = LerpColor(RGB(255, 255, 255), RGB(20, 60, 140), t) End If Next c Application.ScreenUpdating = True End Sub性能上,关键是开头关掉ScreenUpdating。不关的话,每个单元格改色都会触发重绘,一千个格子能卡上半分钟;关掉之后一千个格子一两秒就能跑完。一万个格子大概五到十秒,这个量级还勉强能接受,再往上就建议改用条件格式或者换个工具了。
还有一个边界问题:如果vMax和vMid相等(比如数据里大部分是同一个值),除法会报错。稳妥的做法是在循环前加个判断,If vMax = vMid Then Exit Sub之类的保护。
5.3 插值空间的选择:RGB 还是 HSL
上面那个LerpColor函数是在 RGB 空间做线性插值。这对单向渐变(比如白色到深红)完全够用,但跨色相的时候会出问题。
问题出在 RGB 插值会经过"灰调区"。比如从纯红插值到纯蓝,中间点会变成一种发灰的紫色,看起来脏兮兮的,不像正经热图该有的清透色。这是因为 RGB 空间里,红和蓝的中间值在感知上并不"饱满"。人眼对颜色的感知是非线性的,RGB 的线性插值不符合同一感知步长。
解决办法是改在 HSL 空间插值。HSL 把颜色拆成色相、饱和度、亮度三个维度,色相是个环形量。在 HSL 里插值,色相沿着色环走,饱和度保持高位,出来的颜色始终饱满,不会发灰。代价是要写一堆转换函数(RGB 转 HSL、插值、HSL 转 RGB),代码量翻几倍。
不过说实话,对于绝大多数热图场景,你根本用不到跨色相插值。常见的热图配色就三类:单色渐变(浅到深同一个色相)、双向发散(蓝—白—红这种,中间有白点分段)、近似 viridis(多段但相邻色相接近)。前两类在 RGB 分段插值下效果都不错,第三类主要靠预设色表而不是实时计算。所以我的实际建议是:能用条件格式就用条件格式,非要用 VBA 就老老实实做分段线性插值,别为了炫技写 HSL 转换。
6. 配色、导出与模板固化
6.1 色觉友好配色与灰度兜底
配色这件事,视觉上好看是次要的,保证所有人能读懂才是主要的。红绿色觉障碍在人群中占比不低,纯红到纯绿的发散配色对这部分人来说几乎等于两种深浅差不多的颜色,热图的核心信息(谁高谁低)会直接丢失。
比较安全的几类配色:单色渐变永远安全,因为它只靠明度区分,任何人都不可能搞错;蓝—白—红的发散配色也相对安全,因为蓝和红在大多数色觉障碍者眼里仍然能被区分开;要避免的是红—绿、绿—棕、橙—绿这类组合,这些是风险最高的。
如果要做多段的彩虹配色,可以参考 viridis 的几个锚点色:#440154、#414487、#2A788E、#22A884、#7AD151、#FDE725。这组颜色的特点是明度单调递增,即使转成灰度图,深浅顺序也保持一致。条件格式里做不了六段渐变(最多三段),但可以用 VBA 分段插值实现,或者退一步用三段近似:深紫、青绿、亮黄。
灰度兜底是另一件容易被忽略的事。很多报告是黑白打印的,如果你的热图在彩色下层次分明、在灰度下变成一片均匀的灰,那就白做了。做图的时候务必切一下"视图 → 页面布局 → 黑白"或者直接用系统截图工具转灰度看一眼,如果灰度下还能看出深浅差异,这张图才算合格。单色渐变在灰度下表现最好,双向发散次之,彩虹配色基本会失败。
6.2 导出时色阶丢失的几种情况和排查
做完的图要进报告,导出这一步翻车的概率不低,我遇到过好几种情况。
最常见的是粘贴到文档后颜色变了。从 Excel 复制区域,直接 Ctrl+V 粘贴到文档编辑软件里,如果选的是"保留源格式",大多数情况下条件格式的填充色会被"拍平"成静态色块,效果正确。但如果选的是"只保留文本",那就只剩数字;如果选的是"合并格式",有时会继承目标文档的样式,把填充色吃掉。稳妥做法是粘贴后立刻检查,不对就撤了重粘。
第二种是导出 PDF 后颜色变淡。这通常和页面设置里的"单色打印"选项有关。如果这个选项被勾上了,所有颜色都会按亮度转成黑白色阶,彩色热图会变成灰度图。位置在"页面布局 → 页面设置 → 工作表 → 单色打印",确认不要勾选。另外还要检查"打印质量"是不是设成了低分辨率,低分辨率下渐变会被压缩成很有限的几档色,看起来断断续续。
第三种是粘到在线文档或聊天工具里丢色。这个无解,只能改换方案。最稳的办法是先把热图区域复制,然后用"选择性粘贴 → 图片"粘成一张图片,再把这图片贴到任何地方。图片是固定的,不会因为宿主环境不同而变化。如果要做多次复用,可以用"照相机"工具——把数据区域拍成一张活的图片,数据变了图片跟着变,同时又能像图片一样贴到任何地方。这个工具默认不在功能区,需要手动加到快速访问工具栏:文件 → 选项 → 快速访问工具栏 → 从"不在功能区中的命令"里找到"照相机"。
一个细节:转成图片之前,先把行高列宽调好、把网格线关掉、把不需要的标注删掉。图片是"定格的"状态,之后再怎么改原表都不会影响这张图。所以什么时候转图片,要放在最后一步。
6.3 把整套东西固化成模板
如果这件事你要重复做(大概率是的),花半小时把整套东西固化成一个模板,后面能省下大量时间。模板里应该包含这几样东西:一个数据输入区、一个公式自动算出的归一化辅助区、一条钉死了断点的色阶规则、一条独立的图例规则、一套调好了尺寸的单元格格式。
关键技巧是用"表格"(Ctrl+T)来承载数据区。表格有个特性:往下面追加数据行的时候,条件格式的"应用于"范围会自动扩展,新加的行自动继承色阶。这比手动调整范围省事得多,也不容易漏行。要做的是把数据区转成表格,然后选中数据列建色阶规则,规则会自动绑定到整个表格列。
模板里还可以预置几个常用配色方案,用条件格式的"规则优先级"来切换——建好几条规则,需要哪条就把其他条的"如果为真则停止"勾上。这个做法有点绕,更实用的办法是直接在模板文件里放几个并排的示例区域,用的时候复制那一段就行。
最后一个经验:模板做好之后,把色阶断点数值单独写在一个显眼的单元格里,标注清楚。比如在表格顶上加一行"色标范围:最小值 / 中位数 / 最大值",这三个格子就是断点的唯一来源。需要调整色标的时候改这三个格子,然后到条件格式里同步改一次。虽然还是要手动同步,但至少不会忘记改的是哪三个数。
我个人在实际操作中的体会是,Excel 做一维热图这件事,90% 的时间花在前期把色标定准,10% 的时间花在画图上。色标一旦定死了,后面的排布、配色、导出都是机械操作。所以别急着点条件格式,先把数据的分布摸清楚,把中位数和分位数算出来,把色标的三个断点写在纸上,再动手上色。这个顺序反了,后面返工的次数会让你印象深刻。