第一章:GO富集气泡图的核心原理与生信价值
GO富集气泡图是功能注释分析中最具表现力的可视化形式之一,其本质是将基因本体(Gene Ontology)三个核心维度——生物过程(BP)、分子功能(MF)和细胞组分(CC)——的统计显著性、富集程度与基因数量三重信息,通过气泡的位置、大小与颜色进行联合编码。横轴通常表示富集因子(Enrichment Factor = 基因集中该GO项占比 / 全基因组该GO项占比),纵轴按调整后P值(如Benjamini-Hochberg校正的FDR)降序排列关键GO条目;气泡面积正比于该GO项关联的差异基因数量,颜色梯度映射-log₁₀(FDR),越深红代表越显著。
该图在生信研究中具有不可替代的价值:它能快速定位驱动表型变化的核心生物学主题,避免冗长的富集结果表格阅读;支持跨实验条件横向比较(如处理组vs对照组气泡图并列);且天然适配多组学整合——例如将WGCNA模块特征基因集或单细胞差异表达簇分别作图,可揭示模块/簇的功能特异性。
生成标准气泡图需依赖R语言生态中的clusterProfiler与ggplot2。典型流程如下:
# 加载必需包并构建GO富集结果
library(clusterProfiler)
library(ggplot2)
# 假设deg_genes为差异基因Entrez ID向量,org.Hs.eg.db为人类注释库
ego <- enrichGO(
gene = deg_genes,
OrgDb = org.Hs.eg.db,
ont = "BP", # 可选"MF"或"CC"
pAdjustMethod = "BH",
pvalueCutoff = 0.05,
qvalueCutoff = 0.05
)
# 绘制气泡图(自动按qvalue排序,含显著性颜色与基因数尺寸映射)
dotplot(ego, showCategory = 15) # 显示前15个最显著GO项
关键参数说明:pAdjustMethod = "BH"启用FDR校正;showCategory控制显示条目数;dotplot()内部自动计算富集因子并映射至x轴,无需手动转换。若需自定义配色或坐标标签,可导出as.data.frame(ego)后用ggplot()二次绘图。常见优化包括:过滤掉过于宽泛的GO项(如”cellular process”)、合并语义相近条目(借助simplify()函数)、添加GO层级关系箭头等。
第二章:GO富集分析前的关键数据准备与质控
2.1 GO注释数据库版本选择与物种匹配实践
GO(Gene Ontology)注释质量高度依赖数据库版本与目标物种的匹配精度。不同版本的GOA(GO Annotation)或Ensembl Compara注释集在基因ID映射策略、本体更新频率及物种覆盖范围上存在显著差异。
版本兼容性关键考量
- 2023年12月版GOA:支持UniProtKB AC/ID双向映射,但对非模式生物(如Octopus vulgaris)仅提供IEA证据码注释;
- 2024年4月Ensembl Plants:内置GO-Slim子集,适配Arabidopsis thaliana TAIR10基因模型,ID前缀统一为
AT1G01010。
物种ID标准化示例
# 将NCBI Gene ID批量转换为Ensembl Plant ID(使用g:Profiler API)
curl -X POST "https://biit.cs.ut.ee/gprofiler/api/convert/orthologs/" \
-H "Content-Type: application/json" \
-d '{"organism":"ath","target_organism":"zma","query":["AT1G01010","AT1G01020"]}'
逻辑说明:
organism指定源物种(A. thaliana),target_organism为目标物种(Zea mays),返回OrthoDB直系同源ID映射表。参数校验失败将返回HTTP 400并提示缺失query字段。
推荐版本组合(模式植物)
| 物种 | 推荐GO注释源 | 版本日期 | ID系统 |
|---|---|---|---|
| Homo sapiens | GOA UniProt GAF 2.2 | 2024-03 | UniProt AC |
| Drosophila melanogaster | FlyBase FBbt | 2024-02 | FBgn |
graph TD
A[输入基因列表] --> B{物种是否在GOA官方支持列表?}
B -->|是| C[下载对应GAF 2.2文件]
B -->|否| D[调用g:Convert获取跨库ID映射]
C --> E[过滤IEA证据码条目]
D --> E
E --> F[关联GO Slim层级]
2.2 差异基因列表的标准化输入格式构建(DESeq2/limma输出转换)
为统一下游富集分析与可视化工具(如clusterProfiler、EnhancedVolcano)的输入要求,需将不同差异分析工具的原始输出映射至标准三列结构:gene_id、log2FoldChange、padj。
核心字段对齐规则
- DESeq2结果(
results()):直接提取baseMean(可选)、log2FoldChange、padj - limma-voom(
topTable(fit, coef=..., number=Inf)):logFC→log2FoldChange,adj.P.Val→padj
示例:DESeq2结果转标准DF
# 从DESeqResult对象构建标准输入
de_res <- results(dds, alpha = 0.05)
std_df <- as.data.frame(de_res)[, c("log2FoldChange", "padj")]
std_df$gene_id <- rownames(std_df)
std_df <- std_df[!is.na(std_df$padj), ] # 移除NA校正p值
rownames(std_df) <- NULL
逻辑说明:
results()默认返回已LRT/Wald检验并FDR校正的矩阵;padj列即Benjamini-Hochberg调整后p值;显式剔除padj=NA行避免下游报错。
标准化字段对照表
| 工具 | 原始列名 | 映射目标列 | 备注 |
|---|---|---|---|
| DESeq2 | log2FoldChange |
log2FoldChange |
直接复用 |
| limma | logFC |
log2FoldChange |
含义完全等价 |
| Both | padj / adj.P.Val |
padj |
必须为FDR校正值 |
graph TD
A[原始输出] --> B{工具类型?}
B -->|DESeq2| C[results() → extract columns]
B -->|limma| D[topTable() → rename & filter]
C & D --> E[统一列名:gene_id/log2FoldChange/padj]
E --> F[输出data.frame供下游调用]
2.3 背景基因集的精准定义与去冗余策略(含ENSEMBL ID→Entrez ID映射实操)
精准的背景基因集是富集分析可靠性的基石。需严格限定为当前物种、最新注释版本、蛋白编码基因,并剔除假基因、lncRNA等非目标转录本。
数据同步机制
使用 biomaRt 实时对接 ENSEMBL 112(人类)与 NCBI Gene 数据库:
library(biomaRt)
mart <- useMart("ensembl", dataset = "hsapiens_gene_ensembl")
mapping <- getBM(
attributes = c("ensembl_gene_id", "entrezgene_id"),
filters = "biotype",
values = "protein_coding",
mart = mart
)
逻辑说明:
filters="biotype"确保仅获取protein_coding基因;attributes指定双向ID字段;getBM()执行跨库映射,避免静态文件过期风险。
去冗余核心步骤
- 过滤
entrezgene_id为空或重复的 ENSEMBL ID - 对同一 Entrez ID 保留最长转录本对应的 ENSEMBL ID(依据
transcript_length注释)
| ENSEMBL ID | Entrez ID | Biotype |
|---|---|---|
| ENSG00000141510 | 7157 | protein_coding |
| ENSG00000223972 | pseudogene |
graph TD
A[Raw ENSEMBL IDs] --> B{Biotype filter}
B -->|protein_coding| C[Map to Entrez]
B -->|others| D[Discard]
C --> E[Drop NA/empty]
E --> F[Deduplicate by Entrez]
2.4 多重检验校正方法对比:BH vs BY vs Bonferroni在GO富集中的统计效力实证
GO富集分析中,成千上万的GO术语同时检验,显著性阈值必须严格校正。Bonferroni最保守,BY控制FDR更稳健,BH(Benjamini-Hochberg)则在统计效力与假发现率间取得平衡。
校正方法核心差异
- Bonferroni:α′ = α/m,极低假阳性但高假阴性
- BH:升序p值中找最大i满足 pᵢ ≤ i·α/m
- BY:BH的保守变体,分母替换为 m·∑(1/j),抗依赖性更强
R代码实现示例
pvals <- c(0.001, 0.012, 0.025, 0.048, 0.09) # 原始p值(模拟5个GO项)
adj_bh <- p.adjust(pvals, method = "BH") # BH校正
adj_by <- p.adjust(pvals, method = "BY") # BY校正
adj_bonf <- p.adjust(pvals, method = "bonferroni") # Bonferroni
data.frame(pvals, adj_bh, adj_by, adj_bonf)
p.adjust()中method="BH"执行经典线性步进法;"BY"自动引入调和数校正因子,提升对基因集间相关性的鲁棒性;"bonferroni"直接缩放,不依赖排序。
| 方法 | FDR控制 | 统计效力 | 适用场景 |
|---|---|---|---|
| Bonferroni | 无 | 极低 | 极少量假设、强保守需求 |
| BH | 弱依赖 | 高 | 独立/近似独立GO项 |
| BY | 任意依赖 | 中高 | 基因功能高度相关时 |
2.5 富集结果可信度评估:Fisher精确检验P值 vs GSEA-like富集分数的协同解读
为何单一指标不可靠?
- Fisher P值仅反映基因集在差异基因中的超几何过表达显著性,忽略基因排序与效应方向;
- GSEA-like富集分数(如NES)刻画富集强度与位置偏移,但对小样本敏感,缺乏统计严格性。
协同判据设计
# 双阈值联合过滤:兼顾统计严谨性与生物学稳健性
def is_robust_enrichment(p_fisher, nes, p_thresh=0.05, nes_thresh=1.5):
return (p_fisher < p_thresh) and (abs(nes) > nes_thresh)
逻辑分析:
p_fisher来自2×2列联表的精确概率计算;nes为归一化富集分数,经1000次基因集置换校正。双条件缺一不可——避免高NES低显著性(假阳性富集)或低P值但NES≈1(无实际富集偏移)。
判定矩阵示例
| Fisher P值 | NES | ≥ 1.5 | 结论 | |
|---|---|---|---|---|
| ✓ | 高置信富集 | |||
| ✗ | 警惕假阳性 | |||
| ≥ 0.05 | ✓ | 需核查排名鲁棒性 |
决策流程可视化
graph TD
A[输入富集结果] --> B{P_fisher < 0.05?}
B -->|Yes| C{abs NES > 1.5?}
B -->|No| D[标记为统计不显著]
C -->|Yes| E[高可信富集]
C -->|No| F[需重排/重抽样验证]
第三章:气泡图可视化底层逻辑与R绘图引擎选型
3.1 ggplot2气泡图几何对象(geom_point)的坐标映射与尺度压缩原理
坐标映射的本质
geom_point() 将数据变量(如 x, y, size)通过尺度函数(scale) 映射到图形属性空间。size 并非直接控制像素半径,而是经 scale_size_area() 默认转换为面积比例,确保视觉大小与数值成正比。
尺度压缩机制
当数值跨度大(如 [1, 1000]),原始线性映射会导致小值点不可见。scale_size_area() 内部执行平方根压缩:
$$ r \propto \sqrt{value} $$
从而保留小值点的可分辨性。
实例对比
# 启用面积缩放(推荐)
ggplot(mtcars, aes(wt, mpg, size = hp)) +
geom_point() +
scale_size_area(max_size = 12) # 将最大hp映射为直径12px的圆
✅
scale_size_area():按面积缩放,max_size控制最大圆的直径(单位:mm);
❌scale_size():按半径线性缩放,易失真。
| 缩放方式 | 映射关系 | 视觉保真度 | 适用场景 |
|---|---|---|---|
scale_size_area |
面积 ∝ value | 高 | 多数量级数据 |
scale_size |
半径 ∝ value | 低 | 数值范围窄时 |
graph TD
A[原始数值] --> B[尺度函数转换]
B --> C[面积映射 sqrt·value]
B --> D[半径映射 value]
C --> E[视觉大小与数值成比例]
D --> F[小值被压缩,大值主导画面]
3.2 clusterProfiler vs enrichplot vs ggsignif:三大R包气泡图渲染机制深度解析
渲染架构差异
clusterProfiler:基于基础ggplot2构建,气泡图(如dotplot())本质是geom_point()+geom_text()的组合,无原生显著性标记支持;enrichplot:专为富集结果优化,dotplot()内置pvalue分层缩放与geneRatio映射逻辑,支持showCategory控制节点粒度;ggsignif:独立于富集分析流程,通过geom_signif()在已有ggplot对象上叠加统计标注线,需手动对齐坐标。
核心参数对比
| 包名 | 关键气泡尺寸映射参数 | 显著性标注原生支持 | 坐标系依赖 |
|---|---|---|---|
| clusterProfiler | size (默认 Count) |
❌ | 自主计算 |
| enrichplot | x/y/size 可自定义 |
✅(pvalue 着色) |
与 ggplot 兼容 |
| ggsignif | 不控制气泡,仅加标注 | ✅(map_signif_level) |
强依赖已有坐标 |
# enrichplot dotplot 示例(自动映射 -log10(padj) 到 size)
dotplot(ea_result,
showCategory = 10,
x = "GeneRatio", # 横轴:富集基因占比
size = "p.adjust") # 气泡大小:校正后 p 值 → 越小越显著,越大越醒目
该调用将 p.adjust 经 -log10() 变换后线性映射至 size,实现“显著性越强、气泡越突出”的视觉编码,避免手动缩放失真。
graph TD
A[原始富集结果] --> B(clusterProfiler: 基础绘图)
A --> C(enrichplot: 语义化渲染)
C --> D[自动p值映射+标签避让]
B --> E[ggsignif: 后期标注叠加]
3.3 富集层级(BP/CC/MF)分离绘制与跨本体一致性校验实战
为避免生物学语义混淆,需将 Gene Ontology 的三个本体(Biological Process、Cellular Component、Molecular Function)严格分离可视化,并校验术语在不同本体中的唯一性。
数据同步机制
使用 clusterProfiler 提取富集结果后,按 ONTOLOGY 字段切分:
# 按本体拆分结果表
bp_res <- enrich_res[enrich_res$ONTOLOGY == "BP", ]
cc_res <- enrich_res[enrich_res$ONTOLOGY == "CC", ]
mf_res <- enrich_res[enrich_res$ONTOLOGY == "MF", ]
逻辑分析:
ONTOLOGY列由 clusterProfiler 自动标注,值为"BP"/"CC"/"MF";此切分确保后续绘图互不干扰。参数enrich_res为enrichGO()输出的 data.frame,含Description、Count、pvalue等关键列。
一致性校验流程
通过 GO ID 映射表检测跨本体重叠术语:
| GO_ID | BP_Count | CC_Count | MF_Count |
|---|---|---|---|
| GO:0005575 | 0 | 12 | 0 |
| GO:0003674 | 0 | 0 | 8 |
graph TD
A[原始GO富集结果] --> B{按ONTOLOGY拆分}
B --> C[BP子集]
B --> D[CC子集]
B --> E[MF子集]
C & D & E --> F[GO ID交集检测]
F --> G[报错:GO:0005737 同时出现在CC/MF]
第四章:5大高频避坑场景的诊断与修复方案
4.1 “气泡重叠遮盖”问题:基于ggrepel的智能标签避让与自定义锚点定位
当散点图中多个数据点密集分布时,geom_text() 的静态标签极易重叠,造成关键信息不可读——即典型的“气泡重叠遮盖”问题。
核心解法:geom_text_repel() 替代方案
library(ggrepel)
ggplot(df, aes(x = x_val, y = y_val, label = name)) +
geom_point() +
geom_text_repel(
direction = "both", # 允许标签向任意方向偏移
max.iter = 200, # 增加迭代次数提升收敛稳定性
seed = 42, # 固定随机种子确保可复现布局
point.padding = 0.5 # 标签与点最小间距(单位:mm)
)
direction = "both" 启用二维自由避让;point.padding 防止标签紧贴气泡边缘而削弱视觉区分度。
自定义锚点控制策略
| 参数 | 作用 | 推荐值 |
|---|---|---|
force |
标签间斥力强度 | 1–3(值越大越分散) |
box.padding |
标签框外延缓冲 | 0.3–0.8 |
segment.color |
连接线颜色 | "grey50" |
graph TD
A[原始重叠标签] --> B[启用repel算法]
B --> C[计算排斥力场]
C --> D[梯度下降优化位置]
D --> E[带箭头连接线的清晰标注]
4.2 “显著性倒挂”现象:-log10(Padj)与-log10(Pvalue)混淆导致的排序逻辑纠错
当对差异表达基因进行火山图可视化或TopN筛选时,若误将校正后P值(Padj)与原始P值(Pvalue)统一用 -log10() 转换后直接排序,将引发“显著性倒挂”——即统计上更严格(经FDR校正后仍显著)的基因反而排在低分位。
常见错误代码示例
# ❌ 错误:混用未校正与已校正P值做同一尺度转换
df$neglog10_p <- -log10(df$Pvalue) # 原始P值
df$neglog10_padj <- -log10(df$Padj) # 校正后P值(含NA及>1值!)
df <- df[order(-df$neglog10_padj), ] # 排序依据含非法值
⚠️ Padj 可能为 NA(未校正)或 >1(校正失败),-log10(>1) 为负数,-log10(NA) 为 NA,导致排序崩塌。
正确处理流程
- 仅对
0 < Padj ≤ 1的行计算-log10(Padj) - 对
Padj == NA或Padj > 1的行设为或NA并显式排除 - 排序前强制
na.last = FALSE并过滤
| 基因 | Pvalue | Padj | -log10(Pvalue) | -log10(Padj) | 是否可排序 |
|---|---|---|---|---|---|
| GENEA | 1e-5 | 0.002 | 5.0 | 2.7 | ✅ |
| GENEB | 1e-8 | NA | 8.0 | NA | ❌(需剔除) |
graph TD
A[输入Pvalue/Padj] --> B{Padj ∈ 0,1?}
B -->|是| C[-log10(Padj)]
B -->|否| D[置为NA/0并标记]
C --> E[排序前drop.na]
D --> E
4.3 “GO term截断失真”:长术语自动换行+中英文混排字体兼容性配置(showtext方案)
GO术语如"regulation of transcription involved in exit from mitosis"在窄图区常被粗暴截断,破坏语义完整性。
核心痛点
- R基础绘图引擎对长字符串缺乏智能换行支持
extrafont/showtext混用时中文字体fallback失效geom_text()默认hjust与width不协同导致偏移
showtext 配置范式
library(showtext)
showtext_auto() # 自动接管所有图形设备
# 关键:预注册多语言字体族
font_add("sans",
regular = "NotoSansCJKsc-Regular.otf",
bold = "NotoSansCJKsc-Bold.otf")
此配置启用OpenType字体的字形级渲染,
showtext_auto()强制所有grid绘图走systemfonts路径,规避base R的ASCII-only字体缓存机制。
智能换行函数
| 参数 | 说明 |
|---|---|
str_wrap() |
stringr提供基于字符数的硬截断 |
str_wrap(width = 20, simplify = FALSE) |
返回向量,每项为一行 |
# GO term安全换行(保留语义单元)
go_wrap <- function(x, width = 18) {
str_replace_all(x, "(\\s+)", " ") %>% # 压缩空格
str_wrap(width = width, simplify = FALSE)
}
str_wrap按空格切分而非字符计数,确保"mitosis"等完整词不被割裂;simplify = FALSE维持原始长度向量结构,适配geom_text(aes(label = go_wrap(term)))。
4.4 “颜色映射失焦”:连续色阶(viridis)vs 分类色阶(Set2)在富集方向表达中的语义适配
色阶语义错位的典型场景
当对 GO 富集结果按 log₂(Enrichment Ratio) 连续量化时,若误用分类色阶 Set2,将导致梯度信息坍缩为离散桶——视觉上看似“分组清晰”,实则掩盖上调/下调的强度差异。
可视化对比代码
import seaborn as sns
import matplotlib.pyplot as plt
# ✅ 连续映射:viridis 保序保距
sns.heatmap(enrich_df, cmap='viridis', center=0) # center=0 强化方向感知
# ❌ 分类映射:Set2 打乱数值序
sns.heatmap(enrich_df, cmap='Set2') # 无中心锚点,正负值混染
cmap='viridis' 是感知均匀的连续色阶,亮度单调递增,天然适配富集强度;center=0 将中性值映射至绿色,强化“上调→黄绿→亮黄,下调→蓝绿→深蓝”的方向语义。而 Set2 仅提供 8 种离散色,无亮度序,强制截断连续富集信号。
语义适配决策表
| 需求维度 | viridis | Set2 |
|---|---|---|
| 富集强度排序 | ✅ 感知线性 | ❌ 无序 |
| 上/下调方向判别 | ✅ 中心锚定 | ❌ 无极性 |
| 类别归属强调 | ❌ 不适用 | ✅ 适合通路分组 |
graph TD
A[富集分析输出] --> B{数值属性}
B -->|连续值<br>log₂ER| C[viridis + center=0]
B -->|离散标签<br>e.g. BP/CC/MF| D[Set2]
第五章:从原始数据到 publication-ready 图的终局交付
科研图表的终局交付绝非简单导出 PNG 就宣告完成——它是一套涵盖格式校验、元数据嵌入、跨平台一致性验证与期刊适配的工程化闭环。以下以 Nature Communications 接收的一篇单细胞空间转录组论文图 3(多模态整合热图+空间坐标叠加)为真实案例,还原其交付全流程。
数据溯源与可复现性保障
原始数据来自 10X Genomics Visium 平台(.tif + .h5 矩阵),经 spaceranger count 生成 filtered_feature_bc_matrix/。关键操作全部封装于 Snakemake pipeline 中,每张图的生成命令均绑定 commit hash(e.g., git checkout a7f3b9d && python plot_fig3.py --dpi 600),确保三年后仍可一键重建。
出版级格式强制规范
| 期刊明确要求: | 项目 | 要求 | 工具链验证 |
|---|---|---|---|
| 分辨率 | ≥600 dpi | identify -format "%x x %y" fig3.tiff |
|
| 颜色空间 | CMYK 或 sRGB | convert fig3.png -colorspace sRGB fig3_srgb.png |
|
| 字体嵌入 | TrueType 字体(非位图) | pdffonts fig3.pdf \| grep "Type 1\|TrueType" |
矢量图层分离策略
使用 Inkscape 手动拆分图层:
- 底层:
heatmap.svg(无文字,仅颜色矩阵) - 中层:
spatial_coords.svg(坐标轴、刻度线) - 顶层:
labels.svg(所有文本、箭头标注)
此结构允许编辑部单独调整字体大小而不破坏热图渲染精度。
# 自动化嵌入 ICC 配置文件(避免 Adobe Illustrator 渲染偏色)
from PIL import Image, ImageCms
srgb_profile = ImageCms.createProfile("sRGB")
img = Image.open("fig3.png")
ImageCms.profileToProfile(img, img.info.get("icc_profile"), srgb_profile)
img.save("fig3_srgb.png", icc_profile=srgb_profile.tobytes())
期刊模板硬约束应对
Nature Communications 模板禁止使用 Helvetica 字体(版权风险),强制替换为开源字体 TeX Gyre Heros。通过批量脚本修改 SVG 中所有 font-family="Helvetica" 为 font-family="TeX Gyre Heros, sans-serif",并预加载字体至 PDF 导出环境:
# Ghostscript 嵌入字体(规避 Acrobat 字体缺失警告)
gs -dNOPAUSE -dBATCH -sDEVICE=pdfwrite \
-dEmbedAllFonts=true \
-sOutputFile=fig3_final.pdf \
fig3_with_heros.pdf
跨平台渲染一致性验证
在三台设备上并行执行渲染比对:
- macOS Monterey + Safari 16.4(WebP 导出)
- Ubuntu 22.04 + Inkscape 1.2(PDF 导出)
- Windows 11 + Adobe Illustrator 2023(EPS 导出)
使用imagemagick compare计算像素级差异(阈值 ≤0.002%),自动标记异常区域。
flowchart LR
A[原始.h5矩阵] --> B[Seurat v5.0 标准化]
B --> C[ComplexHeatmap::pheatmap]
C --> D[Inkscape 图层分离]
D --> E[ICC 配置文件嵌入]
E --> F[字体合规性扫描]
F --> G{通过Nature模板检查?}
G -->|Yes| H[生成DOI关联元数据]
G -->|No| D
H --> I[上传Figshare存档]
I --> J[嵌入DOI至SVG <metadata> 标签]
元数据机器可读化
最终 SVG 文件头部注入结构化元数据:
<metadata>
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about="">
<dc:identifier>https://doi.org/10.1038/s41467-023-42155-8/fig3</dc:identifier>
<dc:source>10X Visium HD, Human Brain Cortex</dc:source>
<prism:publicationName>Nature Communications</prism:publicationName>
</rdf:Description>
</rdf:RDF>
</metadata>
审稿人快速验证通道
提供 verify_fig3.sh 脚本,审稿人仅需运行:
chmod +x verify_fig3.sh && ./verify_fig3.sh
# 输出:✓ DPI: 600 ✓ Font: TeX Gyre Heros ✓ ICC: sRGB ✓ DOI: Valid ✓ Layering: 3 