@basic-genomics/hivtrace-viz 1.0.0 → 1.1.0
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.css +341 -0
- package/dist/embed/hivtrace.css.map +1 -1
- package/dist/embed/hivtrace.js +1 -1
- package/dist/embed/hivtrace.js.map +1 -1
- package/dist/embed/index.html +268 -13
- package/dist/embed/locales/en-US.json +5 -2
- package/dist/embed/locales/zh-CN.json +5 -2
- package/dist/hivtrace.css +341 -0
- package/dist/hivtrace.css.map +1 -1
- package/dist/hivtrace.js +1 -1
- package/dist/hivtrace.js.map +1 -1
- package/package.json +2 -2
package/dist/embed/index.html
CHANGED
|
@@ -212,7 +212,8 @@
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
/* 网络标签页操作栏 - 超出换行 */
|
|
215
|
-
#network_ui_bar
|
|
215
|
+
#network_ui_bar,
|
|
216
|
+
.cloned-cluster-tab {
|
|
216
217
|
display: flex;
|
|
217
218
|
flex-wrap: wrap;
|
|
218
219
|
align-items: center;
|
|
@@ -221,7 +222,11 @@
|
|
|
221
222
|
|
|
222
223
|
#network_ui_bar>.input-group-btn,
|
|
223
224
|
#network_ui_bar>.btn,
|
|
224
|
-
#network_ui_bar>.input-group
|
|
225
|
+
#network_ui_bar>.input-group,
|
|
226
|
+
.cloned-cluster-tab>.input-group-btn,
|
|
227
|
+
.cloned-cluster-tab>.btn,
|
|
228
|
+
.cloned-cluster-tab>.input-group,
|
|
229
|
+
.cloned-cluster-tab>.span {
|
|
225
230
|
flex-shrink: 0;
|
|
226
231
|
width: auto;
|
|
227
232
|
}
|
|
@@ -277,7 +282,7 @@
|
|
|
277
282
|
}
|
|
278
283
|
|
|
279
284
|
.dropdown-menu>li>a {
|
|
280
|
-
padding:
|
|
285
|
+
padding: 4px 12px;
|
|
281
286
|
}
|
|
282
287
|
|
|
283
288
|
/* ========================================
|
|
@@ -354,7 +359,7 @@
|
|
|
354
359
|
color: #1e40af;
|
|
355
360
|
}
|
|
356
361
|
|
|
357
|
-
/*
|
|
362
|
+
/* 下方:图表和表格上下排列(默认/小屏幕) */
|
|
358
363
|
.hivtrace-attributes-content {
|
|
359
364
|
flex: 1;
|
|
360
365
|
display: flex;
|
|
@@ -364,6 +369,13 @@
|
|
|
364
369
|
overflow: auto;
|
|
365
370
|
}
|
|
366
371
|
|
|
372
|
+
/* 大屏幕(> 996px):左右布局 */
|
|
373
|
+
@media (min-width: 997px) {
|
|
374
|
+
.hivtrace-attributes-content {
|
|
375
|
+
flex-direction: row;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
367
379
|
/* 可视化面板 */
|
|
368
380
|
.hivtrace-viz-panel {
|
|
369
381
|
flex-shrink: 0;
|
|
@@ -375,6 +387,14 @@
|
|
|
375
387
|
padding: 12px;
|
|
376
388
|
}
|
|
377
389
|
|
|
390
|
+
/* 大屏幕(> 996px):图表固定宽度 500px */
|
|
391
|
+
@media (min-width: 997px) {
|
|
392
|
+
.hivtrace-viz-panel {
|
|
393
|
+
width: 500px;
|
|
394
|
+
flex-shrink: 0;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
378
398
|
.hivtrace-panel-toolbar {
|
|
379
399
|
display: flex;
|
|
380
400
|
align-items: center;
|
|
@@ -408,6 +428,9 @@
|
|
|
408
428
|
border-radius: 8px;
|
|
409
429
|
padding: 12px;
|
|
410
430
|
min-height: 200px;
|
|
431
|
+
min-width: 0;
|
|
432
|
+
/* 确保 flexbox 子元素不会超出容器 */
|
|
433
|
+
overflow: hidden;
|
|
411
434
|
}
|
|
412
435
|
|
|
413
436
|
.hivtrace-table-wrapper {
|
|
@@ -418,9 +441,50 @@
|
|
|
418
441
|
border-radius: 6px;
|
|
419
442
|
}
|
|
420
443
|
|
|
444
|
+
/* 大屏幕(左右布局)时限制表格高度 */
|
|
445
|
+
@media (min-width: 997px) {
|
|
446
|
+
.hivtrace-table-wrapper {
|
|
447
|
+
max-height: 500px;
|
|
448
|
+
/* 表格最大高度,超出滚动 */
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/* 簇和节点标签页的表格高度 - 自适应 + 最小高度 */
|
|
453
|
+
#trace-clusters .hivtrace-table-wrapper,
|
|
454
|
+
#trace-nodes .hivtrace-table-wrapper {
|
|
455
|
+
min-height: 200px;
|
|
456
|
+
max-height: calc(100vh - 120px);
|
|
457
|
+
}
|
|
458
|
+
|
|
421
459
|
.hivtrace-table-wrapper table {
|
|
422
460
|
margin: 0;
|
|
423
461
|
width: 100%;
|
|
462
|
+
border-collapse: separate;
|
|
463
|
+
border-spacing: 0;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/* 表头吸顶 */
|
|
467
|
+
.hivtrace-table-wrapper thead th {
|
|
468
|
+
position: sticky;
|
|
469
|
+
top: 0;
|
|
470
|
+
z-index: 2;
|
|
471
|
+
background: #f9fafb;
|
|
472
|
+
border-bottom: 1px solid #e5e7eb;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/* 左侧第一列吸附 */
|
|
476
|
+
.hivtrace-table-wrapper tbody td:first-child,
|
|
477
|
+
.hivtrace-table-wrapper thead th:first-child {
|
|
478
|
+
position: sticky;
|
|
479
|
+
left: 0;
|
|
480
|
+
z-index: 1;
|
|
481
|
+
background: #f9fafb;
|
|
482
|
+
border-right: 1px solid #e5e7eb;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/* 左上角交叉单元格需要最高层级 */
|
|
486
|
+
.hivtrace-table-wrapper thead th:first-child {
|
|
487
|
+
z-index: 3;
|
|
424
488
|
}
|
|
425
489
|
|
|
426
490
|
.hivtrace-table-content table {
|
|
@@ -432,6 +496,7 @@
|
|
|
432
496
|
display: inline-flex;
|
|
433
497
|
align-items: center;
|
|
434
498
|
gap: 6px;
|
|
499
|
+
margin: 0px;
|
|
435
500
|
font-size: 13px;
|
|
436
501
|
color: #6b7280;
|
|
437
502
|
cursor: pointer;
|
|
@@ -711,25 +776,49 @@
|
|
|
711
776
|
</div>
|
|
712
777
|
|
|
713
778
|
<div id="trace-graph" class="tab-pane">
|
|
714
|
-
<div class="
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
<
|
|
779
|
+
<div class="hivtrace-stats-container">
|
|
780
|
+
<!-- 顶部:Cluster 选择器工具栏 -->
|
|
781
|
+
<div class="hivtrace-stats-header">
|
|
782
|
+
<div class="input-group input-group-sm" style="max-width: 280px;">
|
|
783
|
+
<span class="input-group-addon">选择簇</span>
|
|
784
|
+
<div class="input-group-btn">
|
|
785
|
+
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"
|
|
786
|
+
id="stats_cluster_selector_btn">
|
|
787
|
+
<span id="stats_cluster_selector_label">总览</span> <span class="caret"></span>
|
|
788
|
+
</button>
|
|
789
|
+
<ul class="dropdown-menu" role="menu" id="stats_cluster_selector_menu">
|
|
790
|
+
<li><a href="#" data-value="">总览</a></li>
|
|
791
|
+
</ul>
|
|
792
|
+
</div>
|
|
793
|
+
</div>
|
|
718
794
|
</div>
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
795
|
+
|
|
796
|
+
<!-- 下方:统计表格和直方图 -->
|
|
797
|
+
<div class="hivtrace-stats-content">
|
|
798
|
+
<div class="hivtrace-stats-panel">
|
|
799
|
+
<p class="lead" id="stats_title">网络统计</p>
|
|
800
|
+
<table class="table table-striped table-condensed table-responsive" id="graph_summary_table"></table>
|
|
801
|
+
</div>
|
|
802
|
+
<div class="hivtrace-stats-panel">
|
|
803
|
+
<p id="histogram_label" class="lead"></p>
|
|
804
|
+
<div id="histogram_tag"></div>
|
|
805
|
+
</div>
|
|
722
806
|
</div>
|
|
723
807
|
</div>
|
|
724
808
|
</div>
|
|
725
809
|
|
|
726
810
|
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
|
|
727
814
|
<div id="trace-clusters" class="tab-pane">
|
|
728
815
|
<div class="row">
|
|
729
816
|
<div class="col-lg-12">
|
|
730
817
|
<span class="pull-right" id="cluster-table-export"> </span>
|
|
731
818
|
<p class="lead">簇</p>
|
|
732
|
-
<
|
|
819
|
+
<div class="hivtrace-table-wrapper">
|
|
820
|
+
<table class="table table-striped table-condensed table-hover" id="cluster_table"></table>
|
|
821
|
+
</div>
|
|
733
822
|
</div>
|
|
734
823
|
</div>
|
|
735
824
|
</div>
|
|
@@ -740,7 +829,9 @@
|
|
|
740
829
|
<div class="col-lg-12">
|
|
741
830
|
<span class="pull-right" id="node-table-export"> </span>
|
|
742
831
|
<p class="lead">关联个体</p>
|
|
743
|
-
<
|
|
832
|
+
<div class="hivtrace-table-wrapper">
|
|
833
|
+
<table class="table table-striped table-condensed table-hover" id="node_table"></table>
|
|
834
|
+
</div>
|
|
744
835
|
</div>
|
|
745
836
|
</div>
|
|
746
837
|
</div>
|
|
@@ -870,6 +961,170 @@
|
|
|
870
961
|
hivtrace.histogramDistances(graph, histogram_tag, histogram_label);
|
|
871
962
|
hivtrace.graphSummary(user_graph, graph_summary_tag);
|
|
872
963
|
|
|
964
|
+
// 初始化 cluster 选择器 (Bootstrap dropdown)
|
|
965
|
+
(function initClusterSelector() {
|
|
966
|
+
var menu = document.getElementById('stats_cluster_selector_menu');
|
|
967
|
+
var label = document.getElementById('stats_cluster_selector_label');
|
|
968
|
+
if (!menu || !label) return;
|
|
969
|
+
|
|
970
|
+
// 获取所有 cluster 并按 ID 排序
|
|
971
|
+
var clusters = user_graph.clusters || [];
|
|
972
|
+
clusters = clusters.slice().sort(function (a, b) {
|
|
973
|
+
return a.cluster_id - b.cluster_id;
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
// 填充 cluster 选项到 dropdown menu
|
|
977
|
+
clusters.forEach(function (cluster) {
|
|
978
|
+
var li = document.createElement('li');
|
|
979
|
+
var a = document.createElement('a');
|
|
980
|
+
a.href = '#';
|
|
981
|
+
a.setAttribute('data-value', cluster.cluster_id);
|
|
982
|
+
a.textContent = '簇 ' + cluster.cluster_id + ' (' + cluster.children.length + ' 节点)';
|
|
983
|
+
li.appendChild(a);
|
|
984
|
+
menu.appendChild(li);
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
// 监听 dropdown 菜单项点击
|
|
988
|
+
menu.addEventListener('click', function (e) {
|
|
989
|
+
if (e.target.tagName === 'A') {
|
|
990
|
+
e.preventDefault();
|
|
991
|
+
var selectedClusterId = e.target.getAttribute('data-value');
|
|
992
|
+
var selectedText = e.target.textContent;
|
|
993
|
+
var statsTitle = document.getElementById('stats_title');
|
|
994
|
+
|
|
995
|
+
// 更新按钮文本
|
|
996
|
+
label.textContent = selectedText;
|
|
997
|
+
|
|
998
|
+
if (!selectedClusterId) {
|
|
999
|
+
// 显示全网统计
|
|
1000
|
+
statsTitle.textContent = '网络统计';
|
|
1001
|
+
hivtrace.graphSummary(user_graph, graph_summary_tag);
|
|
1002
|
+
hivtrace.histogramDistances(graph, histogram_tag, histogram_label);
|
|
1003
|
+
} else {
|
|
1004
|
+
// 显示单个 cluster 的统计
|
|
1005
|
+
var cluster = clusters.find(function (c) {
|
|
1006
|
+
return String(c.cluster_id) === String(selectedClusterId);
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
if (cluster) {
|
|
1010
|
+
statsTitle.textContent = '簇 ' + selectedClusterId + ' 统计';
|
|
1011
|
+
updateClusterStatistics(cluster);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
// 计算并显示单个 cluster 的统计
|
|
1019
|
+
function updateClusterStatistics(cluster) {
|
|
1020
|
+
var table = d3.select(graph_summary_tag);
|
|
1021
|
+
var tbody = table.select("tbody");
|
|
1022
|
+
if (tbody.empty()) {
|
|
1023
|
+
tbody = table.append("tbody");
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// 使用 cluster 对象已有的预计算统计信息(由 clusternetwork.js 计算)
|
|
1027
|
+
// cluster.degrees: 节点度分布统计 (mean, median, min, max, Q1, Q3)
|
|
1028
|
+
// cluster.distances: 边长度分布统计 (mean, median, min, max, Q1, Q3)
|
|
1029
|
+
var degreeStats = cluster.degrees || { mean: 0, median: 0, min: 0, max: 0, Q1: 0, Q3: 0 };
|
|
1030
|
+
var distanceStats = cluster.distances || { mean: 0, median: 0, min: 0, max: 0, Q1: 0, Q3: 0 };
|
|
1031
|
+
|
|
1032
|
+
// 节点数
|
|
1033
|
+
var nodeCount = cluster.children ? cluster.children.length : 0;
|
|
1034
|
+
|
|
1035
|
+
// 边数计算:度数总和 / 2(每条边贡献两个度)
|
|
1036
|
+
// 从预计算的统计反推:总度数 ≈ mean * nodeCount
|
|
1037
|
+
var totalDegree = (degreeStats.mean || 0) * nodeCount;
|
|
1038
|
+
var edgeCount = Math.round(totalDegree / 2);
|
|
1039
|
+
|
|
1040
|
+
var floatFormat = d3.format(",.2r");
|
|
1041
|
+
var percentFormat = d3.format(",.3p");
|
|
1042
|
+
|
|
1043
|
+
// 构建表格数据
|
|
1044
|
+
var tableData = [
|
|
1045
|
+
['节点', nodeCount],
|
|
1046
|
+
['边', edgeCount],
|
|
1047
|
+
['每节点链接数', ''],
|
|
1048
|
+
[' <i>平均值</i>', floatFormat(degreeStats.mean || 0)],
|
|
1049
|
+
[' <i>中位数</i>', floatFormat(degreeStats.median || 0)],
|
|
1050
|
+
[' <i>范围</i>', (degreeStats.min || 0) + ' - ' + (degreeStats.max || 0)],
|
|
1051
|
+
[' <i>四分位距</i>', (degreeStats.Q1 || 0) + ' - ' + (degreeStats.Q3 || 0)]
|
|
1052
|
+
];
|
|
1053
|
+
|
|
1054
|
+
// 添加遗传距离统计(使用预计算的 distances)
|
|
1055
|
+
if (distanceStats && distanceStats.mean !== null && distanceStats.mean !== undefined) {
|
|
1056
|
+
tableData.push(['链接节点间的遗传距离', '']);
|
|
1057
|
+
tableData.push([' <i>平均值</i>', percentFormat(distanceStats.mean)]);
|
|
1058
|
+
tableData.push([' <i>中位数</i>', percentFormat(distanceStats.median)]);
|
|
1059
|
+
tableData.push([' <i>范围</i>', percentFormat(distanceStats.min) + ' - ' + percentFormat(distanceStats.max)]);
|
|
1060
|
+
tableData.push([' <i>四分位距</i>', percentFormat(distanceStats.Q1) + ' - ' + percentFormat(distanceStats.Q3)]);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// 更新表格
|
|
1064
|
+
var rows = tbody.selectAll("tr").data(tableData);
|
|
1065
|
+
rows.enter().append("tr");
|
|
1066
|
+
rows.exit().remove();
|
|
1067
|
+
var columns = rows.selectAll("td").data(function (d) { return d; });
|
|
1068
|
+
columns.enter().append("td");
|
|
1069
|
+
columns.exit().remove();
|
|
1070
|
+
columns.html(function (d) { return d; });
|
|
1071
|
+
|
|
1072
|
+
// 先清除之前的直方图内容
|
|
1073
|
+
d3.select(histogram_tag).selectAll("*").remove();
|
|
1074
|
+
d3.select(histogram_label).text('');
|
|
1075
|
+
|
|
1076
|
+
// 为 cluster 生成直方图
|
|
1077
|
+
// 从 user_graph.edges 提取属于该 cluster 的边
|
|
1078
|
+
var clusterNodeIds = new Set(cluster.children.map(function (n) { return n.id; }));
|
|
1079
|
+
var clusterEdges = [];
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
// 遍历所有边,找到属于该 cluster 的边
|
|
1083
|
+
if (user_graph.edges && user_graph.edges.length > 0) {
|
|
1084
|
+
user_graph.edges.forEach(function (e) {
|
|
1085
|
+
// 边的 source 和 target 可能是对象或索引
|
|
1086
|
+
var sourceNode = typeof e.source === 'object' ? e.source : user_graph.nodes[e.source];
|
|
1087
|
+
var targetNode = typeof e.target === 'object' ? e.target : user_graph.nodes[e.target];
|
|
1088
|
+
|
|
1089
|
+
if (sourceNode && targetNode) {
|
|
1090
|
+
var sourceId = sourceNode.id;
|
|
1091
|
+
var targetId = targetNode.id;
|
|
1092
|
+
|
|
1093
|
+
if (clusterNodeIds.has(sourceId) && clusterNodeIds.has(targetId)) {
|
|
1094
|
+
clusterEdges.push({ length: e.length });
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// 如果有足够边数据,显示直方图;否则显示提示
|
|
1101
|
+
// 直方图至少需要 3 条边才能有意义地显示分布
|
|
1102
|
+
if (clusterEdges.length >= 3) {
|
|
1103
|
+
var clusterGraphData = { Edges: clusterEdges };
|
|
1104
|
+
hivtrace.histogramDistances(clusterGraphData, histogram_tag, histogram_label);
|
|
1105
|
+
} else {
|
|
1106
|
+
// 边数太少,显示提示
|
|
1107
|
+
d3.select(histogram_tag).selectAll("*").remove();
|
|
1108
|
+
d3.select(histogram_label).text('簇 ' + cluster.cluster_id + ' 遗传距离分布');
|
|
1109
|
+
|
|
1110
|
+
var message = clusterEdges.length === 0
|
|
1111
|
+
? "暂无直方图数据"
|
|
1112
|
+
: "边数过少(" + clusterEdges.length + " 条),无法生成直方图";
|
|
1113
|
+
|
|
1114
|
+
d3.select(histogram_tag).append("p")
|
|
1115
|
+
.style("color", "#6b7280")
|
|
1116
|
+
.style("text-align", "center")
|
|
1117
|
+
.style("padding", "40px 0")
|
|
1118
|
+
.text(message);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
}
|
|
1122
|
+
})();
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
|
|
873
1128
|
// 可配置的追踪簇回调功能 - 基于传入参数决定是否启用
|
|
874
1129
|
if (enableClusterTracking) {
|
|
875
1130
|
user_graph.onTrackCluster = function (clusterInfo, allData) {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"collapse_filtered": "Collapse filtered",
|
|
10
10
|
"expand_all": "Expand all",
|
|
11
11
|
"expand_filtered": "Expand filtered",
|
|
12
|
-
"export_colors": "
|
|
12
|
+
"export_colors": "Copy color scheme",
|
|
13
13
|
"fix_all_objects_in_place": "Fix all objects in place",
|
|
14
14
|
"reset_layout": "Reset layout",
|
|
15
15
|
"expand_cluster": "Expand cluster",
|
|
@@ -74,7 +74,10 @@
|
|
|
74
74
|
"sequences": "Sequences",
|
|
75
75
|
"individuals": "Individuals",
|
|
76
76
|
"sequences_used_to_make_links": "Sequences used to make links",
|
|
77
|
-
"singletons": "Singletons"
|
|
77
|
+
"singletons": "Singletons",
|
|
78
|
+
"select_cluster": "Select Cluster",
|
|
79
|
+
"all_network": "Overview",
|
|
80
|
+
"cluster_statistics": "Cluster Statistics"
|
|
78
81
|
},
|
|
79
82
|
"tabs": {
|
|
80
83
|
"network": "Network",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"collapse_filtered": "折叠已过滤",
|
|
10
10
|
"expand_all": "展开全部",
|
|
11
11
|
"expand_filtered": "展开已过滤",
|
|
12
|
-
"export_colors": "
|
|
12
|
+
"export_colors": "复制配色方案",
|
|
13
13
|
"fix_all_objects_in_place": "固定所有对象位置",
|
|
14
14
|
"reset_layout": "重置布局",
|
|
15
15
|
"expand_cluster": "展开簇",
|
|
@@ -74,7 +74,10 @@
|
|
|
74
74
|
"sequences": "序列",
|
|
75
75
|
"individuals": "个体",
|
|
76
76
|
"sequences_used_to_make_links": "用于建立链接的序列数",
|
|
77
|
-
"singletons": "单节点"
|
|
77
|
+
"singletons": "单节点",
|
|
78
|
+
"select_cluster": "选择簇",
|
|
79
|
+
"all_network": "总览",
|
|
80
|
+
"cluster_statistics": "簇统计"
|
|
78
81
|
},
|
|
79
82
|
"tabs": {
|
|
80
83
|
"network": "网络",
|