@basic-genomics/hivtrace-viz 1.1.8 → 1.1.9

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,11 +147,14 @@
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;
150
151
  }
151
152
 
152
- .hivtrace-fullscreen-btn button:hover {
153
+ .hivtrace-fullscreen-btn button:hover,
154
+ .hivtrace-fullscreen-btn button:focus {
153
155
  background: #f4f4f5;
154
156
  color: #18181b;
157
+ text-decoration: none;
155
158
  }
156
159
 
157
160
  /* 状态栏 badge 和 label 样式优化 */
@@ -424,6 +427,41 @@
424
427
  }
425
428
  }
426
429
 
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 {
443
+ flex-direction: row;
444
+ }
445
+ }
446
+
447
+ /* 统计面板样式 */
448
+ .hivtrace-stats-panel {
449
+ 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
+
427
465
  /* 可视化面板 */
428
466
  .hivtrace-viz-panel {
429
467
  flex-shrink: 0;
@@ -641,9 +679,6 @@
641
679
  <li class="active" id="main-tab">
642
680
  <a id="trace-default-tab" href="#trace-results" data-toggle="tab">网络</a>
643
681
  </li>
644
- <li class="disabled" id="graph-tab">
645
- <a href="#trace-graph" data-toggle="tab">统计</a>
646
- </li>
647
682
  <li class="disabled" id="clusters-tab">
648
683
  <a href="#trace-clusters" data-toggle="tab">簇</a>
649
684
  </li>
@@ -823,42 +858,6 @@
823
858
  </div>
824
859
  </div>
825
860
 
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
-
860
-
861
-
862
861
  <div id="trace-clusters" class="tab-pane">
863
862
  <div class="row">
864
863
  <div class="col-lg-12">
@@ -907,7 +906,20 @@
907
906
  <div class="hivtrace-pill-row" data-hivtrace-ui-role="attributes_cat"></div>
908
907
  </div>
909
908
 
910
- <!-- 下方:左侧图表 + 右侧表格 -->
909
+ <!-- 概览统计内容(选中概览 pill 时显示,默认隐藏) -->
910
+ <div class="hivtrace-overview-stats" id="attrs_stats_content" style="display: none;">
911
+ <div class="hivtrace-stats-panel">
912
+ <p class="lead" id="attrs_stats_title">网络统计</p>
913
+ <table class="table table-striped table-condensed table-responsive" id="attrs_graph_summary_table">
914
+ </table>
915
+ </div>
916
+ <div class="hivtrace-stats-panel">
917
+ <p id="attrs_histogram_label" class="lead"></p>
918
+ <div id="attrs_histogram_tag"></div>
919
+ </div>
920
+ </div>
921
+
922
+ <!-- 属性内容(选中属性 pill 时显示) -->
911
923
  <div class="hivtrace-attributes-content">
912
924
  <!-- 左侧:可视化区域(弦图/散点图) -->
913
925
  <div class="hivtrace-viz-panel" data-hivtrace-ui-role="aux_svg_holder_enclosed" style="display: none">
@@ -1078,168 +1090,111 @@
1078
1090
  "This network contains no clusters and cannot be displayed"
1079
1091
  );
1080
1092
  } else {
1081
- hivtrace.histogramDistances(graph, histogram_tag, histogram_label);
1082
- hivtrace.graphSummary(user_graph, graph_summary_tag);
1083
-
1084
- // 初始化 cluster 选择器 (Bootstrap dropdown)
1085
- (function initClusterSelector() {
1086
- var menu = document.getElementById('stats_cluster_selector_menu');
1087
- var label = document.getElementById('stats_cluster_selector_label');
1088
- if (!menu || !label) return;
1089
-
1090
- // 获取所有 cluster 并按 ID 排序
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';
1091
1102
  var clusters = user_graph.clusters || [];
1092
- clusters = clusters.slice().sort(function (a, b) {
1093
- return a.cluster_id - b.cluster_id;
1094
- });
1095
-
1096
- // 填充 cluster 选项到 dropdown menu
1097
- clusters.forEach(function (cluster) {
1098
- var li = document.createElement('li');
1099
- var a = document.createElement('a');
1100
- a.href = '#';
1101
- a.setAttribute('data-value', cluster.cluster_id);
1102
- a.textContent = '簇 ' + cluster.cluster_id + ' (' + cluster.children.length + ' 节点)';
1103
- li.appendChild(a);
1104
- menu.appendChild(li);
1105
- });
1106
-
1107
- // 监听 dropdown 菜单项点击
1108
- menu.addEventListener('click', function (e) {
1109
- if (e.target.tagName === 'A') {
1110
- e.preventDefault();
1111
- var selectedClusterId = e.target.getAttribute('data-value');
1112
- var selectedText = e.target.textContent;
1113
- var statsTitle = document.getElementById('stats_title');
1114
-
1115
- // 更新按钮文本
1116
- label.textContent = selectedText;
1117
-
1118
- if (!selectedClusterId) {
1119
- // 显示全网统计
1120
- statsTitle.textContent = '网络统计';
1121
- hivtrace.graphSummary(user_graph, graph_summary_tag);
1122
- hivtrace.histogramDistances(graph, histogram_tag, histogram_label);
1123
- } else {
1124
- // 显示单个 cluster 的统计
1125
- var cluster = clusters.find(function (c) {
1126
- return String(c.cluster_id) === String(selectedClusterId);
1127
- });
1128
1103
 
1129
- if (cluster) {
1130
- statsTitle.textContent = '簇 ' + selectedClusterId + ' 统计';
1131
- updateClusterStatistics(cluster);
1132
- }
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");
1133
1123
  }
1134
- }
1135
- });
1136
-
1137
1124
 
1138
- // 计算并显示单个 cluster 的统计
1139
- function updateClusterStatistics(cluster) {
1140
- var table = d3.select(graph_summary_tag);
1141
- var tbody = table.select("tbody");
1142
- if (tbody.empty()) {
1143
- tbody = table.append("tbody");
1144
- }
1145
-
1146
- // 使用 cluster 对象已有的预计算统计信息(由 clusternetwork.js 计算)
1147
- // cluster.degrees: 节点度分布统计 (mean, median, min, max, Q1, Q3)
1148
- // cluster.distances: 边长度分布统计 (mean, median, min, max, Q1, Q3)
1149
- var degreeStats = cluster.degrees || { mean: 0, median: 0, min: 0, max: 0, Q1: 0, Q3: 0 };
1150
- var distanceStats = cluster.distances || { mean: 0, median: 0, min: 0, max: 0, Q1: 0, Q3: 0 };
1151
-
1152
- // 节点数
1153
- var nodeCount = cluster.children ? cluster.children.length : 0;
1154
-
1155
- // 边数计算:度数总和 / 2(每条边贡献两个度)
1156
- // 从预计算的统计反推:总度数 ≈ mean * nodeCount
1157
- var totalDegree = (degreeStats.mean || 0) * nodeCount;
1158
- var edgeCount = Math.round(totalDegree / 2);
1159
-
1160
- var floatFormat = d3.format(",.2r");
1161
- var percentFormat = d3.format(",.3p");
1162
-
1163
- // 构建表格数据
1164
- var tableData = [
1165
- ['节点', nodeCount],
1166
- ['边', edgeCount],
1167
- ['每节点链接数', ''],
1168
- ['&nbsp;&nbsp;<i>平均值</i>', floatFormat(degreeStats.mean || 0)],
1169
- ['&nbsp;&nbsp;<i>中位数</i>', floatFormat(degreeStats.median || 0)],
1170
- ['&nbsp;&nbsp;<i>范围</i>', (degreeStats.min || 0) + ' - ' + (degreeStats.max || 0)],
1171
- ['&nbsp;&nbsp;<i>四分位距</i>', (degreeStats.Q1 || 0) + ' - ' + (degreeStats.Q3 || 0)]
1172
- ];
1173
-
1174
- // 添加遗传距离统计(使用预计算的 distances)
1175
- if (distanceStats && distanceStats.mean !== null && distanceStats.mean !== undefined) {
1176
- tableData.push(['链接节点间的遗传距离', '']);
1177
- tableData.push(['&nbsp;&nbsp;<i>平均值</i>', percentFormat(distanceStats.mean)]);
1178
- tableData.push(['&nbsp;&nbsp;<i>中位数</i>', percentFormat(distanceStats.median)]);
1179
- tableData.push(['&nbsp;&nbsp;<i>范围</i>', percentFormat(distanceStats.min) + ' - ' + percentFormat(distanceStats.max)]);
1180
- tableData.push(['&nbsp;&nbsp;<i>四分位距</i>', percentFormat(distanceStats.Q1) + ' - ' + percentFormat(distanceStats.Q3)]);
1181
- }
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
+ }
1182
1151
 
1183
- // 更新表格
1184
- var rows = tbody.selectAll("tr").data(tableData);
1185
- rows.enter().append("tr");
1186
- rows.exit().remove();
1187
- var columns = rows.selectAll("td").data(function (d) { return d; });
1188
- columns.enter().append("td");
1189
- columns.exit().remove();
1190
- columns.html(function (d) { return d; });
1191
-
1192
- // 先清除之前的直方图内容
1193
- d3.select(histogram_tag).selectAll("*").remove();
1194
- d3.select(histogram_label).text('');
1195
-
1196
- // cluster 生成直方图
1197
- // 从 user_graph.edges 提取属于该 cluster 的边
1198
- var clusterNodeIds = new Set(cluster.children.map(function (n) { return n.id; }));
1199
- var clusterEdges = [];
1200
-
1201
-
1202
- // 遍历所有边,找到属于该 cluster 的边
1203
- if (user_graph.edges && user_graph.edges.length > 0) {
1204
- user_graph.edges.forEach(function (e) {
1205
- // 边的 source target 可能是对象或索引
1206
- var sourceNode = typeof e.source === 'object' ? e.source : user_graph.nodes[e.source];
1207
- var targetNode = typeof e.target === 'object' ? e.target : user_graph.nodes[e.target];
1208
-
1209
- if (sourceNode && targetNode) {
1210
- var sourceId = sourceNode.id;
1211
- var targetId = targetNode.id;
1212
-
1213
- if (clusterNodeIds.has(sourceId) && clusterNodeIds.has(targetId)) {
1214
- clusterEdges.push({ length: e.length });
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
+ }
1215
1177
  }
1216
- }
1217
- });
1218
- }
1178
+ });
1179
+ }
1219
1180
 
