@basic-genomics/hivtrace-viz 1.1.8 → 1.1.10

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.
@@ -149,9 +149,11 @@
149
149
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
150
150
  }
151
151
 
152
- .hivtrace-fullscreen-btn button:hover {
152
+ .hivtrace-fullscreen-btn button:hover,
153
+ .hivtrace-fullscreen-btn button:focus {
153
154
  background: #f4f4f5;
154
155
  color: #18181b;
156
+ text-decoration: none;
155
157
  }
156
158
 
157
159
  /* 状态栏 badge 和 label 样式优化 */
@@ -285,54 +287,6 @@
285
287
  padding: 4px 12px;
286
288
  }
287
289
 
288
- /* 簇选择器样式 */
289
- .hivtrace-cluster-selector {
290
- display: inline-flex;
291
- align-items: stretch;
292
- border: 1px solid #ccc;
293
- border-radius: 4px;
294
- background: #fff;
295
- }
296
-
297
- .hivtrace-cluster-selector-label {
298
- display: flex;
299
- align-items: center;
300
- justify-content: center;
301
- padding: 6px 12px;
302
- font-size: 13px;
303
- color: #555;
304
- background: #f5f5f5;
305
- border-right: 1px solid #ccc;
306
- white-space: nowrap;
307
- min-width: 60px;
308
- }
309
-
310
- .hivtrace-cluster-selector .input-group-btn {
311
- display: flex;
312
- }
313
-
314
- .hivtrace-cluster-selector-btn {
315
- border: none !important;
316
- border-radius: 0 !important;
317
- box-shadow: none !important;
318
- min-width: 100px;
319
- text-align: left;
320
- display: flex;
321
- align-items: center;
322
- justify-content: space-between;
323
- padding: 6px 12px;
324
- font-size: 13px;
325
- }
326
-
327
- .hivtrace-cluster-selector-btn:hover,
328
- .hivtrace-cluster-selector-btn:focus {
329
- background: #f9f9f9;
330
- }
331
-
332
- .hivtrace-cluster-selector-btn .caret {
333
- margin-left: 8px;
334
- }
335
-
336
290
  /* ========================================
337
291
  属性标签页 - 新布局
338
292
  顶部横向 pills + 下方左图右表
@@ -408,7 +362,8 @@
408
362
  }
409
363
 
410
364
  /* 下方:图表和表格上下排列(默认/小屏幕) */
