Cell by Cell
关于年轮

技术biopalette 01

颜色也应该有出处

我们会记录数据、代码和图像的来源,颜色却常常只剩下一串匿名的十六进制值。biopalette 始于一个简单的想法:颜色也应该保留自己的出处。

  • 发布时间
  • 预计阅读6 分钟
  • 语言ENZH
  • biopalette
  • R
  • Color
  • Data Visualization
  • Scientific Figures

画图的时候,我常常会停在颜色这里。

数据已经整理好,图的结构也已经确定,剩下的问题看起来很小:用什么颜色?有时我会打开 以前的代码,找一组用过的色值;有时翻一遍熟悉的调色板;更多时候,只是在几种看起来 都还可以的选择之间反复试,直到其中一组不再显得碍眼。

颜色也会在另一个时刻出现。读论文、看电影,或者只是经过一张图像时,我偶尔会觉得: 这几个颜色放在一起真好。如果以后画图时还能用到它们就好了。

于是我开始把这些颜色留下来。

最初留下的只是一排十六进制色值。它们被放进脚本、笔记或某个临时文件里,名字可能是 colorsmy_palette,也可能根本没有名字。过一阵再看,颜色还在,我却已经不记得 它们来自哪里,不记得原图是什么样子,也不记得当初为什么觉得它们值得保存。

我们会认真记录一份数据从哪里来,一段代码依赖哪个版本,一张图片出自谁的作品;颜色却 经常在被复制的那一刻失去身份,只剩下几个匿名的 # 号。

我慢慢觉得,这不太对。颜色也应该有出处。

留下颜色,也留下看见它的那一刻

一套配色的来源,并不只是写在末尾的一条引用。它解释了这些颜色为什么会同时出现。

walter_white 来自《绝命毒师》试播集的海报:Walter White 站在新墨西哥的荒漠里, 身后是天空、灌木和被洗淡的地平线。我先从中读出一套冷色的发散配色——天空蓝在一端, 深橄榄绿在另一端,中间由近乎失去颜色的灰白承接。它后来可以进入差异表达热图或效应值 图形,但颜色之间的关系,最初来自那片天空怎样落向荒漠。

同一张海报还可以有别的读法。把天空、灌木、房车、沙地和烟雾看成五个并列的对象,得到 的是定性配色 walter_white2;沿着玫瑰色、暖米色与绿色重新组织,又成为另一套发散配色 walter_white3。来源没有替我决定唯一的答案,却让每一种取舍都有迹可循。

《绝命毒师》试播集海报中的 Walter White、新墨西哥天空与荒漠
《绝命毒师》试播集海报。walter_white 从画面里的天空、地平线与荒漠而来。
由蓝色、灰白与橄榄绿色组成的五级发散色条
天空蓝、灰白与深橄榄绿,组成了 walter_white 对这张海报的冷色解读。

bcell_clusters 则来自一篇发表于 Cell 的泛癌单细胞 B 细胞研究。Figure 1B 用 20 个 颜色区分不同的 B 细胞 cluster;biopalette 保留了图例色块的顺序,也留下了它们原本 对应的细胞亚型。但这些颜色并不是在真空中独自工作:原图还使用了标签、位置、分组和 分隔线。知道这一点很重要,因为它提醒我,换到另一张图里时,不能要求颜色独自承担 20 个类别的全部辨识工作。

论文 Figure 1B 中二十个 B 细胞 cluster 及其颜色图例
Cell(2024)论文的 Figure 1B。二十种颜色与细胞亚型、位置和标签共同工作。
从论文图例重建的二十色色条
重建为 bcell_clusters 后,色值、原始图例顺序与标签被一起留下。

同样一组色值,如果只看色条,可能显得漂亮;回到来源以后,才能看见它原本解决的是什么 问题。出处不是给颜色增加一个动人的故事,而是保留判断它的依据。

这也是 biopalette 最早的念头:与其再建一个只收藏色值的列表,不如让每套配色带着 名字、类型、来源和适合使用的场景一起留下来。以后再次遇见它时,我拿到的不只是几个 颜色,也能知道自己正在拿走什么。

从收藏变成可以使用的东西

当然,记住出处还不够。颜色最终还是要回到图里。

