@basic-genomics/hivtrace-viz 1.5.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/embed/index.html +170 -17
- package/package.json +2 -2
package/dist/embed/index.html
CHANGED
|
@@ -1023,6 +1023,10 @@
|
|
|
1023
1023
|
style="margin-left: 8px;">
|
|
1024
1024
|
<i class="fa fa-play"></i> 播放动画
|
|
1025
1025
|
</button>
|
|
1026
|
+
<!-- 聚焦根按钮 -->
|
|
1027
|
+
<button type="button" class="btn btn-default btn-sm" id="tree_show_root_btn" title="聚焦到根节点">
|
|
1028
|
+
<i class="fa fa-dot-circle-o"></i> 聚焦根
|
|
1029
|
+
</button>
|
|
1026
1030
|
<!-- 统计信息 -->
|
|
1027
1031
|
<div style="flex: 1;"></div>
|
|
1028
1032
|
<div class="hivtrace-tree-stats" style="display: flex; gap: 12px; font-size: 13px; color: #6b7280;">
|
|
@@ -1166,35 +1170,79 @@
|
|
|
1166
1170
|
|
|
1167
1171
|
// 停止任何正在进行的动画,防止状态冲突
|
|
1168
1172
|
this.stopAnimation();
|
|
1173
|
+
this.cy.stop();
|
|
1169
1174
|
|
|
1170
1175
|
this.currentClusterId = clusterId;
|
|
1171
1176
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1177
|
+
// 将 matchingNodes 提升到外部作用域,供聚焦使用
|
|
1178
|
+
var matchingNodes = null;
|
|
1179
|
+
if (clusterId != null) {
|
|
1180
|
+
var strClusterId = String(clusterId);
|
|
1181
|
+
matchingNodes = this.cy.nodes().filter(function (n) {
|
|
1182
|
+
return n.data('cluster_id') != null && String(n.data('cluster_id')) === strClusterId;
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// 计算需要高亮的路径元素(在 batch 外部计算,供聚焦使用)
|
|
1187
|
+
var pathEles = self.cy.collection();
|
|
1188
|
+
if (clusterId != null && matchingNodes && matchingNodes.length > 1) {
|
|
1189
|
+
var mrca = self.findClusterRoot(clusterId);
|
|
1190
|
+
if (mrca) {
|
|
1191
|
+
var mrcaId = mrca.id();
|
|
1192
|
+
pathEles = pathEles.union(mrca);
|
|
1193
|
+
matchingNodes.forEach(function (node) {
|
|
1194
|
+
var preds = node.predecessors();
|
|
1195
|
+
for (var i = 0; i < preds.length; i++) {
|
|
1196
|
+
var ele = preds[i];
|
|
1197
|
+
pathEles = pathEles.union(ele);
|
|
1198
|
+
if (ele.isNode() && ele.id() === mrcaId) break;
|
|
1199
|
+
}
|
|
1178
1200
|
});
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1179
1203
|
|
|
1180
|
-
|
|
1204
|
+
// 批量更新样式(高亮/置灰)
|
|
1205
|
+
this.cy.batch(function () {
|
|
1206
|
+
if (clusterId != null && matchingNodes && matchingNodes.length > 0) {
|
|
1181
1207
|
self.cy.elements().addClass('grayed-out');
|
|
1182
1208
|
matchingNodes.removeClass('grayed-out');
|
|
1183
|
-
|
|
1184
|
-
if (matchingNodes.length > 1) {
|
|
1185
|
-
var dijk = self.cy.elements().dijkstra({ root: matchingNodes[0], directed: false });
|
|
1186
|
-
// 使用集合 union 快速合并路径元素
|
|
1187
|
-
var pathEles = matchingNodes.slice(1).reduce(function (acc, node) {
|
|
1188
|
-
return acc.union(dijk.pathTo(node));
|
|
1189
|
-
}, self.cy.collection());
|
|
1209
|
+
if (pathEles.length > 0) {
|
|
1190
1210
|
pathEles.removeClass('grayed-out');
|
|
1191
1211
|
}
|
|
1192
1212
|
} else {
|
|
1193
1213
|
self.cy.elements().removeClass('grayed-out');
|
|
1194
1214
|
}
|
|
1195
1215
|
});
|
|
1216
|
+
|
|
1217
|
+
// 自动聚焦到对应视图(包含节点和连接边)
|
|
1218
|
+
if (clusterId != null && matchingNodes && matchingNodes.length > 0) {
|
|
1219
|
+
// 合并簇成员节点和路径边,确保完整视图
|
|
1220
|
+
var fitEles = matchingNodes.union(pathEles);
|
|
1221
|
+
this.cy.animate({
|
|
1222
|
+
fit: { eles: fitEles, padding: 50 }
|
|
1223
|
+
}, {
|
|
1224
|
+
duration: 300,
|
|
1225
|
+
easing: 'ease-out',
|
|
1226
|
+
queue: false
|
|
1227
|
+
});
|
|
1228
|
+
} else if (clusterId == null) {
|
|
1229
|
+
// 恢复全局视图
|
|
1230
|
+
this.cy.animate({
|
|
1231
|
+
fit: { eles: this.cy.elements(), padding: 50 }
|
|
1232
|
+
}, {
|
|
1233
|
+
duration: 300,
|
|
1234
|
+
easing: 'ease-out',
|
|
1235
|
+
queue: false
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
TreeViz.prototype.fitView = function () {
|
|
1240
|
+
if (!this.cy) return;
|
|
1241
|
+
this.stopAnimation();
|
|
1242
|
+
this.cy.stop();
|
|
1243
|
+
this.cy.resize();
|
|
1244
|
+
this.cy.fit(undefined, 50);
|
|
1196
1245
|
};
|
|
1197
|
-
TreeViz.prototype.fitView = function () { if (this.cy) { this.cy.resize(); this.cy.fit(undefined, 50); } };
|
|
1198
1246
|
TreeViz.prototype.getAvailableClusters = function () {
|
|
1199
1247
|
if (!this.cy) return [];
|
|
1200
1248
|
var counts = new Map();
|
|
@@ -1230,6 +1278,26 @@
|
|
|
1230
1278
|
}
|
|
1231
1279
|
return null;
|
|
1232
1280
|
};
|
|
1281
|
+
// 聚焦到根节点(使用官方 animate API)
|
|
1282
|
+
TreeViz.prototype.focusOnRoot = function () {
|
|
1283
|
+
if (!this.cy) return null;
|
|
1284
|
+
var root = this.findRoot();
|
|
1285
|
+
if (!root) return null;
|
|
1286
|
+
// 停止渐显动画(setTimeout 定时器)
|
|
1287
|
+
this.stopAnimation();
|
|
1288
|
+
// 停止视图动画(Cytoscape animate)
|
|
1289
|
+
this.cy.stop();
|
|
1290
|
+
// 使用 Cytoscape.js 官方推荐的 animate + fit API
|
|
1291
|
+
// queue: false 确保立即执行,不排队
|
|
1292
|
+
this.cy.animate({
|
|
1293
|
+
fit: { eles: root, padding: 100 }
|
|
1294
|
+
}, {
|
|
1295
|
+
duration: 300,
|
|
1296
|
+
easing: 'ease-out',
|
|
1297
|
+
queue: false
|
|
1298
|
+
});
|
|
1299
|
+
return root.data();
|
|
1300
|
+
};
|
|
1233
1301
|
// 查找簇成员的最近共同祖先(MRCA)
|
|
1234
1302
|
// 使用官方 predecessors() + intersection() API
|
|
1235
1303
|
TreeViz.prototype.findClusterRoot = function (clusterId) {
|
|
@@ -1284,6 +1352,32 @@
|
|
|
1284
1352
|
: this.findRoot();
|
|
1285
1353
|
if (!animRoot) return;
|
|
1286
1354
|
|
|
1355
|
+
// 先停止现有视图动画,防止冲突
|
|
1356
|
+
this.cy.stop();
|
|
1357
|
+
|
|
1358
|
+
// 确保视图能看到完整动画区域
|
|
1359
|
+
if (this.currentClusterId != null) {
|
|
1360
|
+
// 簇视图:聚焦到簇的子树(MRCA 及其所有后代)
|
|
1361
|
+
// 这样能确保看到完整的动画范围
|
|
1362
|
+
var subtreeEles = animRoot.union(animRoot.successors());
|
|
1363
|
+
this.cy.animate({
|
|
1364
|
+
fit: { eles: subtreeEles, padding: 50 }
|
|
1365
|
+
}, {
|
|
1366
|
+
duration: 200,
|
|
1367
|
+
easing: 'ease-out',
|
|
1368
|
+
queue: false
|
|
1369
|
+
});
|
|
1370
|
+
} else {
|
|
1371
|
+
// 全局视图:适应整棵树,确保看到完整动画
|
|
1372
|
+
this.cy.animate({
|
|
1373
|
+
fit: { eles: this.cy.elements(), padding: 50 }
|
|
1374
|
+
}, {
|
|
1375
|
+
duration: 200,
|
|
1376
|
+
easing: 'ease-out',
|
|
1377
|
+
queue: false
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1287
1381
|
// 第一步:全部置灰(使用 batch 确保立即生效)
|
|
1288
1382
|
this.cy.batch(function () {
|
|
1289
1383
|
self.cy.elements().addClass('grayed-out');
|
|
@@ -1481,7 +1575,7 @@
|
|
|
1481
1575
|
}
|
|
1482
1576
|
|
|
1483
1577
|
// 禁用进化树工具栏按钮
|
|
1484
|
-
['tree_fit_btn', 'tree_labels_btn', 'tree_animate_btn', 'tree_export_png', 'tree_export_json',
|
|
1578
|
+
['tree_fit_btn', 'tree_labels_btn', 'tree_animate_btn', 'tree_show_root_btn', 'tree_export_png', 'tree_export_json',
|
|
1485
1579
|
'tree_cluster_prev_btn', 'tree_cluster_next_btn', 'tree_cluster_selector'].forEach(function (id) {
|
|
1486
1580
|
var btn = document.getElementById(id);
|
|
1487
1581
|
if (btn) {
|
|
@@ -1502,7 +1596,7 @@
|
|
|
1502
1596
|
}
|
|
1503
1597
|
}
|
|
1504
1598
|
// 恢复工具栏按钮
|
|
1505
|
-
['tree_fit_btn', 'tree_labels_btn', 'tree_animate_btn', 'tree_export_png', 'tree_export_json',
|
|
1599
|
+
['tree_fit_btn', 'tree_labels_btn', 'tree_animate_btn', 'tree_show_root_btn', 'tree_export_png', 'tree_export_json',
|
|
1506
1600
|
'tree_cluster_prev_btn', 'tree_cluster_next_btn', 'tree_cluster_selector'].forEach(function (id) {
|
|
1507
1601
|
var btn = document.getElementById(id);
|
|
1508
1602
|
if (btn) {
|
|
@@ -1513,6 +1607,48 @@
|
|
|
1513
1607
|
});
|
|
1514
1608
|
}
|
|
1515
1609
|
|
|
1610
|
+
// 隐藏进化树 Tab(当没有进化树数据时)
|
|
1611
|
+
function hideTreeTab() {
|
|
1612
|
+
var treeTab = document.getElementById('tree-tab');
|
|
1613
|
+
if (treeTab) {
|
|
1614
|
+
// 检查当前是否正在查看进化树 Tab
|
|
1615
|
+
var isTreeTabActive = treeTab.classList.contains('active');
|
|
1616
|
+
|
|
1617
|
+
treeTab.style.display = 'none';
|
|
1618
|
+
treeTab.classList.add('disabled');
|
|
1619
|
+
treeTab.classList.remove('active');
|
|
1620
|
+
|
|
1621
|
+
// 清除加载指示器
|
|
1622
|
+
var tabLink = treeTab.querySelector('a');
|
|
1623
|
+
if (tabLink) {
|
|
1624
|
+
tabLink.innerHTML = '进化树';
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// 如果当前正在查看进化树 Tab,切换到第一个 Tab(分子网络)
|
|
1628
|
+
if (isTreeTabActive) {
|
|
1629
|
+
var firstTab = document.querySelector('.nav-tabs > li:first-child a[data-toggle="tab"]');
|
|
1630
|
+
if (firstTab) {
|
|
1631
|
+
$(firstTab).tab('show');
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
// 清除树容器内容
|
|
1636
|
+
var treeContainer = document.getElementById('tree_container');
|
|
1637
|
+
if (treeContainer) {
|
|
1638
|
+
treeContainer.innerHTML = '';
|
|
1639
|
+
}
|
|
1640
|
+
// 隐藏树 Tab 内容面板
|
|
1641
|
+
var treePane = document.getElementById('tree');
|
|
1642
|
+
if (treePane) {
|
|
1643
|
+
treePane.classList.remove('active');
|
|
1644
|
+
}
|
|
1645
|
+
// 清除全局树数据引用
|
|
1646
|
+
if (window._treeData) {
|
|
1647
|
+
window._treeData = null;
|
|
1648
|
+
}
|
|
1649
|
+
tree_viz = null;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1516
1652
|
// 进化树初始化函数
|
|
1517
1653
|
|
|
1518
1654
|
function initTreeViz(treeData, options) {
|
|
@@ -1642,6 +1778,11 @@
|
|
|
1642
1778
|
document.getElementById('tree_labels_text').textContent = showing ? '隐藏标签' : '显示标签';
|
|
1643
1779
|
});
|
|
1644
1780
|
|
|
1781
|
+
// 聚焦根按钮
|
|
1782
|
+
document.getElementById('tree_show_root_btn').addEventListener('click', function () {
|
|
1783
|
+
tree_viz.focusOnRoot();
|
|
1784
|
+
});
|
|
1785
|
+
|
|
1645
1786
|
// 上一个簇
|
|
1646
1787
|
document.getElementById('tree_cluster_prev_btn').addEventListener('click', function () {
|
|
1647
1788
|
if (currentClusterIndex >= 0) {
|
|
@@ -2511,7 +2652,13 @@
|
|
|
2511
2652
|
} else if (data.options.treeData && data.options.treeData.nodes && data.options.treeData.nodes.length > 0) {
|
|
2512
2653
|
// 如果有进化树数据,初始化进化树
|
|
2513
2654
|
initTreeViz(data.options.treeData, data.options);
|
|
2655
|
+
} else {
|
|
2656
|
+
// 没有进化树数据,隐藏进化树 Tab
|
|
2657
|
+
hideTreeTab();
|
|
2514
2658
|
}
|
|
2659
|
+
} else {
|
|
2660
|
+
// 没有 options,隐藏进化树 Tab
|
|
2661
|
+
hideTreeTab();
|
|
2515
2662
|
}
|
|
2516
2663
|
} else if (data.type === 'HIVTRACE_UPDATE_DATA') {
|
|
2517
2664
|
updateNetworkData(data.graphData, data.options);
|
|
@@ -2523,7 +2670,13 @@
|
|
|
2523
2670
|
} else if (data.options.treeData && data.options.treeData.nodes && data.options.treeData.nodes.length > 0) {
|
|
2524
2671
|
// 更新进化树数据
|
|
2525
2672
|
initTreeViz(data.options.treeData, data.options);
|
|
2673
|
+
} else {
|
|
2674
|
+
// 没有进化树数据,隐藏进化树 Tab
|
|
2675
|
+
hideTreeTab();
|
|
2526
2676
|
}
|
|
2677
|
+
} else {
|
|
2678
|
+
// 没有 options,隐藏进化树 Tab
|
|
2679
|
+
hideTreeTab();
|
|
2527
2680
|
}
|
|
2528
2681
|
|
|
2529
2682
|
} else if (data.type === 'HIVTRACE_ERROR') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basic-genomics/hivtrace-viz",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "HIV-TRACE molecular transmission network visualization with React integration",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18"
|
|
@@ -104,4 +104,4 @@
|
|
|
104
104
|
"vite"
|
|
105
105
|
],
|
|
106
106
|
"packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447"
|
|
107
|
-
}
|
|
107
|
+
}
|