@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.
@@ -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: 8px 16px;
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="row">
715
- <div class="col-lg-6">
716
- <p class="lead">网络统计</p>
717
- <table class="table table-striped table-condensed table-responsive" id="graph_summary_table"></table>
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
- <div class="col-lg-6">
720
- <p id="histogram_label" class="lead"></p>
721
- <div class="row" id="histogram_tag" style="margin-left: 5px"></div>
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
- <table class="table table-striped table-condensed table-hover" id="cluster_table"></table>
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
- <table class="table table-striped table-condensed table-hover" id="node_table"></table>
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
+ ['&nbsp;&nbsp;<i>平均值</i>', floatFormat(degreeStats.mean || 0)],
1049
+ ['&nbsp;&nbsp;<i>中位数</i>', floatFormat(degreeStats.median || 0)],
1050
+ ['&nbsp;&nbsp;<i>范围</i>', (degreeStats.min || 0) + ' - ' + (degreeStats.max || 0)],
1051
+ ['&nbsp;&nbsp;<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(['&nbsp;&nbsp;<i>平均值</i>', percentFormat(distanceStats.mean)]);
1058
+ tableData.push(['&nbsp;&nbsp;<i>中位数</i>', percentFormat(distanceStats.median)]);
1059
+ tableData.push(['&nbsp;&nbsp;<i>范围</i>', percentFormat(distanceStats.min) + ' - ' + percentFormat(distanceStats.max)]);
1060
+ tableData.push(['&nbsp;&nbsp;<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": "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": "网络",