@basic-genomics/hivtrace-viz 1.1.9 → 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.
@@ -147,7 +147,6 @@
147
147
  cursor: pointer;
148
148
  transition: background 0.15s, color 0.15s;
149
149
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
150
- text-decoration: none;
151
150
  }
152
151
 
153
152
  .hivtrace-fullscreen-btn button:hover,
@@ -288,54 +287,6 @@
288
287
  padding: 4px 12px;
289
288
  }
290
289
 
291
- /* 簇选择器样式 */
292
- .hivtrace-cluster-selector {
293
- display: inline-flex;
294
- align-items: stretch;
295
- border: 1px solid #ccc;
296
- border-radius: 4px;
297
- background: #fff;
298
- }
299
-
300
- .hivtrace-cluster-selector-label {
301
- display: flex;
302
- align-items: center;
303
- justify-content: center;
304
- padding: 6px 12px;
305
- font-size: 13px;
306
- color: #555;
307
- background: #f5f5f5;
308
- border-right: 1px solid #ccc;
309
- white-space: nowrap;
310
- min-width: 60px;
311
- }
312
-
313
- .hivtrace-cluster-selector .input-group-btn {
314
- display: flex;
315
- }
316
-
317
- .hivtrace-cluster-selector-btn {
318
- border: none !important;
319
- border-radius: 0 !important;
320
- box-shadow: none !important;
321
- min-width: 100px;
322
- text-align: left;
323
- display: flex;
324
- align-items: center;
325
- justify-content: space-between;
326
- padding: 6px 12px;
327
- font-size: 13px;
328
- }
329
-
330
- .hivtrace-cluster-selector-btn:hover,
331
- .hivtrace-cluster-selector-btn:focus {
332
- background: #f9f9f9;
333
- }
334
-
335
- .hivtrace-cluster-selector-btn .caret {
336
- margin-left: 8px;
337
- }
338
-
339
290
  /* ========================================
340
291
  属性标签页 - 新布局
341
292
  顶部横向 pills + 下方左图右表
@@ -411,7 +362,8 @@
411
362
  }
412
363
 
413
364
  /* 下方:图表和表格上下排列(默认/小屏幕) */
