@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.
Files changed (2) hide show
  1. package/dist/embed/index.html +170 -17
  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,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.0",
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
+ }