@basic-genomics/hivtrace-viz 1.4.1 → 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/README.md +45 -32
- package/dist/embed/hivtrace.js +1 -1
- package/dist/embed/hivtrace.js.map +1 -1
- package/dist/embed/index.html +261 -21
- package/dist/hivtrace.js +1 -1
- package/dist/hivtrace.js.map +1 -1
- package/dist/react/HivtraceViz.d.ts +1 -1
- package/dist/react/HivtraceViz.d.ts.map +1 -1
- package/dist/react/HivtraceViz.js +4 -2
- package/dist/react/types.d.ts +12 -0
- package/dist/react/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/embed/index.html
CHANGED
|
@@ -121,6 +121,19 @@
|
|
|
121
121
|
background: #e8e8e8;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/* 小尺寸内联 spinner - 用于 Tab 标签等 */
|
|
125
|
+
.hivtrace-loading-spinner-sm {
|
|
126
|
+
display: inline-block;
|
|
127
|
+
width: 14px;
|
|
128
|
+
height: 14px;
|
|
129
|
+
border: 2px solid #ccc;
|
|
130
|
+
border-top-color: #666;
|
|
131
|
+
border-radius: 50%;
|
|
132
|
+
animation: hivtrace-spin 0.8s linear infinite;
|
|
133
|
+
vertical-align: middle;
|
|
134
|
+
margin-left: 4px;
|
|
135
|
+
}
|
|
136
|
+
|
|
124
137
|
/* 状态栏样式 - 简洁中性 */
|
|
125
138
|
.hivtrace-status-bar {
|
|
126
139
|
width: 100%;
|
|
@@ -1010,6 +1023,10 @@
|
|
|
1010
1023
|
style="margin-left: 8px;">
|
|
1011
1024
|
<i class="fa fa-play"></i> 播放动画
|
|
1012
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>
|
|
1013
1030
|
<!-- 统计信息 -->
|
|
1014
1031
|
<div style="flex: 1;"></div>
|
|
1015
1032
|
<div class="hivtrace-tree-stats" style="display: flex; gap: 12px; font-size: 13px; color: #6b7280;">
|
|
@@ -1153,35 +1170,79 @@
|
|
|
1153
1170
|
|
|
1154
1171
|
// 停止任何正在进行的动画,防止状态冲突
|
|
1155
1172
|
this.stopAnimation();
|
|
1173
|
+
this.cy.stop();
|
|
1156
1174
|
|
|
1157
1175
|
this.currentClusterId = clusterId;
|
|
1158
1176
|
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
+
}
|
|
1165
1200
|
});
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1166
1203
|
|
|
1167
|
-
|
|
1204
|
+
// 批量更新样式(高亮/置灰)
|
|
1205
|
+
this.cy.batch(function () {
|
|
1206
|
+
if (clusterId != null && matchingNodes && matchingNodes.length > 0) {
|
|
1168
1207
|
self.cy.elements().addClass('grayed-out');
|
|
1169
1208
|
matchingNodes.removeClass('grayed-out');
|
|
1170
|
-
|
|
1171
|
-
if (matchingNodes.length > 1) {
|
|
1172
|
-
var dijk = self.cy.elements().dijkstra({ root: matchingNodes[0], directed: false });
|
|
1173
|
-
// 使用集合 union 快速合并路径元素
|
|
1174
|
-
var pathEles = matchingNodes.slice(1).reduce(function (acc, node) {
|
|
1175
|
-
return acc.union(dijk.pathTo(node));
|
|
1176
|
-
}, self.cy.collection());
|
|
1209
|
+
if (pathEles.length > 0) {
|
|
1177
1210
|
pathEles.removeClass('grayed-out');
|
|
1178
1211
|
}
|
|
1179
1212
|
} else {
|
|
1180
1213
|
self.cy.elements().removeClass('grayed-out');
|
|
1181
1214
|
}
|
|
1182
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);
|
|
1183
1245
|
};
|
|
1184
|
-
TreeViz.prototype.fitView = function () { if (this.cy) { this.cy.resize(); this.cy.fit(undefined, 50); } };
|
|
1185
1246
|
TreeViz.prototype.getAvailableClusters = function () {
|
|
1186
1247
|
if (!this.cy) return [];
|
|
1187
1248
|
var counts = new Map();
|
|
@@ -1217,6 +1278,26 @@
|
|
|
1217
1278
|
}
|
|
1218
1279
|
return null;
|
|
1219
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
|
+
};
|
|
1220
1301
|
// 查找簇成员的最近共同祖先(MRCA)
|
|
1221
1302
|
// 使用官方 predecessors() + intersection() API
|
|
1222
1303
|
TreeViz.prototype.findClusterRoot = function (clusterId) {
|
|
@@ -1271,6 +1352,32 @@
|
|
|
1271
1352
|
: this.findRoot();
|
|
1272
1353
|
if (!animRoot) return;
|
|
1273
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
|
+
|
|
1274
1381
|
// 第一步:全部置灰(使用 batch 确保立即生效)
|
|
1275
1382
|
this.cy.batch(function () {
|
|
1276
1383
|
self.cy.elements().addClass('grayed-out');
|
|
@@ -1444,12 +1551,114 @@
|
|
|
1444
1551
|
countryCenersObject = null,
|
|
1445
1552
|
countryOutlines = null;
|
|
1446
1553
|
|
|
1554
|
+
// 显示进化树 Tab 的加载状态(用于分析中场景)
|
|
1555
|
+
function showTreeTabLoading(options) {
|
|
1556
|
+
var treeTab = document.getElementById('tree-tab');
|
|
1557
|
+
if (treeTab) {
|
|
1558
|
+
treeTab.style.display = '';
|
|
1559
|
+
treeTab.classList.remove('disabled');
|
|
1560
|
+
// 添加加载指示器到 Tab 标签(使用统一的 CSS spinner)
|
|
1561
|
+
var tabLink = treeTab.querySelector('a');
|
|
1562
|
+
if (tabLink && !tabLink.querySelector('.tree-loading-indicator')) {
|
|
1563
|
+
tabLink.innerHTML = '进化树 <span class="tree-loading-indicator hivtrace-loading-spinner-sm"></span>';
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// 在 Tab 内容区显示加载占位符(使用与主加载态统一的样式)
|
|
1568
|
+
var treeContainer = document.getElementById('tree_container');
|
|
1569
|
+
if (treeContainer) {
|
|
1570
|
+
treeContainer.innerHTML = '<div class="tree-loading-placeholder" style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;min-height:400px;">' +
|
|
1571
|
+
'<div class="hivtrace-loading-spinner"></div>' +
|
|
1572
|
+
'<div class="hivtrace-loading-text">进化树正在分析中...</div>' +
|
|
1573
|
+
'<div style="font-size:13px;margin-top:8px;color:#999;">请稍候,分析完成后将自动显示</div>' +
|
|
1574
|
+
'</div>';
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// 禁用进化树工具栏按钮
|
|
1578
|
+
['tree_fit_btn', 'tree_labels_btn', 'tree_animate_btn', 'tree_show_root_btn', 'tree_export_png', 'tree_export_json',
|
|
1579
|
+
'tree_cluster_prev_btn', 'tree_cluster_next_btn', 'tree_cluster_selector'].forEach(function (id) {
|
|
1580
|
+
var btn = document.getElementById(id);
|
|
1581
|
+
if (btn) {
|
|
1582
|
+
btn.disabled = true;
|
|
1583
|
+
btn.style.opacity = '0.5';
|
|
1584
|
+
btn.style.pointerEvents = 'none';
|
|
1585
|
+
}
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// 清除进化树 Tab 的加载指示器
|
|
1590
|
+
function clearTreeTabLoadingIndicator() {
|
|
1591
|
+
var treeTab = document.getElementById('tree-tab');
|
|
1592
|
+
if (treeTab) {
|
|
1593
|
+
var tabLink = treeTab.querySelector('a');
|
|
1594
|
+
if (tabLink) {
|
|
1595
|
+
tabLink.innerHTML = '进化树';
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
// 恢复工具栏按钮
|
|
1599
|
+
['tree_fit_btn', 'tree_labels_btn', 'tree_animate_btn', 'tree_show_root_btn', 'tree_export_png', 'tree_export_json',
|
|
1600
|
+
'tree_cluster_prev_btn', 'tree_cluster_next_btn', 'tree_cluster_selector'].forEach(function (id) {
|
|
1601
|
+
var btn = document.getElementById(id);
|
|
1602
|
+
if (btn) {
|
|
1603
|
+
btn.disabled = false;
|
|
1604
|
+
btn.style.opacity = '';
|
|
1605
|
+
btn.style.pointerEvents = '';
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
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
|
+
|
|
1447
1652
|
// 进化树初始化函数
|
|
1653
|
+
|
|
1448
1654
|
function initTreeViz(treeData, options) {
|
|
1449
1655
|
if (!treeData || !treeData.nodes || treeData.nodes.length === 0) {
|
|
1450
1656
|
return;
|
|
1451
1657
|
}
|
|
1452
1658
|
|
|
1659
|
+
// 清除加载指示器(如果之前显示过)
|
|
1660
|
+
clearTreeTabLoadingIndicator();
|
|
1661
|
+
|
|
1453
1662
|
// 显示进化树 Tab
|
|
1454
1663
|
var treeTab = document.getElementById('tree-tab');
|
|
1455
1664
|
if (treeTab) {
|
|
@@ -1457,6 +1666,7 @@
|
|
|
1457
1666
|
treeTab.classList.remove('disabled');
|
|
1458
1667
|
}
|
|
1459
1668
|
|
|
1669
|
+
|
|
1460
1670
|
// 保存树数据
|
|
1461
1671
|
window._treeData = treeData;
|
|
1462
1672
|
|
|
@@ -1568,6 +1778,11 @@
|
|
|
1568
1778
|
document.getElementById('tree_labels_text').textContent = showing ? '隐藏标签' : '显示标签';
|
|
1569
1779
|
});
|
|
1570
1780
|
|
|
1781
|
+
// 聚焦根按钮
|
|
1782
|
+
document.getElementById('tree_show_root_btn').addEventListener('click', function () {
|
|
1783
|
+
tree_viz.focusOnRoot();
|
|
1784
|
+
});
|
|
1785
|
+
|
|
1571
1786
|
// 上一个簇
|
|
1572
1787
|
document.getElementById('tree_cluster_prev_btn').addEventListener('click', function () {
|
|
1573
1788
|
if (currentClusterIndex >= 0) {
|
|
@@ -2429,16 +2644,41 @@
|
|
|
2429
2644
|
|
|
2430
2645
|
if (data.type === 'HIVTRACE_DATA') {
|
|
2431
2646
|
init(data.graphData, data.options);
|
|
2432
|
-
//
|
|
2433
|
-
if (data.options
|
|
2434
|
-
|
|
2647
|
+
// 处理进化树显示逻辑
|
|
2648
|
+
if (data.options) {
|
|
2649
|
+
if (data.options.showTreeTab && data.options.treeLoading) {
|
|
2650
|
+
// 显示进化树 Tab 的加载状态
|
|
2651
|
+
showTreeTabLoading(data.options);
|
|
2652
|
+
} else if (data.options.treeData && data.options.treeData.nodes && data.options.treeData.nodes.length > 0) {
|
|
2653
|
+
// 如果有进化树数据,初始化进化树
|
|
2654
|
+
initTreeViz(data.options.treeData, data.options);
|
|
2655
|
+
} else {
|
|
2656
|
+
// 没有进化树数据,隐藏进化树 Tab
|
|
2657
|
+
hideTreeTab();
|
|
2658
|
+
}
|
|
2659
|
+
} else {
|
|
2660
|
+
// 没有 options,隐藏进化树 Tab
|
|
2661
|
+
hideTreeTab();
|
|
2435
2662
|
}
|
|
2436
2663
|
} else if (data.type === 'HIVTRACE_UPDATE_DATA') {
|
|
2437
2664
|
updateNetworkData(data.graphData, data.options);
|
|
2438
|
-
//
|
|
2439
|
-
if (data.options
|
|
2440
|
-
|
|
2665
|
+
// 更新进化树显示逻辑
|
|
2666
|
+
if (data.options) {
|
|
2667
|
+
if (data.options.showTreeTab && data.options.treeLoading) {
|
|
2668
|
+
// 显示进化树 Tab 的加载状态
|
|
2669
|
+
showTreeTabLoading(data.options);
|
|
2670
|
+
} else if (data.options.treeData && data.options.treeData.nodes && data.options.treeData.nodes.length > 0) {
|
|
2671
|
+
// 更新进化树数据
|
|
2672
|
+
initTreeViz(data.options.treeData, data.options);
|
|
2673
|
+
} else {
|
|
2674
|
+
// 没有进化树数据,隐藏进化树 Tab
|
|
2675
|
+
hideTreeTab();
|
|
2676
|
+
}
|
|
2677
|
+
} else {
|
|
2678
|
+
// 没有 options,隐藏进化树 Tab
|
|
2679
|
+
hideTreeTab();
|
|
2441
2680
|
}
|
|
2681
|
+
|
|
2442
2682
|
} else if (data.type === 'HIVTRACE_ERROR') {
|
|
2443
2683
|
// 处理来自宿主应用的错误(如 API 请求失败)
|
|
2444
2684
|
showLoadingError(data.message || '数据加载失败');
|