411
- .hivtrace-attributes-content {
365
+ .hivtrace-attributes-content,
366
+ .hivtrace-stats-content {
412
367
  flex: 1;
413
368
  display: flex;
414
369
  flex-direction: column;
@@ -419,20 +374,24 @@
419
374
 
420
375
  /* 大屏幕(> 996px):左右布局 */
421
376
  @media (min-width: 997px) {
422
- .hivtrace-attributes-content {
377
+
378
+ .hivtrace-attributes-content,
379
+ .hivtrace-stats-content {
423
380
  flex-direction: row;
424
381
  }
425
382
  }
426
383
 
427
- /* 可视化面板 */
428
- .hivtrace-viz-panel {
429
- flex-shrink: 0;
384
+ /* 可视化面板和统计面板 */
385
+ .hivtrace-viz-panel,
386
+ .hivtrace-stats-panel {
387
+ flex: 1;
430
388
  display: flex;
431
389
  flex-direction: column;
432
390
  background: #f9fafb;
433
391
  border: 1px solid #e5e7eb;
434
392
  border-radius: 8px;
435
393
  padding: 12px;
394
+ min-width: 0;
436
395
  }
437
396
 
438
397
  /* 大屏幕(> 996px):图表固定宽度 500px */
@@ -441,6 +400,11 @@
441
400
  width: 500px;
442
401
  flex-shrink: 0;
443
402
  }
403
+
404
+ .hivtrace-stats-panel {
405
+ flex: 1;
406
+ min-width: 300px;
407
+ }
444
408
  }
445
409
 
446
410
  .hivtrace-panel-toolbar {
@@ -641,9 +605,6 @@
641
605
  <li class="active" id="main-tab">
642
606
  <a id="trace-default-tab" href="#trace-results" data-toggle="tab">网络</a>
643
607
  </li>
644
- <li class="disabled" id="graph-tab">
645
- <a href="#trace-graph" data-toggle="tab">统计</a>
646
- </li>
647
608
  <li class="disabled" id="clusters-tab">
648
609
  <a href="#trace-clusters" data-toggle="tab">簇</a>
649
610
  </li>
@@ -651,7 +612,7 @@
651
612
  <a href="#trace-nodes" data-toggle="tab">节点</a>
652
613
  </li>
653
614
  <li class="disabled" id="attributes-tab">
654
- <a href="#trace-attributes" data-toggle="tab">属性</a>
615
+ <a href="#trace-attributes" data-toggle="tab">统计</a>
655
616
  </li>
656
617
  </ul>
657
618
 
@@ -823,39 +784,6 @@
823
784
  </div>
824
785
  </div>
825
786
 
826
- <div id="trace-graph" class="tab-pane">
827
- <div class="hivtrace-stats-container">
828
- <!-- 顶部:Cluster 选择器工具栏 -->
829
- <div class="hivtrace-stats-header">
830
- <div class="hivtrace-cluster-selector">
831
- <span class="hivtrace-cluster-selector-label">选择簇</span>
832
- <div class="input-group-btn">
833
- <button type="button" class="btn btn-default dropdown-toggle hivtrace-cluster-selector-btn"
834
- data-toggle="dropdown" id="stats_cluster_selector_btn">
835
- <span id="stats_cluster_selector_label">总览</span> <span class="caret"></span>
836
- </button>
837
- <ul class="dropdown-menu" role="menu" id="stats_cluster_selector_menu">
838
- <li><a href="#" data-value="">总览</a></li>
839
- </ul>
840
- </div>
841
- </div>
842
- </div>
843
-
844
- <!-- 下方:统计表格和直方图 -->
845
- <div class="hivtrace-stats-content">
846
- <div class="hivtrace-stats-panel">
847
- <p class="lead" id="stats_title">网络统计</p>
848
- <table class="table table-striped table-condensed table-responsive" id="graph_summary_table"></table>
849
- </div>
850
- <div class="hivtrace-stats-panel">
851
- <p id="histogram_label" class="lead"></p>
852
- <div id="histogram_tag"></div>
853
- </div>
854
- </div>
855
- </div>
856
- </div>
857
-
858
-
859
787
 
860
788
 
861
789
 
@@ -886,16 +814,17 @@
886
814
 
887
815
  <div id="trace-attributes" class="tab-pane">
888
816
  <div class="hivtrace-attributes-container">
889
- <!-- 顶部:簇选择器 + 属性选择器(横向 pills) -->
817
+ <!-- 顶部:簇选择器 + 属性选择器 -->
890
818
  <div class="hivtrace-attributes-header">
891
819
  <!-- 簇选择器 -->
892
820
  <div style="margin-bottom: 12px;">
893
- <div class="hivtrace-cluster-selector">
894
- <span class="hivtrace-cluster-selector-label">选择簇</span>
821
+ <div class="input-group input-group-sm" style="max-width: 280px;">
822
+ <span class="input-group-addon" style="min-width: 60px; text-align: center;">选择簇</span>
895
823
  <div class="input-group-btn">
896
- <button type="button" class="btn btn-default dropdown-toggle hivtrace-cluster-selector-btn"
897
- data-toggle="dropdown" id="attrs_cluster_selector_btn">
898
- <span id="attrs_cluster_selector_label">总览</span> <span class="caret"></span>
824
+ <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"
825
+ id="attrs_cluster_selector_btn" style="min-width: 140px; text-align: left;">
826
+ <span id="attrs_cluster_selector_label">总览</span> <span class="caret"
827
+ style="float: right; margin-top: 8px;"></span>
899
828
  </button>
900
829
  <ul class="dropdown-menu" role="menu" id="attrs_cluster_selector_menu">
901
830
  <li><a href="#" data-value="">总览</a></li>
@@ -907,8 +836,21 @@
907
836
  <div class="hivtrace-pill-row" data-hivtrace-ui-role="attributes_cat"></div>
908
837
  </div>
909
838
 
910
- <!-- 下方:左侧图表 + 右侧表格 -->
911
- <div class="hivtrace-attributes-content">
839
+ <!-- 统计内容区域(选中 None 时显示) -->
840
+ <div class="hivtrace-stats-content" id="attrs_stats_content">
841
+ <div class="hivtrace-stats-panel">
842
+ <p class="lead" id="attrs_stats_title">网络统计</p>
843
+ <table class="table table-striped table-condensed table-responsive" id="graph_summary_table">
844
+ </table>
845
+ </div>
846
+ <div class="hivtrace-stats-panel">
847
+ <p id="histogram_label" class="lead"></p>
848
+ <div id="histogram_tag"></div>
849
+ </div>
850
+ </div>
851
+
852
+ <!-- 属性可视化内容(选中属性时显示) -->
853
+ <div class="hivtrace-attributes-content" style="display: none;">
912
854
  <!-- 左侧:可视化区域(弦图/散点图) -->
913
855
  <div class="hivtrace-viz-panel" data-hivtrace-ui-role="aux_svg_holder_enclosed" style="display: none">
914
856
  <div class="hivtrace-panel-toolbar">
@@ -947,7 +889,6 @@
947
889
  <img class="hidden" id="hyphy-chart-image" />
948
890
  <canvas class="hidden" id="hyphy-chart-canvas"></canvas>
949
891
 
950
- </div> <!-- 关闭内容容器 -->
951
892
  </div> <!-- 关闭 #hivtrace-main -->
952
893
 
953
894
  <script src="./hivtrace.js"></script>
@@ -982,9 +923,8 @@
982
923
  var attributes = null;
983
924
 
984
925
  // 从传入的options中获取配置
926
+ var enableClusterTracking = options && options.enableClusterTracking === true;
985
927
  var expandClusters = options && Array.isArray(options.expand) ? options.expand : [];
986
- // 获取自定义上下文菜单项配置
987
- var customContextMenuItems = options && Array.isArray(options.customContextMenuItems) ? options.customContextMenuItems : [];
988
928
 
989
929
  // 获取基因距离阈值并动态更新簇标签
990
930
  var threshold = options && typeof options.threshold === 'number' ? options.threshold : 0.015;
@@ -1018,61 +958,6 @@
1018
958
  }
1019
959
  );
1020
960
 
1021
- // 将自定义上下文菜单项赋值给 user_graph 实例
1022
- if (customContextMenuItems.length > 0) {
1023
- user_graph.customContextMenuItems = customContextMenuItems;
1024
- // 设置点击回调,通过 postMessage 通知宿主应用
1025
- user_graph.onCustomMenuItemClick = function (itemId, clusterInfo) {
1026
- // 提取节点的关键信息(避免循环引用和不可序列化的对象)
1027
- var extractNodeInfo = function (node) {
1028
- if (!node) return null;
1029
- return {
1030
- id: node.id,
1031
- // 基本属性
1032
- degree: node.degree,
1033
- // 节点属性(如果存在)
1034
- attributes: node.patient_attributes || node.attributes || undefined,
1035
- // 位置信息
1036
- x: typeof node.x === 'number' ? node.x : undefined,
1037
- y: typeof node.y === 'number' ? node.y : undefined,
1038
- // 状态信息
1039
- is_hidden: Boolean(node.is_hidden),
1040
- match_filter: Boolean(node.match_filter),
1041
- // 子簇信息(如果存在)
1042
- subcluster_id: node.subcluster_id,
1043
- subcluster_label: node.subcluster_label,
1044
- };
1045
- };
1046
-
1047
- var essentialClusterData = {
1048
- cluster_id: clusterInfo?.cluster_id ?? clusterInfo?.id ?? undefined,
1049
- node_count: clusterInfo?.node_count ?? (Array.isArray(clusterInfo?.nodes) ? clusterInfo.nodes.length : 0),
1050
- cluster_size: typeof clusterInfo?.cluster_size === 'number' ? clusterInfo.cluster_size : undefined,
1051
- position: {
1052
- x: typeof clusterInfo?.x === 'number' ? clusterInfo.x : undefined,
1053
- y: typeof clusterInfo?.y === 'number' ? clusterInfo.y : undefined
1054
- },
1055
- state: {
1056
- expanded: Boolean(clusterInfo?.expanded),
1057
- fixed: Boolean(clusterInfo?.fixed)
1058
- },
1059
- // 节点 ID 列表(向后兼容)
1060
- node_ids: Array.isArray(clusterInfo?.nodes)
1061
- ? clusterInfo.nodes.map(function (node) { return node?.id }).filter(Boolean)
1062
- : [],
1063
- // 完整节点信息(用于扩展)
1064
- nodes: Array.isArray(clusterInfo?.nodes)
1065
- ? clusterInfo.nodes.map(extractNodeInfo).filter(Boolean)
1066
- : []
1067
- };
1068
- window.parent.postMessage({
1069
- type: 'CUSTOM_MENU_CLICK',
1070
- itemId: itemId,
1071
- clusterInfo: essentialClusterData
1072
- }, '*');
1073
- };
1074
- }
1075
-
1076
961
  if (user_graph.is_empty()) {
1077
962
  HandleAppError(
1078
963
  "This network contains no clusters and cannot be displayed"
@@ -1081,10 +966,10 @@
1081
966
  hivtrace.histogramDistances(graph, histogram_tag, histogram_label);
1082
967
  hivtrace.graphSummary(user_graph, graph_summary_tag);
1083
968
 
1084
- // 初始化 cluster 选择器 (Bootstrap dropdown)
969
+ // 初始化 cluster 选择器 (统计 Tab)
1085
970
  (function initClusterSelector() {
1086
- var menu = document.getElementById('stats_cluster_selector_menu');
1087
- var label = document.getElementById('stats_cluster_selector_label');
971
+ var menu = document.getElementById('attrs_cluster_selector_menu');
972
+ var label = document.getElementById('attrs_cluster_selector_label');
1088
973
  if (!menu || !label) return;
1089
974
 
1090
975
  // 获取所有 cluster 并按 ID 排序
@@ -1104,20 +989,26 @@
1104
989
  menu.appendChild(li);
1105
990
  });
1106
991
 
992
+ // 设置属性页的簇过滤状态
993
+ user_graph.attributes_selected_cluster = null;
994
+
1107
995
  // 监听 dropdown 菜单项点击
1108
996
  menu.addEventListener('click', function (e) {
1109
997
  if (e.target.tagName === 'A') {
1110
998
  e.preventDefault();
1111
999
  var selectedClusterId = e.target.getAttribute('data-value');
1112
1000
  var selectedText = e.target.textContent;
1113
- var statsTitle = document.getElementById('stats_title');
1001
+ var statsTitle = document.getElementById('attrs_stats_title');
1114
1002
 
1115
1003
  // 更新按钮文本
1116
1004
  label.textContent = selectedText;
1117
1005
 
1006
+ // 设置属性页的簇过滤
1007
+ user_graph.attributes_selected_cluster = selectedClusterId || null;
1008
+
1118
1009
  if (!selectedClusterId) {
1119
1010
  // 显示全网统计
1120
- statsTitle.textContent = '网络统计';
1011
+ if (statsTitle) statsTitle.textContent = '网络统计';
1121
1012
  hivtrace.graphSummary(user_graph, graph_summary_tag);
1122
1013
  hivtrace.histogramDistances(graph, histogram_tag, histogram_label);
1123
1014
  } else {
@@ -1127,10 +1018,19 @@
1127
1018
  });
1128
1019
 
1129
1020
  if (cluster) {
1130
- statsTitle.textContent = '簇 ' + selectedClusterId + ' 统计';
1021
+ if (statsTitle) statsTitle.textContent = '簇 ' + selectedClusterId + ' 统计';
1131
1022
  updateClusterStatistics(cluster);
1132
1023
  }
1133
1024
  }
1025
+
1026
+ // 重新触发当前选中的属性(如果有)
1027
+ if (user_graph.colorizer && user_graph.colorizer['category_id']) {
1028
+ if (user_graph.colorizer['continuous']) {
1029
+ user_graph.handle_attribute_continuous(user_graph.colorizer['category_id']);
1030
+ } else {
1031
+ user_graph.handle_attribute_categorical(user_graph.colorizer['category_id'], false);
1032
+ }
1033
+ }
1134
1034
  }
1135
1035
  });
1136
1036
 
@@ -1241,64 +1141,75 @@
1241
1141
  }
