在 Longevity & Aging Cell Atlas 的 speciesinfo.php 页面中:
typeId 着色_extractCellTypes() — 提取 JSON Legend// 从页面 JSON 解析出所有 cell type this.cellTypes = []; // 按 JSON 中出现的顺序存入数组 jsonData.forEach(item => { this.cellTypes.push({ name: item.name, index: item.index, // JSON 中的 index 字段 (可能 ≠ 数组下标) count: item.count, color: item.color, category: item.category }); }); // 结果: cellTypes[0].index 可能是 0, 也可能是 5, 取决于 JSON 顺序
_detectBestOffset() — 自动检测偏移量
// 对比每个 cell type 的 count 来猜测 offset
for (let offset = -20; offset <= 20; offset++) {
let score = 0;
this.cellTypes.forEach(ct => {
let mappedId = ct.index + offset;
let binCount = binaryCounts[mappedId]; // binary 中该 ID 的细胞数
if (差异 < 10%) score += 10;
else score += 1;
});
}
// 结果: offset=0, score=1140 (看似完美)
// 但这只证明 ct.index 与 binary typeId 的 COUNT 匹配
// 并不能证明 palette 颜色分配是一致的!
_applyMapping() — 建立映射
this._idxToName = {};
this.cellTypes.forEach((ct, arrayIndex) => {
ct.mappedId = ct.index + 0; // offset=0
this._idxToName[ct.mappedId] = ct.name;
});
// _idxToName[4] → "Excitatory neurons" ✓ (name 映射是对的)
// 但 arrayIndex 和 mappedId 的关系被忽略了!
this.cellTypes.forEach((ct, arrayIndex) => { // 色块颜色 = palette[arrayIndex] ctx.fillStyle = this.palette[arrayIndex]; // 用数组下标取色! ctx.fillRect(...); // 文字 ctx.fillText(ct.name, ...); }); // 所以 Legend 中: // arrayIndex=0 → "B cells" → palette[0] 🔴 // arrayIndex=1 → "T cells" → palette[1] 🔵 // arrayIndex=2 → "Excitatory neurons" → palette[2] 🟢 // arrayIndex=4 → "Pericytes" → palette[4] 🟠
cells.forEach(cell => {
let typeId = cell.typeId; // 从 binary 读取, 例如 4
ctx.fillStyle = this.palette[typeId]; // 用 typeId 取色!
ctx.fillRect(cell.x, cell.y, ...);
});
// 所以 UMAP 中:
// typeId=4 的细胞 → palette[4] 🟠
// 但 typeId=4 实际是 "Excitatory neurons"
// Legend 里 "Excitatory neurons" 却是 palette[2] 🟢
onHover(cell) {
let name = this._idxToName[cell.typeId]; // 这个是对的: typeId=4 → "Excitatory neurons"
tooltip.show(name);
}
// 但用户看到的颜色是 palette[typeId]=🟠
// 对应 Legend 中 arrayIndex=4 → "Pericytes" 🟠
// 所以用户 觉得 hover 的是 Pericytes 的颜色区域
★ 根因: arrayIndex ≠ mappedId(typeId), 但两处都用作 palette 索引
cellTypes 数组的排列顺序(由 JSON 提取顺序决定)与 binary 中的 typeId 编号不是同一套序号。
Legend 用 arrayIndex 索引 palette → 得到颜色 A
UMAP 用 typeId 索引 palette → 得到颜色 B
同一个 cell type,Legend 和 UMAP 显示不同颜色 → 视觉上就是"偏移"。
| 层面 | Legend | UMAP | 是否一致 |
|---|---|---|---|
| 颜色来源 | palette[arrayIndex] | palette[typeId] | ❌ 不一致 |
| 名称来源 | cellTypes[arrayIndex].name | _idxToName[typeId] | ✅ 各自正确 |
| 点击选中 | 找 mappedId == typeId | 高亮 typeId 对应的细胞 | ✅ ID 正确 |
| 视觉效果 | 色块颜色 A | 细胞颜色 B | ❌ 用户看到不匹配 |
核心思路:统一使用 mappedId 作为 palette 索引,确保 Legend 和 UMAP 取到相同颜色。
让 Legend 色块使用 palette[ct.mappedId] 而非 palette[arrayIndex]
// ============================================
// 修复: Legend 渲染部分
// 搜索 Legend 渲染的 forEach 循环
// ============================================
// ❌ 修改前 (Bug):
this.cellTypes.forEach((ct, i) => {
ctx.fillStyle = this.palette[i]; // 用 arrayIndex
// ... 绘制色块和文字
});
// ✅ 修改后 (Fix):
this.cellTypes.forEach((ct, i) => {
ctx.fillStyle = this.palette[ct.mappedId]; // 用 mappedId, 与 UMAP 一致
// ... 绘制色块和文字
});
让 UMAP 细胞着色使用 palette[arrayIndex],需要建立 typeId → arrayIndex 反查表
// ============================================
// 修复: 在 _applyMapping() 中新增反查表
// ============================================
// ✅ 新增:
this._typeIdToArrayIndex = {};
this.cellTypes.forEach((ct, arrayIndex) => {
ct.mappedId = ct.index + this._offset;
this._idxToName[ct.mappedId] = ct.name;
this._typeIdToArrayIndex[ct.mappedId] = arrayIndex; // 新增反查
});
// ============================================
// 修复: UMAP 渲染部分
// ============================================
// ❌ 修改前 (Bug):
ctx.fillStyle = this.palette[cell.typeId];
// ✅ 修改后 (Fix):
let aidx = this._typeIdToArrayIndex[cell.typeId];
ctx.fillStyle = this.palette[aidx !== undefined ? aidx : cell.typeId];
在 _applyMapping() 后,按 mappedId 重排数组,使 arrayIndex === mappedId
// ============================================
// 修复: 在 _applyMapping() 末尾添加
// ============================================
// ✅ 按 mappedId 重排,使 arrayIndex === mappedId
let reordered = new Array(this.cellTypes.length);
this.cellTypes.forEach(ct => {
if (ct.mappedId >= 0 && ct.mappedId < reordered.length) {
reordered[ct.mappedId] = ct;
}
});
// 填补空位(mappedId 超出范围或有空洞时)
let spare = this.cellTypes.filter(ct =>
ct.mappedId < 0 || ct.mappedId >= reordered.length || reordered[ct.mappedId] !== ct
);
let si = 0;
for (let i = 0; i < reordered.length; i++) {
if (!reordered[i] && si < spare.length) {
reordered[i] = spare[si++];
}
}
this.cellTypes = reordered.filter(Boolean);
// 这样之后 palette[arrayIndex] === palette[mappedId],全部代码都不需要改
在 speciesinfo.php 中搜索以下关键词来定位具体代码行:
| 修改目标 | 搜索关键词 | 修改内容 |
|---|---|---|
| Legend 色块取色 | palette[i] 或 palette[idx]在 legend / celltype 渲染附近 |
改为 palette[ct.mappedId] |
| UMAP 细胞取色 | palette[typeId] 或 palette[cell.type]在 canvas drawRect / fillRect 附近 |
改为 palette[_typeIdToArrayIndex[typeId]] |
| 点击 Legend 选中 | selectedType 或 clickedType在 click handler 中 |
确保使用 ct.mappedId 而非 arrayIndex |
| Tooltip 显示 | _idxToName 或 tooltip |
此处通常是正确的,不需要改 |
palette[,
检查每个出现位置的索引是用的 arrayIndex/i 还是 typeId/mappedId,
确保所有位置统一使用同一种索引即可。
如果不想修改原始代码,可以在 speciesinfo.php 的 </body> 前插入以下 <script> 标签:
<script>
// =============================================
// Cell Type Legend ↔ UMAP 颜色修复补丁
// 放在页面底部, 在 GeneViewer 初始化之后执行
// =============================================
(function patchCellTypeMapping() {
// 等待 viewer 初始化完成
var checkInterval = setInterval(function() {
// 尝试找到 viewer 实例 (根据实际变量名调整)
var viewer = window.geneViewer || window.viewer || window.gv;
if (!viewer || !viewer.cellTypes || viewer.cellTypes.length === 0) return;
clearInterval(checkInterval);
console.log('[Patch] 开始修复 cellType 颜色映射...');
// 方法: 按 mappedId 重新排列 cellTypes 数组
// 使得 arrayIndex === mappedId, 从而 palette[arrayIndex] === palette[typeId]
var maxId = 0;
viewer.cellTypes.forEach(function(ct) {
var mid = (ct.mappedId !== undefined) ? ct.mappedId : ct.index;
if (mid > maxId) maxId = mid;
});
var reordered = new Array(maxId + 1);
var unplaced = [];
viewer.cellTypes.forEach(function(ct) {
var mid = (ct.mappedId !== undefined) ? ct.mappedId : ct.index;
if (mid >= 0 && mid <= maxId && !reordered[mid]) {
reordered[mid] = ct;
} else {
unplaced.push(ct);
}
});
// 填补空位
var ui = 0;
for (var i = 0; i <= maxId; i++) {
if (!reordered[i] && ui < unplaced.length) {
reordered[i] = unplaced[ui++];
}
}
viewer.cellTypes = reordered.filter(Boolean);
// 验证
var fixed = 0, total = viewer.cellTypes.length;
viewer.cellTypes.forEach(function(ct, i) {
var mid = (ct.mappedId !== undefined) ? ct.mappedId : ct.index;
if (i === mid) fixed++;
});
console.log('[Patch] 修复完成: ' + fixed + '/' + total + ' 个 cellType 的 arrayIndex === mappedId');
// 触发重绘 (根据实际方法名调整)
if (viewer.renderLegend) viewer.renderLegend();
if (viewer.render) viewer.render();
if (viewer.draw) viewer.draw();
if (viewer.refresh) viewer.refresh();
}, 500);
})();
</script>
window.geneViewer 需要替换为你实际的 viewer 实例变量名。
在 Console 中输入 Object.keys(window).filter(k => window[k] && window[k].cellTypes) 可以找到它。
| 项目 | 内容 |
|---|---|
| 根因 | Legend 使用 palette[arrayIndex] 取色,UMAP 使用 palette[typeId] 取色。由于 cellTypes 数组的排列顺序 ≠ typeId 编号顺序,同一个 cell type 在两处显示不同颜色。 |
| 为什么 offset=0 没用 | offset 修正的是 ct.index → typeId 的 数值映射(_idxToName 是对的),但没有修正 palette 颜色索引 不一致的问题。 |
| 最简修复 | 方案 A: Legend 渲染改用 palette[ct.mappedId]或方案 C: 按 mappedId 重排 cellTypes 数组 |
| 改动量 | 方案 A: 改 1 行 | 方案 B: 改 2 处 + 加 3 行 | 方案 C: 加 15 行 |