@basic-genomics/hivtrace-viz 1.1.9 → 1.1.11
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.
- package/dist/embed/hivtrace.js +1 -1
- package/dist/embed/hivtrace.js.map +1 -1
- package/dist/embed/index.html +286 -301
- package/dist/hivtrace.js +1 -1
- package/dist/hivtrace.js.map +1 -1
- package/package.json +1 -1
package/dist/embed/index.html
CHANGED
|
@@ -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
|
-
|
|
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"
|
|
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
|
-
<!-- 顶部:簇选择器 +
|
|
817
|
+
<!-- 顶部:簇选择器 + 属性选择器 -->
|
|
889
818
|
<div class="hivtrace-attributes-header">
|
|
890
819
|
<!-- 簇选择器 -->
|
|
891
820
|
<div style="margin-bottom: 12px;">
|
|
892
|
-
<div class="
|
|
893
|
-
<span class="
|
|
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
|
|
896
|
-
|
|
897
|
-
<span id="attrs_cluster_selector_label">总览</span> <span class="caret"
|
|
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
|
-
<!--
|
|
910
|
-
<div class="hivtrace-
|
|
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="
|
|
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="
|
|
918
|
-
<div id="
|
|
847
|
+
<p id="histogram_label" class="lead"></p>
|
|
848
|
+
<div id="histogram_tag"></div>
|
|
919
849
|
</div>
|
|
920
850
|
</div>
|
|
921
851
|
|
|
922
|
-
<!--
|
|
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
|
-
|
|
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
|
-
[' <i>平均值</i>', floatFormat(degreeStats.mean || 0)],
|
|
1139
|
-
[' <i>中位数</i>', floatFormat(degreeStats.median || 0)],
|
|
1140
|
-
[' <i>范围</i>', (degreeStats.min || 0) + ' - ' + (degreeStats.max || 0)],
|
|
1141
|
-
[' <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([' <i>平均值</i>', percentFormat(distanceStats.mean)]);
|
|
1147
|
-
tableData.push([' <i>中位数</i>', percentFormat(distanceStats.median)]);
|
|
1148
|
-
tableData.push([' <i>范围</i>', percentFormat(distanceStats.min) + ' - ' + percentFormat(distanceStats.max)]);
|
|
1149
|
-
tableData.push([' <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
|
-
//
|
|
1200
|
-
(function
|
|
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,265 @@
|
|
|
1232
1006
|
// 设置属性页的簇过滤
|
|
1233
1007
|
user_graph.attributes_selected_cluster = selectedClusterId || null;
|
|
1234
1008
|
|
|
1235
|
-
|
|
1236
|
-
|
|
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['
|
|
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
|
}
|
|
1034
|
+
|
|
1035
|
+
// 更新 pill badges(唯一值数量)
|
|
1036
|
+
updatePillBadges(selectedClusterId, clusters);
|
|
1251
1037
|
}
|
|
1252
1038
|
});
|
|
1039
|
+
|
|
1040
|
+
// 更新 pill badges 以反映选定簇的唯一值数量
|
|
1041
|
+
function updatePillBadges(selectedClusterId, clusters) {
|
|
1042
|
+
var pills = document.querySelectorAll('.hivtrace-pill-row .hivtrace-pill');
|
|
1043
|
+
if (!pills.length) return;
|
|
1044
|
+
|
|
1045
|
+
// 确定要统计的节点集合
|
|
1046
|
+
var nodes = [];
|
|
1047
|
+
if (!selectedClusterId) {
|
|
1048
|
+
// 全网:使用所有节点
|
|
1049
|
+
nodes = user_graph.nodes || [];
|
|
1050
|
+
} else {
|
|
1051
|
+
// 特定簇:只使用该簇的节点
|
|
1052
|
+
var cluster = clusters.find(function (c) {
|
|
1053
|
+
return String(c.cluster_id) === String(selectedClusterId);
|
|
1054
|
+
});
|
|
1055
|
+
if (cluster && cluster.children) {
|
|
1056
|
+
nodes = cluster.children;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// 更新每个 pill 的 badge
|
|
1061
|
+
pills.forEach(function (pill) {
|
|
1062
|
+
var badge = pill.querySelector('.badge');
|
|
1063
|
+
if (!badge) return;
|
|
1064
|
+
|
|
1065
|
+
// 从 pill 的原始 HTML 中提取属性 ID(通过闭包数据不可用,需要通过文本匹配)
|
|
1066
|
+
var pillText = pill.textContent.trim();
|
|
1067
|
+
var attrId = null;
|
|
1068
|
+
|
|
1069
|
+
// 遍历 user_graph.json 中的属性来匹配
|
|
1070
|
+
if (user_graph.json && user_graph.json.patient_attribute_schema) {
|
|
1071
|
+
for (var key in user_graph.json.patient_attribute_schema) {
|
|
1072
|
+
var attr = user_graph.json.patient_attribute_schema[key];
|
|
1073
|
+
if (attr.label && pillText.indexOf(attr.label) === 0) {
|
|
1074
|
+
attrId = key;
|
|
1075
|
+
break;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
if (attrId) {
|
|
1081
|
+
// 计算选定节点中该属性的唯一值数量
|
|
1082
|
+
var uniqueValues = new Set();
|
|
1083
|
+
nodes.forEach(function (node) {
|
|
1084
|
+
var value = user_graph.attribute_node_value_by_id(node, attrId);
|
|
1085
|
+
if (value !== null && value !== undefined && value !== 'missing') {
|
|
1086
|
+
uniqueValues.add(value);
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
badge.textContent = uniqueValues.size;
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
// 计算并显示单个 cluster 的统计
|
|
1096
|
+
function updateClusterStatistics(cluster) {
|
|
1097
|
+
var table = d3.select(graph_summary_tag);
|
|
1098
|
+
var tbody = table.select("tbody");
|
|
1099
|
+
if (tbody.empty()) {
|
|
1100
|
+
tbody = table.append("tbody");
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// 使用 cluster 对象已有的预计算统计信息(由 clusternetwork.js 计算)
|
|
1104
|
+
// cluster.degrees: 节点度分布统计 (mean, median, min, max, Q1, Q3)
|
|
1105
|
+
// cluster.distances: 边长度分布统计 (mean, median, min, max, Q1, Q3)
|
|
1106
|
+
var degreeStats = cluster.degrees || { mean: 0, median: 0, min: 0, max: 0, Q1: 0, Q3: 0 };
|
|
1107
|
+
var distanceStats = cluster.distances || { mean: 0, median: 0, min: 0, max: 0, Q1: 0, Q3: 0 };
|
|
1108
|
+
|
|
1109
|
+
// 节点数
|
|
1110
|
+
var nodeCount = cluster.children ? cluster.children.length : 0;
|
|
1111
|
+
|
|
1112
|
+
// 边数计算:度数总和 / 2(每条边贡献两个度)
|
|
1113
|
+
// 从预计算的统计反推:总度数 ≈ mean * nodeCount
|
|
1114
|
+
var totalDegree = (degreeStats.mean || 0) * nodeCount;
|
|
1115
|
+
var edgeCount = Math.round(totalDegree / 2);
|
|
1116
|
+
|
|
1117
|
+
var floatFormat = d3.format(",.2r");
|
|
1118
|
+
var percentFormat = d3.format(",.3p");
|
|
1119
|
+
|
|
1120
|
+
// 构建表格数据
|
|
1121
|
+
var tableData = [
|
|
1122
|
+
['节点', nodeCount],
|
|
1123
|
+
['边', edgeCount],
|
|
1124
|
+
['每节点链接数', ''],
|
|
1125
|
+
[' <i>平均值</i>', floatFormat(degreeStats.mean || 0)],
|
|
1126
|
+
[' <i>中位数</i>', floatFormat(degreeStats.median || 0)],
|
|
1127
|
+
[' <i>范围</i>', (degreeStats.min || 0) + ' - ' + (degreeStats.max || 0)],
|
|
1128
|
+
[' <i>四分位距</i>', (degreeStats.Q1 || 0) + ' - ' + (degreeStats.Q3 || 0)]
|
|
1129
|
+
];
|
|
1130
|
+
|
|
1131
|
+
// 添加遗传距离统计(使用预计算的 distances)
|
|
1132
|
+
if (distanceStats && distanceStats.mean !== null && distanceStats.mean !== undefined) {
|
|
1133
|
+
tableData.push(['链接节点间的遗传距离', '']);
|
|
1134
|
+
tableData.push([' <i>平均值</i>', percentFormat(distanceStats.mean)]);
|
|
1135
|
+
tableData.push([' <i>中位数</i>', percentFormat(distanceStats.median)]);
|
|
1136
|
+
tableData.push([' <i>范围</i>', percentFormat(distanceStats.min) + ' - ' + percentFormat(distanceStats.max)]);
|
|
1137
|
+
tableData.push([' <i>四分位距</i>', percentFormat(distanceStats.Q1) + ' - ' + percentFormat(distanceStats.Q3)]);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// 更新表格
|
|
1141
|
+
var rows = tbody.selectAll("tr").data(tableData);
|
|
1142
|
+
rows.enter().append("tr");
|
|
1143
|
+
rows.exit().remove();
|
|
1144
|
+
var columns = rows.selectAll("td").data(function (d) { return d; });
|
|
1145
|
+
columns.enter().append("td");
|
|
1146
|
+
columns.exit().remove();
|
|
1147
|
+
columns.html(function (d) { return d; });
|
|
1148
|
+
|
|
1149
|
+
// 先清除之前的直方图内容
|
|
1150
|
+
d3.select(histogram_tag).selectAll("*").remove();
|
|
1151
|
+
d3.select(histogram_label).text('');
|
|
1152
|
+
|
|
1153
|
+
// 为 cluster 生成直方图
|
|
1154
|
+
// 从 user_graph.edges 提取属于该 cluster 的边
|
|
1155
|
+
var clusterNodeIds = new Set(cluster.children.map(function (n) { return n.id; }));
|
|
1156
|
+
var clusterEdges = [];
|
|
1157
|
+
|
|
1158
|
+
|
|
1159
|
+
// 遍历所有边,找到属于该 cluster 的边
|
|
1160
|
+
if (user_graph.edges && user_graph.edges.length > 0) {
|
|
1161
|
+
user_graph.edges.forEach(function (e) {
|
|
1162
|
+
// 边的 source 和 target 可能是对象或索引
|
|
1163
|
+
var sourceNode = typeof e.source === 'object' ? e.source : user_graph.nodes[e.source];
|
|
1164
|
+
var targetNode = typeof e.target === 'object' ? e.target : user_graph.nodes[e.target];
|
|
1165
|
+
|
|
1166
|
+
if (sourceNode && targetNode) {
|
|
1167
|
+
var sourceId = sourceNode.id;
|
|
1168
|
+
var targetId = targetNode.id;
|
|
1169
|
+
|
|
1170
|
+
if (clusterNodeIds.has(sourceId) && clusterNodeIds.has(targetId)) {
|
|
1171
|
+
clusterEdges.push({ length: e.length });
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// 如果有足够边数据,显示直方图;否则显示提示
|
|
1178
|
+
// 直方图至少需要 3 条边才能有意义地显示分布
|
|
1179
|
+
if (clusterEdges.length >= 3) {
|
|
1180
|
+
var clusterGraphData = { Edges: clusterEdges };
|
|
1181
|
+
hivtrace.histogramDistances(clusterGraphData, histogram_tag, histogram_label);
|
|
1182
|
+
} else {
|
|
1183
|
+
// 边数太少,显示提示
|
|
1184
|
+
d3.select(histogram_tag).selectAll("*").remove();
|
|
1185
|
+
d3.select(histogram_label).text('簇 ' + cluster.cluster_id + ' 遗传距离分布');
|
|
1186
|
+
|
|
1187
|
+
var message = clusterEdges.length === 0
|
|
1188
|
+
? "暂无直方图数据"
|
|
1189
|
+
: "边数过少(" + clusterEdges.length + " 条),无法生成直方图";
|
|
1190
|
+
|
|
1191
|
+
d3.select(histogram_tag).append("p")
|
|
1192
|
+
.style("color", "#6b7280")
|
|
1193
|
+
.style("text-align", "center")
|
|
1194
|
+
.style("padding", "40px 0")
|
|
1195
|
+
.text(message);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
}
|
|
1253
1199
|
})();
|
|
1254
1200
|
|
|
1255
1201
|
|
|
1256
1202
|
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
// 可配置的追踪簇回调功能 - 基于传入参数决定是否启用
|
|
1206
|
+
if (enableClusterTracking) {
|
|
1207
|
+
user_graph.onTrackCluster = function (clusterInfo, allData) {
|
|
1208
|
+
try {
|
|
1209
|
+
// 直接提取需要的数据,避免复杂的数据清理
|
|
1210
|
+
const essentialClusterData = {
|
|
1211
|
+
cluster_id: clusterInfo?.cluster_id ?? clusterInfo?.id ?? undefined,
|
|
1212
|
+
node_count: clusterInfo?.node_count ?? (Array.isArray(clusterInfo?.nodes) ? clusterInfo.nodes.length : 0),
|
|
1213
|
+
cluster_size: typeof clusterInfo?.cluster_size === 'number' ? clusterInfo.cluster_size : undefined,
|
|
1214
|
+
position: {
|
|
1215
|
+
x: typeof clusterInfo?.x === 'number' ? clusterInfo.x : undefined,
|
|
1216
|
+
y: typeof clusterInfo?.y === 'number' ? clusterInfo.y : undefined
|
|
1217
|
+
},
|
|
1218
|
+
state: {
|
|
1219
|
+
expanded: Boolean(clusterInfo?.expanded),
|
|
1220
|
+
fixed: Boolean(clusterInfo?.fixed)
|
|
1221
|
+
},
|
|
1222
|
+
// 只提取节点ID列表,避免传递复杂对象
|
|
1223
|
+
node_ids: Array.isArray(clusterInfo?.nodes)
|
|
1224
|
+
? clusterInfo.nodes.slice(0, 20).map(node => node?.id).filter(Boolean)
|
|
1225
|
+
: []
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
// 提取网络统计信息
|
|
1229
|
+
const networkStats = {
|
|
1230
|
+
total_nodes: allData?.network_info?.node_count ?? user_graph?.nodes?.length ?? 0,
|
|
1231
|
+
total_edges: allData?.network_info?.edge_count ?? user_graph?.edges?.length ?? 0,
|
|
1232
|
+
total_clusters: allData?.network_info?.cluster_count ?? 0
|
|
1233
|
+
};
|
|
1234
|
+
|
|
1235
|
+
// 发送轻量级、精确的数据
|
|
1236
|
+
window.parent.postMessage({
|
|
1237
|
+
type: 'TRACK_CLUSTER',
|
|
1238
|
+
clusterInfo: essentialClusterData,
|
|
1239
|
+
allData: { network_info: networkStats }
|
|
1240
|
+
}, '*');
|
|
1241
|
+
|
|
1242
|
+
} catch (error) {
|
|
1243
|
+
console.warn('簇追踪回调处理失败:', error.message);
|
|
1244
|
+
|
|
1245
|
+
// 最小化降级数据
|
|
1246
|
+
window.parent.postMessage({
|
|
1247
|
+
type: 'TRACK_CLUSTER',
|
|
1248
|
+
clusterInfo: {
|
|
1249
|
+
cluster_id: 'fallback',
|
|
1250
|
+
node_count: 0,
|
|
1251
|
+
cluster_size: undefined,
|
|
1252
|
+
position: { x: undefined, y: undefined },
|
|
1253
|
+
state: { expanded: false, fixed: false },
|
|
1254
|
+
node_ids: []
|
|
1255
|
+
},
|
|
1256
|
+
allData: {
|
|
1257
|
+
network_info: {
|
|
1258
|
+
total_nodes: 0,
|
|
1259
|
+
total_edges: 0,
|
|
1260
|
+
total_clusters: 0
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}, '*');
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1257
1268
|
[
|
|
1258
1269
|
"#main-tab",
|
|
1259
1270
|
"#clusters-tab",
|
|
@@ -1306,26 +1317,6 @@
|
|
|
1306
1317
|
}
|
|
1307
1318
|
);
|
|
1308
1319
|
|
|
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
1320
|
}
|
|
1330
1321
|
document
|
|
1331
1322
|
.getElementById("min_cluster_size_input")
|
|
@@ -1388,17 +1379,11 @@
|
|
|
1388
1379
|
overlay.innerHTML = `
|
|
1389
1380
|
<i class="fa fa-exclamation-triangle hivtrace-error-icon"></i>
|
|
1390
1381
|
<div class="hivtrace-error-message">${message}</div>
|
|
1391
|
-
<button class="hivtrace-retry-btn" onclick="
|
|
1382
|
+
<button class="hivtrace-retry-btn" onclick="location.reload()">重试</button>
|
|
1392
1383
|
`;
|
|
1393
1384
|
window.parent.postMessage({ type: 'ERROR', message: message }, '*');
|
|
1394
1385
|
}
|
|
1395
1386
|
|
|
1396
|
-
// 请求宿主应用重试
|
|
1397
|
-
function requestRetry() {
|
|
1398
|
-
window.parent.postMessage({ type: 'HIVTRACE_RETRY' }, '*');
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
1387
|
|
|
1403
1388
|
function in_progress() {
|
|
1404
1389
|
return $(".progress").length > 0;
|