1242
1142
  })();
1243
1143
 
1244
- // 初始化属性标签页的 cluster 选择器
1245
- (function initAttrsClusterSelector() {
1246
- var menu = document.getElementById('attrs_cluster_selector_menu');
1247
- var label = document.getElementById('attrs_cluster_selector_label');
1248
- if (!menu || !label) return;
1249
-
1250
- // 获取所有 cluster 并按 ID 排序
1251
- var clusters = user_graph.clusters || [];
1252
- clusters = clusters.slice().sort(function (a, b) {
1253
- return a.cluster_id - b.cluster_id;
1254
- });
1255
-
1256
- // 填充 cluster 选项到 dropdown menu
1257
- clusters.forEach(function (cluster) {
1258
- var li = document.createElement('li');
1259
- var a = document.createElement('a');
1260
- a.href = '#';
1261
- a.setAttribute('data-value', cluster.cluster_id);
1262
- a.textContent = '簇 ' + cluster.cluster_id + ' (' + cluster.children.length + ' 节点)';
1263
- li.appendChild(a);
1264
- menu.appendChild(li);
1265
- });
1266
-
1267
- // 监听 dropdown 菜单项点击
1268
- menu.addEventListener('click', function (e) {
1269
- if (e.target.tagName === 'A') {
1270
- e.preventDefault();
1271
- var selectedClusterId = e.target.getAttribute('data-value');
1272
- var selectedText = e.target.textContent;
1273
1144
 
1274
- // 更新按钮文本
1275
- label.textContent = selectedText;
1276
1145
 
1277
- // 设置属性页的簇过滤
1278
- user_graph.attributes_selected_cluster = selectedClusterId || null;
1279
1146
 
1280
- // 更新属性 pill 上的数字(显示簇内唯一值数量)
1281
- user_graph.updateAttributePillCounts(selectedClusterId || null);
1282
1147
 
1283
- // 重新触发当前选中的属性(支持分类属性和连续属性)
1284
- if (user_graph.colorizer && user_graph.colorizer['category_id']) {
1285
- if (user_graph.colorizer['continuous']) {
1286
- // 连续属性 - 散点图
1287
- user_graph.handle_attribute_continuous(user_graph.colorizer['category_id']);
1288
- } else {
1289
- // 分类属性 - 弦图
1290
- user_graph.handle_attribute_categorical(user_graph.colorizer['category_id'], false);
1148
+ // 可配置的追踪簇回调功能 - 基于传入参数决定是否启用
1149
+ if (enableClusterTracking) {
1150
+ user_graph.onTrackCluster = function (clusterInfo, allData) {
1151
+ try {
1152
+ // 直接提取需要的数据,避免复杂的数据清理
1153
+ const essentialClusterData = {
1154
+ cluster_id: clusterInfo?.cluster_id ?? clusterInfo?.id ?? undefined,
1155
+ node_count: clusterInfo?.node_count ?? (Array.isArray(clusterInfo?.nodes) ? clusterInfo.nodes.length : 0),
1156
+ cluster_size: typeof clusterInfo?.cluster_size === 'number' ? clusterInfo.cluster_size : undefined,
1157
+ position: {
1158
+ x: typeof clusterInfo?.x === 'number' ? clusterInfo.x : undefined,
1159
+ y: typeof clusterInfo?.y === 'number' ? clusterInfo.y : undefined
1160
+ },
1161
+ state: {
1162
+ expanded: Boolean(clusterInfo?.expanded),
1163
+ fixed: Boolean(clusterInfo?.fixed)
1164
+ },
1165
+ // 只提取节点ID列表,避免传递复杂对象
1166
+ node_ids: Array.isArray(clusterInfo?.nodes)
1167
+ ? clusterInfo.nodes.slice(0, 20).map(node => node?.id).filter(Boolean)
1168
+ : []
1169
+ };
1170
+
1171
+ // 提取网络统计信息
1172
+ const networkStats = {
1173
+ total_nodes: allData?.network_info?.node_count ?? user_graph?.nodes?.length ?? 0,
1174
+ total_edges: allData?.network_info?.edge_count ?? user_graph?.edges?.length ?? 0,
1175
+ total_clusters: allData?.network_info?.cluster_count ?? 0
1176
+ };
1177
+
1178
+ // 发送轻量级、精确的数据
1179
+ window.parent.postMessage({
1180
+ type: 'TRACK_CLUSTER',
1181
+ clusterInfo: essentialClusterData,
1182
+ allData: { network_info: networkStats }
1183
+ }, '*');
1184
+
1185
+ } catch (error) {
1186
+ console.warn('簇追踪回调处理失败:', error.message);
1187
+
1188
+ // 最小化降级数据
1189
+ window.parent.postMessage({
1190
+ type: 'TRACK_CLUSTER',
1191
+ clusterInfo: {
1192
+ cluster_id: 'fallback',
1193
+ node_count: 0,
1194
+ cluster_size: undefined,
1195
+ position: { x: undefined, y: undefined },
1196
+ state: { expanded: false, fixed: false },
1197
+ node_ids: []
1198
+ },
1199
+ allData: {
1200
+ network_info: {
1201
+ total_nodes: 0,
1202
+ total_edges: 0,
1203
+ total_clusters: 0
1204
+ }
1291
1205
  }
1292
- }
1206
+ }, '*');
1293
1207
  }
1294
- });
1295
- })();
1296
-
1297
-
1208
+ };
1209
+ }
1298
1210
 
1299
1211
  [
1300
1212
  "#main-tab",
1301
- "#graph-tab",
1302
1213
  "#clusters-tab",
1303
1214
  "#subclusters-tab",
1304
1215
  "#nodes-tab",
@@ -1411,17 +1322,11 @@
1411
1322
  overlay.innerHTML = `
1412
1323
  <i class="fa fa-exclamation-triangle hivtrace-error-icon"></i>
1413
1324
  <div class="hivtrace-error-message">${message}</div>
1414
- <button class="hivtrace-retry-btn" onclick="requestRetry()">重试</button>
1325
+ <button class="hivtrace-retry-btn" onclick="location.reload()">重试</button>
1415
1326
  `;
1416
1327
  window.parent.postMessage({ type: 'ERROR', message: message }, '*');
1417
1328
  }
1418
1329
 
1419
- // 请求宿主应用重试
1420
- function requestRetry() {
1421
- window.parent.postMessage({ type: 'HIVTRACE_RETRY' }, '*');
1422
- }
1423
-
1424
-
1425
1330
 
1426
1331
  function in_progress() {
1427
1332
  return $(".progress").length > 0;