414
- .hivtrace-attributes-content {
365
+ .hivtrace-attributes-content,
366
+ .hivtrace-stats-content {
415
367
  flex: 1;
416
368
  display: flex;
417
369
  flex-direction: column;
@@ -422,55 +374,24 @@
422
374
 
423
375
  /* 大屏幕(> 996px):左右布局 */
424
376
  @media (min-width: 997px) {
425
- .hivtrace-attributes-content {
426
- flex-direction: row;
427
- }
428
- }
429
377
 
430
- /* 概览统计区域:默认上下排列(小屏幕) */
431
- .hivtrace-overview-stats {
432
- flex: 1;
433
- display: flex;
434
- flex-direction: column;
435
- gap: 12px;
436
- min-height: 0;
437
- overflow: auto;
438
- }
439
-
440
- /* 大屏幕(> 996px):概览统计左右布局 */
441
- @media (min-width: 997px) {
442
- .hivtrace-overview-stats {
378
+ .hivtrace-attributes-content,
379
+ .hivtrace-stats-content {
443
380
  flex-direction: row;
444
381
  }
445
382
  }
446
383
 
447
- /* 统计面板样式 */
384
+ /* 可视化面板和统计面板 */
385
+ .hivtrace-viz-panel,
448
386
  .hivtrace-stats-panel {
449
387
  flex: 1;
450
- background: #f9fafb;
451
- border: 1px solid #e5e7eb;
452
- border-radius: 8px;
453
- padding: 12px;
454
- min-width: 0;
455
- }
456
-
457
- /* 大屏幕(> 996px):统计面板各占一半 */
458
- @media (min-width: 997px) {
459
- .hivtrace-stats-panel {
460
- flex: 1;
461
- min-width: 300px;
462
- }
463
- }
464
-
465
- /* 可视化面板 */
466
- .hivtrace-viz-panel {
467
- flex-shrink: 0;
468
388
  display: flex;
469
389
  flex-direction: column;
470
390
  background: #f9fafb;
471
391
  border: 1px solid #e5e7eb;
472
392
  border-radius: 8px;
473
393
  padding: 12px;
394
+ min-width: 0;
474
395
  }
475
396
 
476
397
  /* 大屏幕(> 996px):图表固定宽度 500px */
@@ -479,6 +400,11 @@
479
400
  width: 500px;
480
401
  flex-shrink: 0;
481
402
  }
403
+
404
+ .hivtrace-stats-panel {
405
+ flex: 1;
406
+ min-width: 300px;
407
+ }
482
408
  }
483
409
 
484
410
  .hivtrace-panel-toolbar {
@@ -686,7 +612,7 @@
686
612
  <a href="#trace-nodes" data-toggle="tab">节点</a>
687
613
  </li>
688
614
  <li class="disabled" id="attributes-tab">
689
- <a href="#trace-attributes" data-toggle="tab">属性</a>
615
+ <a href="#trace-attributes" data-toggle="tab">统计</a>
690
616
  </li>
691
617
  </ul>
692
618
 
@@ -858,6 +784,9 @@
858
784
  </div>
859
785
  </div>
860
786
 
787
+
788
+
789
+
861
790
  <div id="trace-clusters" class="tab-pane">
862
791
  <div class="row">
863
792
  <div class="col-lg-12">
@@ -885,16 +814,17 @@
885
814
 
886
815
  <div id="trace-attributes" class="tab-pane">
887
816
  <div class="hivtrace-attributes-container">
888
- <!-- 顶部:簇选择器 + 属性选择器(横向 pills) -->
817
+ <!-- 顶部:簇选择器 + 属性选择器 -->
889
818
  <div class="hivtrace-attributes-header">
890
819
  <!-- 簇选择器 -->
891
820
  <div style="margin-bottom: 12px;">
892
- <div class="hivtrace-cluster-selector">
893
- <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>
894
823
  <div class="input-group-btn">
895
- <button type="button" class="btn btn-default dropdown-toggle hivtrace-cluster-selector-btn"
896
- data-toggle="dropdown" id="attrs_cluster_selector_btn">
897
- <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>
898
828
  </button>
899
829
  <ul class="dropdown-menu" role="menu" id="attrs_cluster_selector_menu">
900
830
  <li><a href="#" data-value="">总览</a></li>
@@ -906,21 +836,21 @@
906
836
  <div class="hivtrace-pill-row" data-hivtrace-ui-role="attributes_cat"></div>
907
837
  </div>
908
838
 
909
- <!-- 概览统计内容(选中概览 pill 时显示,默认隐藏) -->
910
- <div class="hivtrace-overview-stats" id="attrs_stats_content" style="display: none;">
839
+ <!-- 统计内容区域(选中 None 时显示) -->
840
+ <div class="hivtrace-stats-content" id="attrs_stats_content">
911
841
  <div class="hivtrace-stats-panel">
912
842
  <p class="lead" id="attrs_stats_title">网络统计</p>
913
- <table class="table table-striped table-condensed table-responsive" id="attrs_graph_summary_table">
843
+ <table class="table table-striped table-condensed table-responsive" id="graph_summary_table">
914
844
  </table>
915
845
  </div>
916
846
  <div class="hivtrace-stats-panel">
917
- <p id="attrs_histogram_label" class="lead"></p>
918
- <div id="attrs_histogram_tag"></div>
847
+ <p id="histogram_label" class="lead"></p>
848
+ <div id="histogram_tag"></div>
919
849
  </div>
920
850
  </div>
921
851
 
922
- <!-- 属性内容(选中属性 pill 时显示) -->
923
- <div class="hivtrace-attributes-content">
852
+ <!-- 属性可视化内容(选中属性时显示) -->
853
+ <div class="hivtrace-attributes-content" style="display: none;">
924
854
  <!-- 左侧:可视化区域(弦图/散点图) -->
925
855
  <div class="hivtrace-viz-panel" data-hivtrace-ui-role="aux_svg_holder_enclosed" style="display: none">
926
856
  <div class="hivtrace-panel-toolbar">
@@ -959,7 +889,6 @@
959
889
  <img class="hidden" id="hyphy-chart-image" />
960
890
  <canvas class="hidden" id="hyphy-chart-canvas"></canvas>
961
891
 
962
- </div> <!-- 关闭内容容器 -->
963
892
  </div> <!-- 关闭 #hivtrace-main -->
964
893
 
965
894
  <script src="./hivtrace.js"></script>
@@ -994,9 +923,8 @@
994
923
  var attributes = null;
995
924
 
996
925
  // 从传入的options中获取配置
926
+ var enableClusterTracking = options && options.enableClusterTracking === true;
997
927
  var expandClusters = options && Array.isArray(options.expand) ? options.expand : [];
998
- // 获取自定义上下文菜单项配置
999
- var customContextMenuItems = options && Array.isArray(options.customContextMenuItems) ? options.customContextMenuItems : [];
1000
928
 
1001
929
  // 获取基因距离阈值并动态更新簇标签
1002
930
  var threshold = options && typeof options.threshold === 'number' ? options.threshold : 0.015;
@@ -1030,174 +958,16 @@
1030
958
  }
1031
959
  );