我需要在 R 中用一个稳定的名字找到它,需要决定它是用于无序类别的定性配色、表示数值 高低的渐进配色,还是围绕中点向两侧变化的发散配色;需要在颜色太多或太少时给出明确的 处理方式,也需要让它进入 base R 和 ggplot2,而不是每次都把一串色值复制进脚本。

这些需要逐渐把最初的收藏变成了一个 R 包:

library(biopalette)

get_palette("bcell_clusters", n = 5)
get_palette("three_body")
preview_palette("walter_white")

scale_color_biopalette("three_body")
scale_fill_biopalette_gradient("mitonuclear_blue")

名字在这里很重要。#1991A9 很难被记住,walter_white 却会把颜色带回它的来源。 mitonuclear_bluemitonuclear_orange 不只说明色相,也保留了它们在原始科研图形中 分别对应年轻与衰老状态的线索。名字不是装饰,它是从代码返回语境的一条路。

但一个 R 包适合回答“怎样调用”,不适合装下所有事情。源图、色表、示例数据、为什么 这样使用,以及最后做出了什么图,如果全都挤进函数文档,颜色虽然有了出处,出处却很难 真正被看见。

于是这些内容被放进了 Tessera。在那里,一套配色可以和来源素材、toy dataset、 作图代码与渲染结果待在一起。biopalette 是把颜色带进 R 的接口,Tessera 更像是它们 带着上下文停留的地方。

出处不会替颜色作担保

保留来源并不意味着一套配色从此就是“正确”的。

一张电影画面可以有很好的气氛,却未必能给十个类别提供足够清楚的差异。一套来自论文的 颜色可以在原图中成立,换到不同背景、不同面积和不同图形以后,也可能变得难以辨认。 有些颜色适合大片填充,缩成散点就会靠得太近;有些渐变看起来顺滑,却不能准确表达一个 有意义的中点。

甚至原图的用法本身也不能被盲目照搬。lactate_steps 在来源图中对应五个研究阶段, 但这些颜色的明度并不单调变化。它适合把五个阶段作为离散类别展示,却不应该因为“阶段” 听起来有顺序,就被插值成连续渐变。

出处给了我重新判断的起点,而不是免检证明。

这也是为什么后来需要 Palette Lab:固定数据和图形结构,只替换配色,看看同一组颜色 进入散点图、箱线图、堆叠图、生存曲线或热图以后会发生什么。色条上的和谐只是第一眼, 图形中的辨识、层次和重点才是它真正要完成的工作。

Palette Lab 中由不同配色渲染的多种统计图形
在 Palette Lab 里,配色离开色条,进入散点图、热图、生存曲线和其他真实图形。

在这条路的另一端,还有更小的工具。看到一张值得留下的图像时,Palette Extractor 帮助我取出并整理候选颜色;需要把颜色放进不同环境时,Color Converter 在 HEX、 RGB 和带 Alpha 的表示之间转换,并生成可以直接带走的代码。它们没有改变“颜色应该有 出处”这个想法,只是让从看见到使用的路少一些摩擦。

我真正想保存的是什么

现在回头看,biopalette 保存的当然还是颜色。没有那些十六进制值,任何来源与说明都没有 办法进入代码。但我真正不想丢掉的,是颜色被看见时的关系:它与原图的关系,与其他颜色 的关系,与某种数据结构的关系,以及它在一张图里承担过的工作。

这样的记录也让我在复用时多问几个问题:我喜欢的是这个颜色本身,还是它与背景形成的 对比?原图依靠颜色区分类别,还是同时依靠标签和位置?这套配色离开原来的面积、顺序与 语境以后,还剩下什么?

这些问题不会自动给出一套完美配色。它们只是让“看起来不错”不再是唯一的理由,也让一组 颜色不必每次都从匿名状态重新开始。

biopalette 现在可以直接从 CRAN 安装:

install.packages("biopalette")

但上线 CRAN 不是这个故事的起点,也不是终点。它只是意味着,那些从论文、电影和图像中 留下来的颜色,如今有了一个更稳定的入口,可以走进下一张图。

而无论它们走到哪里,我仍然希望能沿着名字回去,找到它们最初来自的地方。