@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.
@@ -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
- this.cy.batch(function () {
1160
- if (clusterId != null) {
1161
- var strClusterId = String(clusterId);
1162
- // 一次性过滤匹配节点
1163
- var matchingNodes = self.cy.nodes().filter(function (n) {
1164
- return n.data('cluster_id') != null && String(n.data('cluster_id')) === strClusterId;
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 && data.options.treeData) {
2434
- initTreeViz(data.options.treeData, data.options);
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 && data.options.treeData) {
2440
- initTreeViz(data.options.treeData, data.options);
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 || '数据加载失败');