做遗传学相关研究的人,应该都经历过这种场景:手头有一批分子标记、QTL位点或者全基因组关联信号,老板让你做一张图,把密度分布和标记位置同时展示在一套染色体上。以前最常见的做法是本地装R、装Python,配好环境再一顿折腾,才能出一张像样的图。但这两年实验室里的协作方式变了:合作者要快速看结果、项目要出在线数据报告、很多不带代码背景的实验组同事也希望有个网页能直接上传表格出图,于是“在线绘制染色体叠加密度和标记图”这个需求被反复提出来。
这篇文章我会从一个真实跑过很多次这类图的人的角度,把数据准备、能落地的在线方案、云环境里可以照抄的代码,以及从“能看”到“能发文章”之间的细节决策全部拆开讲。适合三类人:刚接触群体遗传、基因组可视化,想快速出图但不想折腾本地环境的研究生;想给课题组建一个内部在线画图入口,让不会写代码的同事也能自助出图的干湿结合团队;以及准备投稿,需要可复现、可导出高质量矢量图的研究者。
1. 先说清楚“在线”这个词:四种真实需求对应四条不同路线
1.1 不同人嘴里的“在线”,根本不是一回事
我在好几个群里回答过类似“有没有在线画染色体图的工具”的问题,发现背后需求差异很大。整理一下大概有四类:
第一类是“我就要一张图,不想装环境”。这种人给个在线R环境也行,给个上传CSV就能出图的页面也行,甚至你帮他画好发过去也行。他们要的是零安装、很快速、能下载结果。
第二类是“我想把结果做成可分享的网页报告”。比如给审稿人、合作者看,他们不需要自己画图,而是希望鼠标移上去能看到标记名、密度值这类交互信息。这类需求其实重点在网页展示,不一定要在浏览器里现场跑计算。
第三类是“我想给组里同事做一个上传-画图的小工具”。这种人要的是真正的小型Web应用,同事们把数据传上去,页面返回图,大家自己下载。
第四类是“我就想找一个免费现成网站,啥也不用维护”。这是我最常遇到的,但也是目前最尴尬的一类,原因下面展开讲。
把这四类需求拆开,好处是不会因为找不到“一个完美在线网站”而卡死进度。很多情况下,用云端R环境、或者自建一个Plotly小页面,比寻找现成工具靠谱得多。
1.2 现成的免费在线网站,为什么总是差一点
浏览器里直接画染色体图、又能叠加密度的现成工具确实存在。一些主流基因组浏览器自带染色体视图,部分物种数据库也有标记分布查看器,能满足一部分需求。但实际用起来,问题很集中:
- 绑定了一个固定的参考基因组。你想画自己的物种、自己的群体标记,它不让你自定义。
- 对“叠加”的支持比较弱。有的只能显示注释基因,不能把你自己算好的窗口密度叠上去。
- 数据隐私没保障。未发表数据的位点信息在很多课题组里属于核心机密,上传到公共网站总是心里打鼓。
- 出图样式和分辨率往往达不到期刊要求,导出的格式要么是截图,要么是低分辨率PNG。
所以我个人的结论是:在这个需求上,“在线”更应该理解成“工作流程在云端可复现、结果可以网页分享”,而不是非要找一个鼠标点点的现成网站。沿着这个思路往下走,你会发现可选的路其实很宽。
1.3 落地顺序建议
如果你现在就要开始做这件事,我的建议是:先把数据整理规范,这一步决定后面所有工作的成败;然后根据你的场景选一条路线——临时出图走云端R环境加RIdeogram,组内长期用就搞一个Plotly加Streamlit的小应用;最后统一处理配色、导出格式、字体这些细节。下面展开讲每一步的坑和技巧。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 出图之前,先把三张基础表彻底搞清楚
染色体叠加图看起来很花哨,其实底层的输入就三类数据:染色体长度/带型、密度数据的区间表、标记的位置表。所有在线工具和绘图包,本质上都是把这三份数据映射到画布上。数据不对,后面用什么工具都白搭。
2.1 染色体骨架:长度和带型
第一张表是染色体的骨架。最简单的版本只需要两列:染色体ID和总长度,单位通常用bp。
code复制chr1 248956422
chr2 242193529
chr3 198295559
如果你希望画出带G显带纹路的染色体,还需要每一条染色体的区带文件,一般包含染色体ID、起始位置、终止位置、染色深浅档位这四列。这个文件可以从UCSC Genome Browser或Ensembl下载,自己的物种可以从基因组注释文件里提取,或者干脆自己按着基因密度生成一个简化版本的带型。
这里有一个非常容易忽略的坑:染色体ID必须全文件一致。同一个染色体,在长度表里叫“chr1”,在标记文件里叫“1”,程序就会当作两条不同的染色体处理,画出来要么缺染色体,要么标记全挤在一个莫名其妙的位置。这种错误不报错、不提示,图表看起来也正常,等你提交审稿时被审稿人抓住才麻烦。
2.2 密度数据:窗口才是核心概念
密度图其实是在回答一个问题:每个区间里有多少个我们关心的事件。以SNP密度为例,做法是把染色体划分成等长的窗口,统计每个窗口内的SNP个数。窗口大小直接决定图片细节:
- 窗口太小,噪音大,相邻窗口差别显著,图看着像心电图。
- 窗口太大,把所有信号都磨平了,看不出热点区域。
常用的经验是先取染色体平均长度的五百分之一到三百分之一作为初始窗口。比如一条250Mb的染色体,窗口取500kb到800kb左右比较合理。画出来如果锯齿感太强,再适当放大窗口。
密度表的结构一般是四列:
code复制chr1 0 500000 34
chr1 500000 1000000 52
chr1 1000000 1500000 28
就是染色体ID、窗口起始、窗口终止、窗口内的统计值。注意统计值可以是SNP个数、基因个数、重组率、核酸多样性,什么都可以,但务必在图上标明这是什么密度。我见过不少图只有一条“看起来像密度”的曲线,不说清指标,读者只能猜,这种图信息量直接打对折。
2.3 标记数据:不只是画点,还要能看懂
标记数据是第三张表,最简单的结构是染色体ID、物理位置、标记名称三列:
code复制chr1 1250000 Marker_A
chr1 2300000 QTL_B
chr2 35000000 Gene_C
如果需要给标记分组着色,比如区分已克隆基因、QTL、SNP标记,就再加一列分组信息。标记数据的常见坑是位置坐标系不一致。BED格式是0-based,第一碱基的起始位置是0;很多表格会用1-based,第一碱基是1。相差一个碱基对画图其实看不出来,但是当你后续要去提取实际序列、做比较时,一个碱基的偏差会导致提取结果错误。建议在整理数据时统一走1-based,并把单位明确写在脚本注释里。
2.4 智能检查:这套数据在跑图前应该过三道自查
第一,所有文件的染色体ID完全一致,包括大小写;第二,密度表和标记的位置范围不超过对应染色体长度;第三,单位统一为bp,不要出现某个文件是Mb、另一个文件是bp的情况。这三项检查用Excel的筛选或几行R代码两分钟就能完成,却能让后面少走很多弯路。
3. 最快落地的在线体验:云端R环境跑通RIdeogram
如果你是非生信背景的湿实验研究者,或者只是临时需要一张图,我强烈建议用云端R环境加RIdeogram这个组合。它是我目前见过把“效率”和“专业感”平衡得最好的方案,而且操作过程中只需要浏览器,是名副其实的“在线绘制”。
3.1 为什么在“在线节奏”下首推RIdeogram
RIdeogram是一个专门画染色体级别图层叠加的R包,它最大的优势是遵循染色体真实带型的位置逻辑,能把带型、密度、标记按坐标精确叠加到同一条染色体上。顶部画标记,中部画密度,底部是染色体骨架,层次清楚,出来的图直接可以放组会。
更重要的是它完全适配云端R环境。你把三个数据文件上传到一个RStudio Cloud项目里,运行同一套脚本,输出SVG、PNG、PDF,整个流程没有任何本地依赖包袱。我经常在一个云项目里保存不同物种的脚本,下次换数据只需要重新上传表格,运行一遍,十分钟内出图。
3.2 云端操作流程
整体就是在浏览器里打开RStudio Cloud(或Posit Cloud这类云端R服务)的账号,新建一个RStudio项目,然后把事先准备好的三个文件传到项目目录。RStudio Cloud的免费额度对染色体图这种量级的数据来说完全够用,只要不拿它去跑几十G的比对数据,不会触发资源限制。
传完文件后新建一个R脚本,把下面这段跑通。
3.3 可以直接改的RIdeogram脚本
r复制# 安装RIdeogram(只用装一次)
# install.packages("BiocManager")
# BiocManager::install("RIdeogram")
library(RIdeogram)
# 1) 染色体带型文件:Chr, Start, End, fill
# 参考UCSC的cytoBandIdeo格式,也可以只用gpos100这一档
karyotype <- read.table("chrom_band.tsv", header = FALSE, sep = "\t")
# 2) 密度区间文件:Chr, Start, End, fill, color
# fill是区间填充档位,color是具体颜色(十六进制或R颜色名)
overlaid.data <- read.table("density_windows.tsv", header = FALSE, sep = "\t")
# 3) 标记文件:Chr, Start, End, Label
# 不同版本字段略有差异,运行 ?ideogram 查看帮助文档确认
label.data <- read.table("markers.tsv", header = FALSE, sep = "\t")
# 只用密度,不叠加标记时,把label=NULL传入即可
ideogram(
karyotype = karyotype,
overlaid = overlaid.data,
label = label.data
)
# 导出成SVG,再转成PNG
convertSVG("chromosome.svg", device = "png", width = 12, height = 6)
简单解释下代码逻辑。karyotype是画布的骨架,决定了每一条染色体在画面中的长度比例和带型深浅。overlaid.data是密度的叠加层,RIdeogram会根据你传入的区间坐标和颜色,在染色体对应位置生成一个与带型交叠的密度块。label.data是标记层,用来画那些需要给出名称的基因、QTL或SNP点。
实际跑之前,建议先运行data(package = "RIdeogram")看看包内自带的示例数据。里面有人类、小鼠等物种的karyotype和基因示例文件,对照官方示例格式改自己的文件,比我在这里写一百句都管用。
3.4 关于颜色和透明度的细节
RIdeogram的overlaid层里,fill和color是两回事,fill是这个区间的填充档位,你可以理解成一个分组标签,color才是最终显示的颜色。比如你做了一个SNP密度和基因密度的对比,可以用fill来区分两层,再给两层不同的颜色。想让密度块有半透明效果时,可以直接给十六进制颜色加透明度,比如#FF000080就表示红色50%透明度。这个技巧在叠加多层数据时特别有用,不然后画的那层会把前面的全盖上。
3.5 云端方案的体验与边界
在云端R环境里做图,体验非常接近“在线出图”:数据在浏览器里上传,代码在浏览器里运行,结果在浏览器里预览下载。和本地R没有任何区别,只是服务器换到了远端。
局限也有。一是云端环境偶尔会掉线,长时间不操作可能需要刷新重连;二是免费额度对超大个体数量会吃紧;三是如果你追求“鼠标拖动标记看名称”的交互效果,RIdeogram给不了,因为它是静态导出工具。这个需求就需要下一种方案了。
4. 想要网页交互?用Plotly加Streamlit自建一个轻量在线画图器
如果需求不是“十分钟出一张图”,而是“课题组同学以后都能自己传数据、网页上交互看标记”,那RIdeogram就不够用了。这种场景我建议自己写一个Streamlit小应用,用Plotly做绘图后端。Streamlit负责网页上传和控件,Plotly负责交互渲染,整个代码量控制在百行左右,组内跑起来非常顺手。
4.1 为什么是Plotly而不是matplotlib/ggplot
matplotlib出来的图是静态的,标记名只能靠文本标注,重叠在一起就完全没法看。Plotly天生支持鼠标悬停查看标记名、缩放染色体局部区域,而且生成的图可以以HTML形式保存分享,这一点对“在线”需求几乎是量身定做的。我在组内搭过一次之后,同事们的反馈是“比之前发PPT文件截图方便太多了”。
4.2 一个可以直接运行的应用骨架
下面的代码假设你有一个Streamlit环境,文件存为app.py,终端运行streamlit run app.py即可在浏览器里打开。用了三个上传框分别接收染色体长度表、密度表、标记表,全部按制表符分隔。
python复制import streamlit as st
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
st.set_page_config(page_title="染色体叠加图生成器", layout="wide")
st.title("染色体密度 + 标记在线绘制")
sep = st.selectbox("表格分隔符", ["\\t", ","], index=0)
chr_file = st.file_uploader("上传染色体长度表:Chr Length", type=["txt", "tsv", "csv"])
den_file = st.file_uploader("上传密度表:Chr Start End Value", type=["txt", "tsv", "csv"])
marker_file = st.file_uploader("上传标记表:Chr Pos Name(可选)", type=["txt", "tsv", "csv"])
if chr_file and den_file and marker_file:
chrs = pd.read_csv(chr_file, sep=sep, header=None,
names=["chr", "length"])
dens = pd.read_csv(den_file, sep=sep, header=None,
names=["chr", "start", "end", "value"])
markers = pd.read_csv(marker_file, sep=sep, header=None,
names=["chr", "pos", "name"])
dens["mid"] = (dens["start"] + dens["end"]) / 2
n_chr = len(chrs)
fig = make_subplots(
rows=n_chr, cols=1, shared_xaxes=False,
row_heights=[1] * n_chr,
vertical_spacing=0.03,
subplot_titles=chrs["chr"].tolist()
)
for i, row in chrs.iterrows():
rid = i + 1
c = row["chr"]
# 染色体骨架线
fig.add_trace(
go.Scatter(
x=[0, row["length"]], y=[0, 0],
mode="lines",
line=dict(color="#333333", width=6),
hoverinfo="skip",
showlegend=False
), row=rid, col=1
)
# 密度折线,填充到0
sub = dens[dens["chr"] == c]
if not sub.empty:
fig.add_trace(
go.Scatter(
x=sub["mid"], y=sub["value"], mode="lines",
line=dict(color="rgba(30,120,180,0.8)", width=2),
fill="tozeroy",
name="密度" if i == 0 else None,
showlegend=(i == 0)
), row=rid, col=1
)
# 标记散点,悬停显示名字
mark = markers[markers["chr"] == c]
if not mark.empty:
fig.add_trace(
go.Scatter(
x=mark["pos"], y=[0.15] * len(mark),
mode="markers", marker=dict(size=8, color="#D62728"),
text=mark["name"], hovertemplate="%{text}<br>%{x}<extra></extra>",
name="标记" if i == 0 else None,
showlegend=(i == 0)
), row=rid, col=1
)
fig.update_layout(
height=120 * n_chr + 80,
hovermode="closest",
margin=dict(l=40, r=20, t=30, b=40)
)
fig.update_yaxes(visible=False)
st.plotly_chart(fig, use_container_width=True)
# 导出SVG需要kaleido:pip install kaleido
svg_bytes = fig.to_image(format="svg")
st.download_button("下载SVG矢量图", data=svg_bytes,
file_name="chromosome.svg", mime="image/svg+xml")
这个版本我把代码逻辑写得很直白:每一条染色体是一行,左侧显示染色体名;中间的骨架线用深色粗线表示染色体;密度层用半透明折线填充,标记层在染色体线上方用红色散点表示,鼠标悬停会显示标记名称。
如果不想手动写这些代码,还有一个更省事的方式:自己先在浏览器里跑通一遍,然后推到组内公用的服务器或云应用平台上,同事拿一个链接就能访问。这个部署的细节每个平台不太一样,但思路是一致的:把app.py当成一个标准的网页服务部署,“上传数据出图”就变成组内日常工具了。
4.3 自建工具的维护边界
一定不要想着做一个生产级平台,那是过度设计。Streamlit小应用的好处是代码量小、好调整,适合组内维护。如果有一天数据结构变了、或者想加一个分组着色控件,改几行代码再部署就行。代价是长期维护需要有人负责,否则过半年同事找不到人改功能,又回落到“求人画图”的状态。
5. 画出来只是第一步:从“能看”到“能发文章”的细节决策
很多人做到上一步就停了,觉得图已经出来了,剩下的事都是小事。但实际上,细节上的几个决策决定这张图最终能不能放进论文、能不能让审稿人在两秒内抓住重点。
5.1 标记太多时不要硬塞文本
一张染色体图上如果有一百个标记,每个都标名字,结果就是所有文字叠在一起,谁都看不清。我的处理原则是:只给与研究结论直接相关的标记显示文本,其余标记用不带动文字的小圆点表示。在Plotly这种交互式图里更简单,标记名全部放悬停提示里,图上只保留位置信息。RIdeogram里则可以分多个label层,或者用颜色区分级别,依然只展示重点。
5.2 密度色板的选择要尊重色觉缺陷人群
如果密度值最终要映射成颜色,请优先考虑Viridis或Inferno这类渐变色板,它们对色盲人群友好,而且在灰度打印时也有足够层次。经典错误是红绿渐变,红绿色盲是最常见的色觉缺陷类型,用红绿渐变等于直接得罪相当比例的读者。还有就是密度值极差很大时,比如一组数据最大值是几千、大部分窗口只有几十,线性色标会让绝大多数窗口看起来“同一个颜色”,此时先做log2转换或分位数压缩再映射色标,效果会好很多。
5.3 矢量图和位图的场景要分清
投稿图的硬性要求通常包括矢量格式。RIdeogram的convertSVG直接输出SVG,Plotly方案里用fig.to_image(format="svg")(需要pip install kaleido)也能拿到矢量图。如果你需要用Word排版、非要PNG,至少保证分辨率在300dpi以上,否则放到整页宽的排版里全是锯齿。
5.4 在线环境里最容易翻车的是中文字体
云端R环境和Streamlit部署到Linux服务器时,默认字体集里往往没有中文字体,标记名如果是中文,导出PDF或SVG时会出现一堆方块。这个问题极其隐蔽,因为你盯着网页看的时候字体渲染是正常的,一导出就露馅。
我的处理办法是:标记名尽量用基因ID或英文名称;如果必须用中文展示,在R里安装并注册合适的字体包,在Streamlit部署环境里额外安装中文字体,并在Plotly的图里显式指定font.family。不要等到导出后再指望它自动好转,字体问题在源头解决成本最低。
5.5 一个让我印象深刻的单位坑
去年帮一个课题组画一批分子标记在图谱上的分布,第一次跑出来所有标记全部挤在染色体末端,当时第一反应是数据处理脚本出错了,查了半天也没发现问题。后来把标记坐标除以一百万对比之后才意识到,对方表格里的坐标列单位是Mb,而染色体长表用的是bp,差了整整六个数量级。这种错误程序不会报错,图上看起来“好像也有规律”,但如果带着这种图写文章,内行一眼就能看出问题。单位检查应该固定放在数据整理的最后一步,不检查不上图。
还有一次是在云端R环境里反复跑图,内存报错,数据量明明很小。最后发现是因为读入文件时表头没有设header = FALSE,第一行被当成了列名,导致所有坐标整体错位一个单位。这类低级错误用一两行str()和head()检查就能避免,但很多人包括我自己都会下意识跳过这个步骤。
个人沿着这几个章节走完一遍之后,最大的体会是:选择在线的还是本地方案,其实不是技术之争,而是协作效率之争。如果你只给自己出图,本地R脚本和云端R脚本没有任何区别;一旦涉及多人协作、跨地点分享、期刊复现性要求,云端或自建网页带来的便利会明显超过前期多花的一点配置时间。工具层面的取舍永远在其次,真正决定一张染色体叠加密度标记图好不好用的,还是数据是否规范、标注是否克制、细节是否经得起推敲。