1032
960
 
1033
- // 将自定义上下文菜单项赋值给 user_graph 实例
1034
- if (customContextMenuItems.length > 0) {
1035
- user_graph.customContextMenuItems = customContextMenuItems;
1036
- // 设置点击回调,通过 postMessage 通知宿主应用
1037
- user_graph.onCustomMenuItemClick = function (itemId, clusterInfo) {
1038
- // 提取节点的关键信息(避免循环引用和不可序列化的对象)
1039
- var extractNodeInfo = function (node) {
1040
- if (!node) return null;
1041
- return {
1042
- id: node.id,
1043
- // 基本属性
1044
- degree: node.degree,
1045
- // 节点属性(如果存在)
1046
- attributes: node.patient_attributes || node.attributes || undefined,
1047
- // 位置信息
1048
- x: typeof node.x === 'number' ? node.x : undefined,
1049
- y: typeof node.y === 'number' ? node.y : undefined,
1050
- // 状态信息
1051
- is_hidden: Boolean(node.is_hidden),
1052
- match_filter: Boolean(node.match_filter),
1053
- // 子簇信息(如果存在)
1054
- subcluster_id: node.subcluster_id,
1055
- subcluster_label: node.subcluster_label,
1056
- };
1057
- };
1058
-
1059
- var essentialClusterData = {
1060
- cluster_id: clusterInfo?.cluster_id ?? clusterInfo?.id ?? undefined,
1061
- node_count: clusterInfo?.node_count ?? (Array.isArray(clusterInfo?.nodes) ? clusterInfo.nodes.length : 0),
1062
- cluster_size: typeof clusterInfo?.cluster_size === 'number' ? clusterInfo.cluster_size : undefined,
1063
- position: {
1064
- x: typeof clusterInfo?.x === 'number' ? clusterInfo.x : undefined,
1065
- y: typeof clusterInfo?.y === 'number' ? clusterInfo.y : undefined
1066
- },
1067
- state: {
1068
- expanded: Boolean(clusterInfo?.expanded),
1069
- fixed: Boolean(clusterInfo?.fixed)
1070
- },
1071
- // 节点 ID 列表(向后兼容)
1072
- node_ids: Array.isArray(clusterInfo?.nodes)
1073
- ? clusterInfo.nodes.map(function (node) { return node?.id }).filter(Boolean)
1074
- : [],
1075
- // 完整节点信息(用于扩展)
1076
- nodes: Array.isArray(clusterInfo?.nodes)
1077
- ? clusterInfo.nodes.map(extractNodeInfo).filter(Boolean)
1078
- : []
1079
- };
1080
- window.parent.postMessage({
1081
- type: 'CUSTOM_MENU_CLICK',
1082
- itemId: itemId,
1083
- clusterInfo: essentialClusterData
1084
- }, '*');
1085
- };
1086
- }
1087
-
1088
961
  if (user_graph.is_empty()) {
1089
962
  HandleAppError(
1090
963
  "This network contains no clusters and cannot be displayed"
1091
964
  );
1092
965
  } else {
1093
- // 旧的统计初始化已移除,统计功能现在在属性 Tab 的概览模式中
1094
-
1095
- // 渲染概览统计内容(表格 + 直方图)
1096
- // 被 clusternetwork.js 的 handleOverview 函数调用
1097
- window.renderOverviewStats = function (selectedClusterId) {
1098
- var statsTitle = document.getElementById('attrs_stats_title');
1099
- var summaryTable = '#attrs_graph_summary_table';
1100
- var histTag = '#attrs_histogram_tag';
1101
- var histLabel = '#attrs_histogram_label';
1102
- var clusters = user_graph.clusters || [];
1103
-
1104
- if (!selectedClusterId) {
1105
- // 显示全网统计
1106
- if (statsTitle) statsTitle.textContent = '网络统计';
1107
- hivtrace.graphSummary(user_graph, summaryTable);
1108
- hivtrace.histogramDistances(graph, histTag, histLabel);
1109
- } else {
1110
- // 显示单个 cluster 的统计
1111
- var cluster = clusters.find(function (c) {
1112
- return String(c.cluster_id) === String(selectedClusterId);
1113
- });
1114
-
1115
- if (cluster) {
1116
- if (statsTitle) statsTitle.textContent = '簇 ' + selectedClusterId + ' 统计';
1117
-
1118
- // 生成簇统计表格
1119
- var table = d3.select(summaryTable);
1120
- var tbody = table.select("tbody");
1121
- if (tbody.empty()) {
1122
- tbody = table.append("tbody");
1123
- }
1124
-
1125
- var degreeStats = cluster.degrees || { mean: 0, median: 0, min: 0, max: 0, Q1: 0, Q3: 0 };
1126
- var distanceStats = cluster.distances || { mean: 0, median: 0, min: 0, max: 0, Q1: 0, Q3: 0 };
1127
- var nodeCount = cluster.children ? cluster.children.length : 0;
1128
- var totalDegree = (degreeStats.mean || 0) * nodeCount;
1129
- var edgeCount = Math.round(totalDegree / 2);
1130
-
1131
- var floatFormat = d3.format(",.2r");
1132
- var percentFormat = d3.format(",.3p");
1133
-
1134
- var tableData = [
1135
- ['节点', nodeCount],
1136
- ['边', edgeCount],
1137
- ['每节点链接数', ''],
1138
- ['&nbsp;&nbsp;<i>平均值</i>', floatFormat(degreeStats.mean || 0)],
1139
- ['&nbsp;&nbsp;<i>中位数</i>', floatFormat(degreeStats.median || 0)],
1140
- ['&nbsp;&nbsp;<i>范围</i>', (degreeStats.min || 0) + ' - ' + (degreeStats.max || 0)],
1141
- ['&nbsp;&nbsp;<i>四分位距</i>', (degreeStats.Q1 || 0) + ' - ' + (degreeStats.Q3 || 0)]
1142
- ];
1143
-
1144
- if (distanceStats && distanceStats.mean !== null && distanceStats.mean !== undefined) {
1145
- tableData.push(['链接节点间的遗传距离', '']);
1146
- tableData.push(['&nbsp;&nbsp;<i>平均值</i>', percentFormat(distanceStats.mean)]);
1147
- tableData.push(['&nbsp;&nbsp;<i>中位数</i>', percentFormat(distanceStats.median)]);
1148
- tableData.push(['&nbsp;&nbsp;<i>范围</i>', percentFormat(distanceStats.min) + ' - ' + percentFormat(distanceStats.max)]);
1149
- tableData.push(['&nbsp;&nbsp;<i>四分位距</i>', percentFormat(distanceStats.Q1) + ' - ' + percentFormat(distanceStats.Q3)]);
1150
- }
1151
-
1152
- var rows = tbody.selectAll("tr").data(tableData);
1153
- rows.enter().append("tr");
1154
- rows.exit().remove();
1155
- var columns = rows.selectAll("td").data(function (d) { return d; });
1156
- columns.enter().append("td");
1157
- columns.exit().remove();
1158
- columns.html(function (d) { return d; });
1159
-
1160
- // 生成簇直方图
1161
- d3.select(histTag).selectAll("*").remove();
1162
- d3.select(histLabel).text('');
1163
-
1164
- var clusterNodeIds = new Set(cluster.children.map(function (n) { return n.id; }));
1165
- var clusterEdges = [];
1166
-
1167
- if (user_graph.edges && user_graph.edges.length > 0) {
1168
- user_graph.edges.forEach(function (e) {
1169
- var sourceNode = typeof e.source === 'object' ? e.source : user_graph.nodes[e.source];
1170
- var targetNode = typeof e.target === 'object' ? e.target : user_graph.nodes[e.target];
1171
- if (sourceNode && targetNode) {
1172
- var sourceId = sourceNode.id;
1173
- var targetId = targetNode.id;
1174
- if (clusterNodeIds.has(sourceId) && clusterNodeIds.has(targetId)) {
1175
- clusterEdges.push({ length: e.length });
1176
- }
1177
- }
1178
- });
1179
- }
1180
-
1181
- if (clusterEdges.length >= 3) {
1182
- var clusterGraphData = { Edges: clusterEdges };
1183
- hivtrace.histogramDistances(clusterGraphData, histTag, histLabel);
1184
- } else {
1185
- d3.select(histLabel).text('簇 ' + cluster.cluster_id + ' 遗传距离分布');
1186
- var message = clusterEdges.length === 0
1187
- ? "暂无直方图数据"
1188
- : "边数过少(" + clusterEdges.length + " 条),无法生成直方图";
1189
- d3.select(histTag).append("p")
1190
- .style("color", "#6b7280")
1191
- .style("text-align", "center")
1192
- .style("padding", "40px 0")
1193
- .text(message);
1194
- }
1195
- }
1196
- }
1197
- };
966
+ hivtrace.histogramDistances(graph, histogram_tag, histogram_label);
967
+ hivtrace.graphSummary(user_graph, graph_summary_tag);
1198
968
 
1199
- // 初始化属性标签页的 cluster 选择器
1200
- (function initAttrsClusterSelector() {
969
+ // 初始化 cluster 选择器 (统计 Tab)
970
+ (function initClusterSelector() {
1201
971
  var menu = document.getElementById('attrs_cluster_selector_menu');
1202
972
  var label = document.getElementById('attrs_cluster_selector_label');
1203
973
  if (!menu || !label) return;
@@ -1219,12 +989,16 @@
1219
989
  menu.appendChild(li);
1220
990
  });
1221
991
 
992
+ // 设置属性页的簇过滤状态
993
+ user_graph.attributes_selected_cluster = null;
994
+
1222
995
  // 监听 dropdown 菜单项点击
1223
996
  menu.addEventListener('click', function (e) {
1224
997
  if (e.target.tagName === 'A') {
1225
998
  e.preventDefault();
1226
999
  var selectedClusterId = e.target.getAttribute('data-value');
1227
1000
  var selectedText = e.target.textContent;
1001
+ var statsTitle = document.getElementById('attrs_stats_title');
1228
1002
 
1229
1003
  // 更新按钮文本
1230
1004
  label.textContent = selectedText;
@@ -1232,28 +1006,208 @@
1232
1006
  // 设置属性页的簇过滤
1233
1007
  user_graph.attributes_selected_cluster = selectedClusterId || null;
1234
1008
 
1235
- // 更新属性 pill 上的数字(显示簇内唯一值数量)
1236
- user_graph.updateAttributePillCounts(selectedClusterId || null);
1009
+ if (!selectedClusterId) {
1010
+ // 显示全网统计
1011
+ if (statsTitle) statsTitle.textContent = '网络统计';
1012
+ hivtrace.graphSummary(user_graph, graph_summary_tag);
1013
+ hivtrace.histogramDistances(graph, histogram_tag, histogram_label);
1014
+ } else {
1015
+ // 显示单个 cluster 的统计
1016
+ var cluster = clusters.find(function (c) {
1017
+ return String(c.cluster_id) === String(selectedClusterId);
1018
+ });
1237
1019
 
1238
- // 重新触发当前选中的属性(支持分类属性和连续属性)
1020
+ if (cluster) {
1021
+ if (statsTitle) statsTitle.textContent = '簇 ' + selectedClusterId + ' 统计';
1022
+ updateClusterStatistics(cluster);
1023
+ }
1024
+ }
1025
+
1026
+ // 重新触发当前选中的属性(如果有)
1239
1027
  if (user_graph.colorizer && user_graph.colorizer['category_id']) {
1240
- if (user_graph.colorizer['category_id'] === '_overview') {
1241
- // 概览模式 - 更新统计内容
1242
- window.renderOverviewStats(selectedClusterId || null);
1243
- } else if (user_graph.colorizer['continuous']) {
1244
- // 连续属性 - 散点图
1028
+ if (user_graph.colorizer['continuous']) {
1245
1029
  user_graph.handle_attribute_continuous(user_graph.colorizer['category_id']);
1246
1030
  } else {
1247
- // 分类属性 - 弦图
1248
1031
  user_graph.handle_attribute_categorical(user_graph.colorizer['category_id'], false);
1249
1032
  }
1250
1033
  }
1251
1034
  }
1252
1035
  });
1036
+
1037
+
1038
+ // 计算并显示单个 cluster 的统计
1039
+ function updateClusterStatistics(cluster) {
1040
+ var table = d3.select(graph_summary_tag);
1041
+ var tbody = table.select("tbody");
1042
+ if (tbody.empty()) {
1043
+ tbody = table.append("tbody");
1044
+ }
1045
+
1046
+ // 使用 cluster 对象已有的预计算统计信息(由 clusternetwork.js 计算)
1047
+ // cluster.degrees: 节点度分布统计 (mean, median, min, max, Q1, Q3)
1048
+ // cluster.distances: 边长度分布统计 (mean, median, min, max, Q1, Q3)
1049
+ var degreeStats = cluster.degrees || { mean: 0, median: 0, min: 0, max: 0, Q1: 0, Q3: 0 };
1050
+ var distanceStats = cluster.distances || { mean: 0, median: 0, min: 0, max: 0, Q1: 0, Q3: 0 };
1051
+
1052
+ // 节点数
1053
+ var nodeCount = cluster.children ? cluster.children.length : 0;
1054
+
1055
+ // 边数计算:度数总和 / 2(每条边贡献两个度)
1056
+ // 从预计算的统计反推:总度数 ≈ mean * nodeCount
1057
+ var totalDegree = (degreeStats.mean || 0) * nodeCount;
1058
+ var edgeCount = Math.round(totalDegree / 2);
1059
+
1060
+ var floatFormat = d3.format(",.2r");
1061
+ var percentFormat = d3.format(",.3p");
1062
+
1063
+ // 构建表格数据
1064
+ var tableData = [
1065
+ ['节点', nodeCount],
1066
+ ['边', edgeCount],
1067
+ ['每节点链接数', ''],
1068
+ ['&nbsp;&nbsp;<i>平均值</i>', floatFormat(degreeStats.mean || 0)],
1069
+ ['&nbsp;&nbsp;<i>中位数</i>', floatFormat(degreeStats.median || 0)],
1070
+ ['&nbsp;&nbsp;<i>范围</i>', (degreeStats.min || 0) + ' - ' + (degreeStats.max || 0)],
1071
+ ['&nbsp;&nbsp;<i>四分位距</i>', (degreeStats.Q1 || 0) + ' - ' + (degreeStats.Q3 || 0)]
1072
+ ];
1073
+
1074
+ // 添加遗传距离统计(使用预计算的 distances)
1075
+ if (distanceStats && distanceStats.mean !== null && distanceStats.mean !== undefined) {
1076
+ tableData.push(['链接节点间的遗传距离', '']);
1077
+ tableData.push(['&nbsp;&nbsp;<i>平均值</i>', percentFormat(distanceStats.mean)]);
1078
+ tableData.push(['&nbsp;&nbsp;<i>中位数</i>', percentFormat(distanceStats.median)]);
1079
+ tableData.push(['&nbsp;&nbsp;<i>范围</i>', percentFormat(distanceStats.min) + ' - ' + percentFormat(distanceStats.max)]);
1080
+ tableData.push(['&nbsp;&nbsp;<i>四分位距</i>', percentFormat(distanceStats.Q1) + ' - ' + percentFormat(distanceStats.Q3)]);
1081
+ }
1082
+
1083
+ // 更新表格
1084
+ var rows = tbody.selectAll("tr").data(tableData);
1085
+ rows.enter().append("tr");
1086
+ rows.exit().remove();
1087
+ var columns = rows.selectAll("td").data(function (d) { return d; });
1088
+ columns.enter().append("td");
1089
+ columns.exit().remove();
1090
+ columns.html(function (d) { return d; });
1091
+
1092
+ // 先清除之前的直方图内容
1093
+ d3.select(histogram_tag).selectAll("*").remove();
1094
+ d3.select(histogram_label).text('');
1095
+
1096
+ // 为 cluster 生成直方图
1097
+ // 从 user_graph.edges 提取属于该 cluster 的边
1098
+ var clusterNodeIds = new Set(cluster.children.map(function (n) { return n.id; }));
1099
+ var clusterEdges = [];
1100
+
1101
+
1102
+ // 遍历所有边,找到属于该 cluster 的边
1103
+ if (user_graph.edges && user_graph.edges.length > 0) {
1104
+ user_graph.edges.forEach(function (e) {
1105
+ // 边的 source 和 target 可能是对象或索引
1106
+ var sourceNode = typeof e.source === 'object' ? e.source : user_graph.nodes[e.source];
1107
+ var targetNode = typeof e.target === 'object' ? e.target : user_graph.nodes[e.target];
1108
+
1109
+ if (sourceNode && targetNode) {
1110
+ var sourceId = sourceNode.id;
1111
+ var targetId = targetNode.id;
1112
+
1113
+ if (clusterNodeIds.has(sourceId) && clusterNodeIds.has(targetId)) {
1114
+ clusterEdges.push({ length: e.length });
1115
+ }
1116
+ }
1117
+ });
1118
+ }
1119
+
1120
+ // 如果有足够边数据,显示直方图;否则显示提示
1121
+ // 直方图至少需要 3 条边才能有意义地显示分布
1122
+ if (clusterEdges.length >= 3) {
1123
+ var clusterGraphData = { Edges: clusterEdges };
1124
+ hivtrace.histogramDistances(clusterGraphData, histogram_tag, histogram_label);
1125
+ } else {
1126
+ // 边数太少,显示提示
1127
+ d3.select(histogram_tag).selectAll("*").remove();
1128
+ d3.select(histogram_label).text('簇 ' + cluster.cluster_id + ' 遗传距离分布');
1129
+
1130
+ var message = clusterEdges.length === 0
1131
+ ? "暂无直方图数据"
1132
+ : "边数过少(" + clusterEdges.length + " 条),无法生成直方图";
1133
+
1134
+ d3.select(histogram_tag).append("p")
1135
+ .style("color", "#6b7280")
1136
+ .style("text-align", "center")
1137
+ .style("padding", "40px 0")
1138
+ .text(message);
1139
+ }
1140
+
1141
+ }
1253
1142
  })();
1254
1143
 
1255
1144
 
1256
1145
 
1146
+
1147
+
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
+ }
1205
+ }
1206
+ }, '*');
1207
+ }
1208
+ };
1209
+ }
1210
+
1257
1211
  [
1258
1212
  "#main-tab",
1259
1213
  "#clusters-tab",
@@ -1306,26 +1260,6 @@
1306
1260
  }
