@basic-genomics/hivtrace-viz 1.5.0 → 1.5.2

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.
Files changed (2) hide show
  1. package/dist/embed/index.html +279 -60
  2. package/package.json +2 -2
@@ -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
- this.cy.batch(function () {
1173
- if (clusterId != null) {
1174
- var strClusterId = String(clusterId);
1175
- // 一次性过滤匹配节点
1176
- var matchingNodes = self.cy.nodes().filter(function (n) {
1177
- 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
+ }
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,52 @@
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
+ // 清除所有树相关的全局状态
1650
+ window._treeSelectTreeCluster = null;
1651
+ window._treeCurrentClusterIndex = undefined;
1652
+ window._treeExportId = null;
1653
+ tree_viz = null;
1654
+ }
1655
+
1516
1656
  // 进化树初始化函数
1517
1657
 
1518
1658
  function initTreeViz(treeData, options) {
@@ -1606,6 +1746,7 @@
1606
1746
  // 选择簇
1607
1747
  function selectTreeCluster(index) {
1608
1748
  currentClusterIndex = index;
1749
+ window._treeCurrentClusterIndex = currentClusterIndex; // 暴露到全局作用域
1609
1750
  var label = document.getElementById('tree_cluster_selector_label');
1610
1751
 
1611
1752
  if (index < 0) {
@@ -1619,6 +1760,11 @@
1619
1760
  updateTreeNavButtons();
1620
1761
  }
1621
1762
 
1763
+ // 暴露函数到全局作用域供事件处理器使用
1764
+ window._treeSelectTreeCluster = selectTreeCluster;
1765
+ window._treeCurrentClusterIndex = currentClusterIndex;
1766
+ window._treeExportId = options && options.exportId ? options.exportId : null;
1767
+
1622
1768
  // 默认高亮簇
1623
1769
  if (options && options.treeDefaultClusterId != null) {
1624
1770
  var defaultIndex = clusters.findIndex(function (c) {
@@ -1630,66 +1776,124 @@
1630
1776
  }
1631
1777
 
1632
1778
  updateTreeNavButtons();
1779
+ }
1633
1780
 
1781
+ // 进化树工具栏事件初始化(在页面加载时调用一次,使用 null 检查确保安全)
1782
+ function initTreeToolbarEvents() {
1634
1783
  // 适应窗口按钮
1635
- document.getElementById('tree_fit_btn').addEventListener('click', function () {
1636
- tree_viz.fitView();
1637
- });
1784
+ var treeFitBtn = document.getElementById('tree_fit_btn');
1785
+ if (treeFitBtn) {
1786
+ treeFitBtn.addEventListener('click', function () {
1787
+ if (tree_viz && tree_viz.cy) {
1788
+ tree_viz.fitView();
1789
+ }
1790
+ });
1791
+ }
1638
1792
 
1639
1793
  // 标签按钮
1640
- document.getElementById('tree_labels_btn').addEventListener('click', function () {
1641
- var showing = tree_viz.toggleLabels();
1642
- document.getElementById('tree_labels_text').textContent = showing ? '隐藏标签' : '显示标签';
1643
- });
1794
+ var treeLabelsBtn = document.getElementById('tree_labels_btn');
1795
+ if (treeLabelsBtn) {
1796
+ treeLabelsBtn.addEventListener('click', function () {
1797
+ if (tree_viz && tree_viz.cy) {
1798
+ var showing = tree_viz.toggleLabels();
1799
+ var labelsText = document.getElementById('tree_labels_text');
1800
+ if (labelsText) {
1801
+ labelsText.textContent = showing ? '隐藏标签' : '显示标签';
1802
+ }
1803
+ }
1804
+ });
1805
+ }
1806
+
1807
+ // 聚焦根按钮
1808
+ var treeShowRootBtn = document.getElementById('tree_show_root_btn');
1809
+ if (treeShowRootBtn) {
1810
+ treeShowRootBtn.addEventListener('click', function () {
1811
+ if (tree_viz && tree_viz.cy) {
1812
+ tree_viz.focusOnRoot();
1813
+ }
1814
+ });
1815
+ }
1644
1816
 
1645
1817
  // 上一个簇
1646
- document.getElementById('tree_cluster_prev_btn').addEventListener('click', function () {
1647
- if (currentClusterIndex >= 0) {
1648
- selectTreeCluster(currentClusterIndex - 1);
1649
- }
1650
- });
1818
+ var treePrevBtn = document.getElementById('tree_cluster_prev_btn');
1819
+ if (treePrevBtn) {
1820
+ treePrevBtn.addEventListener('click', function () {
1821
+ if (tree_viz && window._treeCurrentClusterIndex >= 0 && window._treeSelectTreeCluster) {
1822
+ window._treeSelectTreeCluster(window._treeCurrentClusterIndex - 1);
1823
+ }
1824
+ });
1825
+ }
1651
1826
 
1652
1827
  // 下一个簇
1653
- document.getElementById('tree_cluster_next_btn').addEventListener('click', function () {
1654
- if (currentClusterIndex < clusters.length - 1) {
1655
- selectTreeCluster(currentClusterIndex + 1);
1656
- }
1657
- });
1828
+ var treeNextBtn = document.getElementById('tree_cluster_next_btn');
1829
+ if (treeNextBtn) {
1830
+ treeNextBtn.addEventListener('click', function () {
1831
+ if (tree_viz && window._treeSelectTreeCluster) {
1832
+ var clusters = tree_viz.getAvailableClusters();
1833
+ if (typeof window._treeCurrentClusterIndex !== 'undefined' &&
1834
+ window._treeCurrentClusterIndex < clusters.length - 1) {
1835
+ window._treeSelectTreeCluster(window._treeCurrentClusterIndex + 1);
1836
+ }
1837
+ }
1838
+ });
1839
+ }
1658
1840
 
1659
1841
  // 簇选择器下拉菜单点击
1660
- document.getElementById('tree_cluster_selector_menu').addEventListener('click', function (e) {
1661
- e.preventDefault();
1662
- var link = e.target.closest('a[data-value]');
1663
- if (!link) return;
1664
-
1665
- var value = link.getAttribute('data-value');
1666
- if (value === '') {
1667
- selectTreeCluster(-1);
1668
- } else {
1669
- var index = parseInt(link.getAttribute('data-index'), 10);
1670
- if (!isNaN(index)) {
1671
- selectTreeCluster(index);
1842
+ var treeClusterMenu = document.getElementById('tree_cluster_selector_menu');
1843
+ if (treeClusterMenu) {
1844
+ treeClusterMenu.addEventListener('click', function (e) {
1845
+ e.preventDefault();
1846
+ var link = e.target.closest('a[data-value]');
1847
+ if (!link) return;
1848
+
1849
+ if (!window._treeSelectTreeCluster) return;
1850
+
1851
+ var value = link.getAttribute('data-value');
1852
+ if (value === '') {
1853
+ window._treeSelectTreeCluster(-1);
1854
+ } else {
1855
+ var index = parseInt(link.getAttribute('data-index'), 10);
1856
+ if (!isNaN(index)) {
1857
+ window._treeSelectTreeCluster(index);
1858
+ }
1672
1859
  }
1673
- }
1674
- });
1860
+ });
1861
+ }
1675
1862
 
1676
1863
  // 导出 PNG
1677
- document.getElementById('tree_export_png').addEventListener('click', function (e) {
1678
- e.preventDefault();
1679
- tree_viz.exportPNG(options && options.exportId ? options.exportId + '_tree' : 'tree');
1680
- });
1864
+ var treeExportPng = document.getElementById('tree_export_png');
1865
+ if (treeExportPng) {
1866
+ treeExportPng.addEventListener('click', function (e) {
1867
+ e.preventDefault();
1868
+ if (tree_viz && tree_viz.cy) {
1869
+ var exportId = window._treeExportId || 'tree';
1870
+ tree_viz.exportPNG(exportId + '_tree');
1871
+ }
1872
+ });
1873
+ }
1681
1874
 
1682
1875
  // 导出 JSON
1683
- document.getElementById('tree_export_json').addEventListener('click', function (e) {
1684
- e.preventDefault();
1685
- tree_viz.exportJSON(treeData, options && options.exportId ? options.exportId + '_tree' : 'tree');
1686
- });
1876
+ var treeExportJson = document.getElementById('tree_export_json');
1877
+ if (treeExportJson) {
1878
+ treeExportJson.addEventListener('click', function (e) {
1879
+ e.preventDefault();
1880
+ if (tree_viz && window._treeData) {
1881
+ var exportId = window._treeExportId || 'tree';
1882
+ tree_viz.exportJSON(window._treeData, exportId + '_tree');
1883
+ }
1884
+ });
1885
+ }
1687
1886
 
1688
1887
  // 播放动画按钮
1689
- document.getElementById('tree_animate_btn').addEventListener('click', function (e) {
1690
- e.preventDefault();
1691
- tree_viz.animateFromRoot({ delay: 100 });
1692
- });
1888
+ var treeAnimateBtn = document.getElementById('tree_animate_btn');
1889
+ if (treeAnimateBtn) {
1890
+ treeAnimateBtn.addEventListener('click', function (e) {
1891
+ e.preventDefault();
1892
+ if (tree_viz && tree_viz.cy) {
1893
+ tree_viz.animateFromRoot({ delay: 100 });
1894
+ }
1895
+ });
1896
+ }
1693
1897
  }
1694
1898
 
1695
1899
  function HandleAppError(err_string) {
@@ -2493,6 +2697,9 @@
2493
2697
  // 初始化全屏功能
2494
2698
  FullscreenManager.init();
2495
2699
 
2700
+ // 初始化进化树工具栏事件(只注册一次,使用 null 检查确保安全)
2701
+ initTreeToolbarEvents();
2702
+
2496
2703
  // 通知父窗口准备就绪
2497
2704
  window.parent.postMessage({ type: 'HIVTRACE_INIT_READY' }, '*');
2498
2705
 
@@ -2511,7 +2718,13 @@
2511
2718
  } else if (data.options.treeData && data.options.treeData.nodes && data.options.treeData.nodes.length > 0) {
2512
2719
  // 如果有进化树数据,初始化进化树
2513
2720
  initTreeViz(data.options.treeData, data.options);
2721
+ } else {
2722
+ // 没有进化树数据,隐藏进化树 Tab
2723
+ hideTreeTab();
2514
2724
  }
2725
+ } else {
2726
+ // 没有 options,隐藏进化树 Tab
2727
+ hideTreeTab();
2515
2728
  }
2516
2729
  } else if (data.type === 'HIVTRACE_UPDATE_DATA') {
2517
2730
  updateNetworkData(data.graphData, data.options);
@@ -2523,7 +2736,13 @@
2523
2736
  } else if (data.options.treeData && data.options.treeData.nodes && data.options.treeData.nodes.length > 0) {
2524
2737
  // 更新进化树数据
2525
2738
  initTreeViz(data.options.treeData, data.options);
2739
+ } else {
2740
+ // 没有进化树数据,隐藏进化树 Tab
2741
+ hideTreeTab();
2526
2742
  }
2743
+ } else {
2744
+ // 没有 options,隐藏进化树 Tab
2745
+ hideTreeTab();
2527
2746
  }
2528
2747
 
2529
2748
  } 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.0",
3
+ "version": "1.5.2",
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
+ }