1220
- // 如果有足够边数据,显示直方图;否则显示提示
1221
- // 直方图至少需要 3 条边才能有意义地显示分布
1222
- if (clusterEdges.length >= 3) {
1223
- var clusterGraphData = { Edges: clusterEdges };
1224
- hivtrace.histogramDistances(clusterGraphData, histogram_tag, histogram_label);
1225
- } else {
1226
- // 边数太少,显示提示
1227
- d3.select(histogram_tag).selectAll("*").remove();
1228
- d3.select(histogram_label).text('簇 ' + cluster.cluster_id + ' 遗传距离分布');
1229
-
1230
- var message = clusterEdges.length === 0
1231
- ? "暂无直方图数据"
1232
- : "边数过少(" + clusterEdges.length + " 条),无法生成直方图";
1233
-
1234
- d3.select(histogram_tag).append("p")
1235
- .style("color", "#6b7280")
1236
- .style("text-align", "center")
1237
- .style("padding", "40px 0")
1238
- .text(message);
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
+ }
1239
1195
  }
1240
-
1241
1196
  }
1242
- })();
1197
+ };
1243
1198
 
1244
1199
  // 初始化属性标签页的 cluster 选择器
1245
1200
  (function initAttrsClusterSelector() {
@@ -1282,7 +1237,10 @@
1282
1237
 
1283
1238
  // 重新触发当前选中的属性(支持分类属性和连续属性)
1284
1239
  if (user_graph.colorizer && user_graph.colorizer['category_id']) {
1285
- if (user_graph.colorizer['continuous']) {
1240
+ if (user_graph.colorizer['category_id'] === '_overview') {
1241
+ // 概览模式 - 更新统计内容
1242
+ window.renderOverviewStats(selectedClusterId || null);
1243
+ } else if (user_graph.colorizer['continuous']) {
1286
1244
  // 连续属性 - 散点图
1287
1245
  user_graph.handle_attribute_continuous(user_graph.colorizer['category_id']);
1288
1246
  } else {
@@ -1298,7 +1256,6 @@
1298
1256
 
1299
1257
  [
1300
1258
  "#main-tab",
1301
- "#graph-tab",
1302
1259
  "#clusters-tab",
1303
1260
  "#subclusters-tab",
1304
1261
  "#nodes-tab",
@@ -1349,6 +1306,26 @@
1349
1306
  }
1350
1307
  );
1351
1308
 
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
+
1352
1329
  }
1353
1330
  document
1354
1331
  .getElementById("min_cluster_size_input")