1307
1261
  );
1308
1262
 
1309
- // 属性 tab 显示时,如果是概览模式则触发统计渲染
1310
- $("#attributes-tab a[data-toggle='tab']").on(
1311
- "shown.bs.tab",
1312
- function (e) {
1313
- // 检查是否为概览模式,并初始化统计内容
1314
- if (!user_graph.colorizer || !user_graph.colorizer['category_id'] ||
1315
- user_graph.colorizer['category_id'] === '_overview') {
1316
- // 显示统计内容,隐藏属性内容
1317
- var statsContent = document.getElementById('attrs_stats_content');
1318
- var attrsContent = document.querySelector('.hivtrace-attributes-content');
1319
- if (statsContent) statsContent.style.display = '';
1320
- if (attrsContent) attrsContent.style.display = 'none';
1321
- // 渲染统计
1322
- if (typeof window.renderOverviewStats === 'function') {
1323
- window.renderOverviewStats(user_graph.attributes_selected_cluster || null);
1324
- }
1325
- }
1326
- }
1327
- );
1328
-
1329
1263
  }
1330
1264
  document
1331
1265
  .getElementById("min_cluster_size_input")
@@ -1388,17 +1322,11 @@
1388
1322
  overlay.innerHTML = `
1389
1323
  <i class="fa fa-exclamation-triangle hivtrace-error-icon"></i>
1390
1324
  <div class="hivtrace-error-message">${message}</div>
1391
- <button class="hivtrace-retry-btn" onclick="requestRetry()">重试</button>
1325
+ <button class="hivtrace-retry-btn" onclick="location.reload()">重试</button>
1392
1326
  `;
1393
1327
  window.parent.postMessage({ type: 'ERROR', message: message }, '*');
1394
1328
  }
1395
1329
 
1396
- // 请求宿主应用重试
1397
- function requestRetry() {
1398
- window.parent.postMessage({ type: 'HIVTRACE_RETRY' }, '*');
1399
- }
1400
-
1401
-
1402
1330
 
1403
1331
  function in_progress() {
1404
1332
  return $(".progress").length > 0;