@basic-genomics/hivtrace-viz 1.2.6 → 1.4.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/README.md +62 -0
- package/dist/embed/hivtrace.js +1 -1
- package/dist/embed/hivtrace.js.LICENSE.txt +10 -0
- package/dist/embed/hivtrace.js.map +1 -1
- package/dist/embed/index.html +703 -35
- package/dist/embed/locales/zh-CN.json +1 -1
- package/dist/hivtrace.js +1 -1
- package/dist/hivtrace.js.LICENSE.txt +10 -0
- package/dist/hivtrace.js.map +1 -1
- package/dist/react/HivtraceViz.d.ts +1 -1
- package/dist/react/HivtraceViz.d.ts.map +1 -1
- package/dist/react/HivtraceViz.js +11 -4
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/types.d.ts +50 -0
- package/dist/react/types.d.ts.map +1 -1
- package/package.json +2 -1
package/dist/embed/index.html
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
<link href="./hivtrace.css" rel="stylesheet" />
|
|
12
12
|
<link rel="shortcut icon" href="#" />
|
|
13
13
|
|
|
14
|
+
<!-- Cytoscape.js is bundled in hivtrace.js -->
|
|
15
|
+
|
|
14
16
|
<style>
|
|
15
17
|
html,
|
|
16
18
|
body {
|
|
@@ -160,6 +162,30 @@
|
|
|
160
162
|
text-decoration: none;
|
|
161
163
|
}
|
|
162
164
|
|
|
165
|
+
/* 自定义状态栏按钮样式 */
|
|
166
|
+
.hivtrace-custom-buttons button {
|
|
167
|
+
display: inline-flex;
|
|
168
|
+
align-items: center;
|
|
169
|
+
justify-content: center;
|
|
170
|
+
gap: 6px;
|
|
171
|
+
padding: 6px 12px;
|
|
172
|
+
border: 1px solid #e2e8f0;
|
|
173
|
+
border-radius: 6px;
|
|
174
|
+
background: #fff;
|
|
175
|
+
color: #374151;
|
|
176
|
+
font-size: 13px;
|
|
177
|
+
cursor: pointer;
|
|
178
|
+
transition: background 0.15s, color 0.15s;
|
|
179
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.hivtrace-custom-buttons button:hover,
|
|
183
|
+
.hivtrace-custom-buttons button:focus {
|
|
184
|
+
background: #f4f4f5;
|
|
185
|
+
color: #18181b;
|
|
186
|
+
text-decoration: none;
|
|
187
|
+
}
|
|
188
|
+
|
|
163
189
|
/* 状态栏 badge 和 label 样式优化 */
|
|
164
190
|
.hivtrace-status-bar .badge {
|
|
165
191
|
display: inline-flex;
|
|
@@ -638,6 +664,9 @@
|
|
|
638
664
|
<li class="disabled" id="attributes-tab">
|
|
639
665
|
<a href="#trace-attributes" data-toggle="tab">统计</a>
|
|
640
666
|
</li>
|
|
667
|
+
<li class="disabled" id="tree-tab" style="display: none;">
|
|
668
|
+
<a href="#trace-tree" data-toggle="tab">进化树</a>
|
|
669
|
+
</li>
|
|
641
670
|
</ul>
|
|
642
671
|
|
|
643
672
|
<div class="tab-content" id="top_level_tab_content">
|
|
@@ -843,17 +872,29 @@
|
|
|
843
872
|
<!-- 簇选择器 -->
|
|
844
873
|
<div style="margin-bottom: 12px;">
|
|
845
874
|
<div class="input-group input-group-sm" style="max-width: 280px;">
|
|
846
|
-
<span class="input-group-
|
|
847
|
-
|
|
875
|
+
<span class="input-group-btn" style="width: auto;">
|
|
876
|
+
<button type="button" class="btn btn-default" id="attrs_cluster_prev_btn" title="上一个簇" disabled
|
|
877
|
+
style="border-top-right-radius: 0; border-bottom-right-radius: 0;">
|
|
878
|
+
<i class="fa fa-chevron-left"></i>
|
|
879
|
+
</button>
|
|
880
|
+
</span>
|
|
881
|
+
<div class="input-group-btn" style="width: auto;">
|
|
848
882
|
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"
|
|
849
|
-
id="attrs_cluster_selector_btn"
|
|
850
|
-
|
|
883
|
+
id="attrs_cluster_selector_btn"
|
|
884
|
+
style="min-width: 120px; text-align: left; border-radius: 0; margin-left: -1px;">
|
|
885
|
+
<span id="attrs_cluster_selector_label">所有簇</span> <span class="caret"
|
|
851
886
|
style="float: right; margin-top: 8px;"></span>
|
|
852
887
|
</button>
|
|
853
888
|
<ul class="dropdown-menu" role="menu" id="attrs_cluster_selector_menu">
|
|
854
|
-
<li><a href="#" data-value=""
|
|
889
|
+
<li><a href="#" data-value="">所有簇</a></li>
|
|
855
890
|
</ul>
|
|
856
891
|
</div>
|
|
892
|
+
<span class="input-group-btn" style="width: auto;">
|
|
893
|
+
<button type="button" class="btn btn-default" id="attrs_cluster_next_btn" title="下一个簇"
|
|
894
|
+
style="border-top-left-radius: 0; border-bottom-left-radius: 0; margin-left: -1px;">
|
|
895
|
+
<i class="fa fa-chevron-right"></i>
|
|
896
|
+
</button>
|
|
897
|
+
</span>
|
|
857
898
|
</div>
|
|
858
899
|
</div>
|
|
859
900
|
<!-- 属性 Pills -->
|
|
@@ -906,6 +947,101 @@
|
|
|
906
947
|
</div>
|
|
907
948
|
</div>
|
|
908
949
|
</div>
|
|
950
|
+
|
|
951
|
+
<!-- 进化树 Tab -->
|
|
952
|
+
<div id="trace-tree" class="tab-pane">
|
|
953
|
+
<div class="hivtrace-tree-container"
|
|
954
|
+
style="display: flex; flex-direction: column; height: 100%; padding: 12px;">
|
|
955
|
+
<!-- 进化树工具栏 -->
|
|
956
|
+
<div class="hivtrace-tree-toolbar"
|
|
957
|
+
style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px; flex-wrap: wrap;">
|
|
958
|
+
<style>
|
|
959
|
+
.hivtrace-tree-toolbar>* {
|
|
960
|
+
flex-shrink: 0;
|
|
961
|
+
}
|
|
962
|
+
</style>
|
|
963
|
+
<!-- 适应窗口 -->
|
|
964
|
+
<button type="button" class="btn btn-default btn-sm" id="tree_fit_btn" title="适应窗口">
|
|
965
|
+
<i class="fa fa-expand"></i> 适应窗口
|
|
966
|
+
</button>
|
|
967
|
+
<!-- 显示/隐藏标签 -->
|
|
968
|
+
<button type="button" class="btn btn-default btn-sm" id="tree_labels_btn" title="显示/隐藏标签">
|
|
969
|
+
<i class="fa fa-tag"></i> <span id="tree_labels_text">显示标签</span>
|
|
970
|
+
</button>
|
|
971
|
+
<!-- 簇选择器 -->
|
|
972
|
+
<div class="input-group input-group-sm" style="max-width: 320px;">
|
|
973
|
+
<span class="input-group-btn" style="width: auto;">
|
|
974
|
+
<button type="button" class="btn btn-default" id="tree_cluster_prev_btn" title="上一个簇" disabled
|
|
975
|
+
style="border-top-right-radius: 0; border-bottom-right-radius: 0;">
|
|
976
|
+
<i class="fa fa-chevron-left"></i>
|
|
977
|
+
</button>
|
|
978
|
+
</span>
|
|
979
|
+
<div class="input-group-btn" style="width: auto;">
|
|
980
|
+
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"
|
|
981
|
+
id="tree_cluster_selector_btn"
|
|
982
|
+
style="min-width: 140px; text-align: left; border-radius: 0; margin-left: -1px;">
|
|
983
|
+
<span id="tree_cluster_selector_label">所有簇</span> <span class="caret"
|
|
984
|
+
style="float: right; margin-top: 8px;"></span>
|
|
985
|
+
</button>
|
|
986
|
+
<ul class="dropdown-menu" role="menu" id="tree_cluster_selector_menu">
|
|
987
|
+
<li><a href="#" data-value="">所有簇</a></li>
|
|
988
|
+
</ul>
|
|
989
|
+
</div>
|
|
990
|
+
<span class="input-group-btn" style="width: auto;">
|
|
991
|
+
<button type="button" class="btn btn-default" id="tree_cluster_next_btn" title="下一个簇"
|
|
992
|
+
style="border-top-left-radius: 0; border-bottom-left-radius: 0; margin-left: -1px;">
|
|
993
|
+
<i class="fa fa-chevron-right"></i>
|
|
994
|
+
</button>
|
|
995
|
+
</span>
|
|
996
|
+
</div>
|
|
997
|
+
<!-- 导出按钮 -->
|
|
998
|
+
<div class="dropdown" style="display: inline-block; position: relative;">
|
|
999
|
+
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown"
|
|
1000
|
+
id="tree_export_btn">
|
|
1001
|
+
<i class="fa fa-download"></i> 导出 <span class="caret"></span>
|
|
1002
|
+
</button>
|
|
1003
|
+
<ul class="dropdown-menu" role="menu">
|
|
1004
|
+
<li><a href="#" id="tree_export_png">PNG 图片</a></li>
|
|
1005
|
+
<li><a href="#" id="tree_export_json">JSON 数据</a></li>
|
|
1006
|
+
</ul>
|
|
1007
|
+
</div>
|
|
1008
|
+
<!-- 动画播放按钮 -->
|
|
1009
|
+
<button type="button" class="btn btn-default btn-sm" id="tree_animate_btn" title="播放从根到叶子的动画"
|
|
1010
|
+
style="margin-left: 8px;">
|
|
1011
|
+
<i class="fa fa-play"></i> 播放动画
|
|
1012
|
+
</button>
|
|
1013
|
+
<!-- 统计信息 -->
|
|
1014
|
+
<div style="flex: 1;"></div>
|
|
1015
|
+
<div class="hivtrace-tree-stats" style="display: flex; gap: 12px; font-size: 13px; color: #6b7280;">
|
|
1016
|
+
<span>节点: <strong id="tree_stat_nodes">0</strong></span>
|
|
1017
|
+
<span>叶节点: <strong id="tree_stat_leaves">0</strong></span>
|
|
1018
|
+
<span>连边: <strong id="tree_stat_edges">0</strong></span>
|
|
1019
|
+
</div>
|
|
1020
|
+
</div>
|
|
1021
|
+
<!-- 进化树可视化容器 -->
|
|
1022
|
+
<div id="tree_container"
|
|
1023
|
+
style="flex: 1; background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; position: relative; min-height: 400px;">
|
|
1024
|
+
</div>
|
|
1025
|
+
<!-- 节点信息面板 -->
|
|
1026
|
+
<div id="tree_node_info" class="hivtrace-tree-node-info"
|
|
1027
|
+
style="display: none; position: absolute; bottom: 20px; right: 20px; width: 240px; background: rgba(255,255,255,0.95); border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); z-index: 100;">
|
|
1028
|
+
<h4 style="margin: 0 0 8px; font-size: 14px; font-weight: 600;">节点信息</h4>
|
|
1029
|
+
<div style="font-size: 12px; color: #374151;">
|
|
1030
|
+
<div style="display: flex; justify-content: space-between; padding: 4px 0;"><span
|
|
1031
|
+
style="color: #6b7280;">ID:</span> <span id="tree_node_id"
|
|
1032
|
+
style="font-family: monospace;">-</span></div>
|
|
1033
|
+
<div style="display: flex; justify-content: space-between; padding: 4px 0;"><span
|
|
1034
|
+
style="color: #6b7280;">标签:</span> <span id="tree_node_label">-</span></div>
|
|
1035
|
+
<div style="display: flex; justify-content: space-between; padding: 4px 0;"><span
|
|
1036
|
+
style="color: #6b7280;">类型:</span> <span id="tree_node_type">-</span></div>
|
|
1037
|
+
<div style="display: flex; justify-content: space-between; padding: 4px 0;"><span
|
|
1038
|
+
style="color: #6b7280;">分支长度:</span> <span id="tree_node_branch">-</span></div>
|
|
1039
|
+
<div style="display: flex; justify-content: space-between; padding: 4px 0;"><span
|
|
1040
|
+
style="color: #6b7280;">连接数:</span> <span id="tree_node_degree">-</span></div>
|
|
1041
|
+
</div>
|
|
1042
|
+
</div>
|
|
1043
|
+
</div>
|
|
1044
|
+
</div>
|
|
909
1045
|
</div>
|
|
910
1046
|
</div>
|
|
911
1047
|
</div>
|
|
@@ -917,6 +1053,192 @@
|
|
|
917
1053
|
|
|
918
1054
|
<script src="./hivtrace.js"></script>
|
|
919
1055
|
<!-- <script src="dist/hivtrace.es.js"></script> -->
|
|
1056
|
+
|
|
1057
|
+
<!-- Tree Visualization Configuration -->
|
|
1058
|
+
<script>
|
|
1059
|
+
// Cytoscape Stylesheet for Tree
|
|
1060
|
+
window.TREE_STYLESHEET = [
|
|
1061
|
+
// 基础节点样式 - 添加 transition 支持平滑动画(官方最佳实践)
|
|
1062
|
+
{
|
|
1063
|
+
selector: 'node', style: {
|
|
1064
|
+
'font-size': 11, 'text-valign': 'center', 'text-halign': 'right', 'text-margin-x': 5, 'color': '#333', 'min-zoomed-font-size': 8,
|
|
1065
|
+
'transition-property': 'background-color, border-color, border-width, width, height, opacity',
|
|
1066
|
+
'transition-duration': '0.25s',
|
|
1067
|
+
'transition-timing-function': 'ease-out'
|
|
1068
|
+
}
|
|
1069
|
+
},
|
|
1070
|
+
{ selector: 'node[?is_leaf]', style: { 'background-color': '#2563eb', 'width': 10, 'height': 10 } },
|
|
1071
|
+
{ selector: 'node[!is_leaf]', style: { 'background-color': '#CBD5E1', 'width': 4, 'height': 4, 'opacity': 0.7, 'label': '', 'border-width': 0 } },
|
|
1072
|
+
{ selector: 'node[!is_leaf][support]', style: { 'background-color': '#94A3B8', 'width': 5, 'height': 5, 'opacity': 0.8 } },
|
|
1073
|
+
// 根节点样式 - 青绿色,与蓝色和谐但有区分
|
|
1074
|
+
{
|
|
1075
|
+
selector: 'node.tree-root', style: {
|
|
1076
|
+
'background-color': '#0d9488', // 青绿色 teal-600
|
|
1077
|
+
'shape': 'ellipse',
|
|
1078
|
+
'width': 12,
|
|
1079
|
+
'height': 12,
|
|
1080
|
+
'opacity': 1,
|
|
1081
|
+
'border-width': 2,
|
|
1082
|
+
'border-color': '#0f766e' // 更深的青绿边框
|
|
1083
|
+
}
|
|
1084
|
+
},
|
|
1085
|
+
// 基础边样式 - 添加 transition 支持
|
|
1086
|
+
{
|
|
1087
|
+
selector: 'edge', style: {
|
|
1088
|
+
'width': 2, 'line-color': '#333', 'target-arrow-color': '#333', 'curve-style': 'bezier', 'opacity': 0.8,
|
|
1089
|
+
'transition-property': 'line-color, width, opacity',
|
|
1090
|
+
'transition-duration': '0.25s',
|
|
1091
|
+
'transition-timing-function': 'ease-out'
|
|
1092
|
+
}
|
|
1093
|
+
},
|
|
1094
|
+
{ selector: 'node:selected', style: { 'background-color': '#FFD700', 'border-width': 3, 'border-color': '#FF6B6B' } },
|
|
1095
|
+
{ selector: 'edge:selected', style: { 'line-color': '#FF6B6B', 'width': 3, 'opacity': 1 } },
|
|
1096
|
+
{ selector: 'node.grayed-out', style: { 'background-color': '#94A3B8', 'border-color': '#CBD5E1', 'opacity': 0.5 } },
|
|
1097
|
+
{ selector: 'edge.grayed-out', style: { 'line-color': '#94A3B8', 'opacity': 0.4 } }
|
|
1098
|
+
];
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* 根据 cluster 标志格式化 cluster_id 的显示值
|
|
1102
|
+
* - cluster = 0: cluster_id 显示时 +1
|
|
1103
|
+
* - cluster = 1: cluster_id 直接显示原值
|
|
1104
|
+
*/
|
|
1105
|
+
function formatClusterLabel(clusterId, clusterFlag) {
|
|
1106
|
+
if (clusterFlag === 0) {
|
|
1107
|
+
var numId = typeof clusterId === 'string' ? parseInt(clusterId, 10) : clusterId;
|
|
1108
|
+
return isNaN(numId) ? clusterId : numId + 1;
|
|
1109
|
+
}
|
|
1110
|
+
return clusterId;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// 保存全局 clusterFlag 用于显示
|
|
1114
|
+
var treeClusterFlag = null;
|
|
1115
|
+
|
|
1116
|
+
// TreeViz Class
|
|
1117
|
+
(function (global) {
|
|
1118
|
+
function TreeViz(containerId, opts) {
|
|
1119
|
+
this.container = document.getElementById(containerId);
|
|
1120
|
+
this.cy = null;
|
|
1121
|
+
this.showLabels = false;
|
|
1122
|
+
this.onStatsChange = opts.onStatsChange || function () { };
|
|
1123
|
+
this.onNodeSelect = opts.onNodeSelect || function () { };
|
|
1124
|
+
}
|
|
1125
|
+
TreeViz.prototype.init = function (treeData) {
|
|
1126
|
+
if (!treeData || !treeData.nodes) return;
|
|
1127
|
+
if (this.cy) { this.cy.destroy(); this.cy = null; }
|
|
1128
|
+
var elements = treeData.nodes.map(function (n) {
|
|
1129
|
+
return { data: { id: n.data.id, label: n.data.label, is_leaf: n.data.is_leaf, branch_length: n.data.branch_length, support: n.data.support, cluster_id: n.data.cluster_id }, position: n.position };
|
|
1130
|
+
}).concat(treeData.edges.map(function (e) {
|
|
1131
|
+
return { data: { id: e.data.id, source: e.data.source, target: e.data.target, branch_length: e.data.branch_length } };
|
|
1132
|
+
}));
|
|
1133
|
+
this.cy = cytoscape({ container: this.container, elements: elements, style: window.TREE_STYLESHEET, layout: { name: 'preset', fit: true, padding: 50 }, wheelSensitivity: 0.2, minZoom: 0.01, maxZoom: 5, boxSelectionEnabled: false });
|
|
1134
|
+
this.onStatsChange({ nodeCount: this.cy.nodes().length, edgeCount: this.cy.edges().length, leafCount: this.cy.nodes('[?is_leaf]').length });
|
|
1135
|
+
var self = this;
|
|
1136
|
+
this.cy.on('tap', 'node', function (evt) {
|
|
1137
|
+
var d = evt.target.data();
|
|
1138
|
+
self.onNodeSelect({ id: d.id, label: d.label, isLeaf: d.is_leaf, branchLength: d.branch_length, support: d.support, degree: evt.target.degree() });
|
|
1139
|
+
});
|
|
1140
|
+
this.cy.on('tap', function (evt) { if (evt.target === self.cy) self.onNodeSelect(null); });
|
|
1141
|
+
};
|
|
1142
|
+
TreeViz.prototype.toggleLabels = function () {
|
|
1143
|
+
this.showLabels = !this.showLabels;
|
|
1144
|
+
var show = this.showLabels;
|
|
1145
|
+
this.cy && this.cy.nodes().forEach(function (n) { if (n.data('is_leaf')) n.style('label', show ? (n.data('label') || '') : ''); });
|
|
1146
|
+
return this.showLabels;
|
|
1147
|
+
};
|
|
1148
|
+
TreeViz.prototype.highlightCluster = function (clusterId) {
|
|
1149
|
+
if (!this.cy) return;
|
|
1150
|
+
if (clusterId != null) {
|
|
1151
|
+
var matchIds = new Set();
|
|
1152
|
+
this.cy.nodes().forEach(function (n) { if (n.data('cluster_id') != null && String(n.data('cluster_id')) === String(clusterId)) matchIds.add(n.id()); });
|
|
1153
|
+
this.cy.nodes().forEach(function (n) { n[matchIds.has(n.id()) ? 'removeClass' : 'addClass']('grayed-out'); });
|
|
1154
|
+
var edgesToHighlight = new Set();
|
|
1155
|
+
var matchingNodes = this.cy.nodes().filter(function (n) { return matchIds.has(n.id()); });
|
|
1156
|
+
if (matchingNodes.length > 1) {
|
|
1157
|
+
var dijk = this.cy.elements().dijkstra({ root: matchingNodes[0], directed: false });
|
|
1158
|
+
for (var i = 1; i < matchingNodes.length; i++) dijk.pathTo(matchingNodes[i]).edges().forEach(function (e) { edgesToHighlight.add(e.id()); });
|
|
1159
|
+
}
|
|
1160
|
+
this.cy.edges().forEach(function (e) { e[(matchIds.has(e.source().id()) || matchIds.has(e.target().id()) || edgesToHighlight.has(e.id())) ? 'removeClass' : 'addClass']('grayed-out'); });
|
|
1161
|
+
} else {
|
|
1162
|
+
this.cy.nodes().removeClass('grayed-out'); this.cy.edges().removeClass('grayed-out');
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
TreeViz.prototype.fitView = function () { if (this.cy) { this.cy.resize(); this.cy.fit(undefined, 50); } };
|
|
1166
|
+
TreeViz.prototype.getAvailableClusters = function () {
|
|
1167
|
+
if (!this.cy) return [];
|
|
1168
|
+
var counts = new Map();
|
|
1169
|
+
this.cy.nodes().forEach(function (n) { var c = n.data('cluster_id'); if (c != null) counts.set(c, (counts.get(c) || 0) + 1); });
|
|
1170
|
+
var arr = []; counts.forEach(function (cnt, id) { arr.push({ id: id, nodeCount: cnt }); });
|
|
1171
|
+
arr.sort(function (a, b) { return typeof a.id === 'number' && typeof b.id === 'number' ? a.id - b.id : String(a.id).localeCompare(String(b.id)); });
|
|
1172
|
+
return arr;
|
|
1173
|
+
};
|
|
1174
|
+
TreeViz.prototype.exportPNG = function (fname) {
|
|
1175
|
+
if (!this.cy) return;
|
|
1176
|
+
var png = this.cy.png({ output: 'blob', bg: '#fff', scale: 2, full: true });
|
|
1177
|
+
var a = document.createElement('a'); a.href = URL.createObjectURL(png); a.download = (fname || 'tree') + '.png'; a.click(); URL.revokeObjectURL(a.href);
|
|
1178
|
+
};
|
|
1179
|
+
TreeViz.prototype.exportJSON = function (data, fname) {
|
|
1180
|
+
if (!data) return;
|
|
1181
|
+
var blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
1182
|
+
var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = (fname || 'tree') + '.json'; a.click(); URL.revokeObjectURL(a.href);
|
|
1183
|
+
};
|
|
1184
|
+
// 查找根节点(入度为 0 的节点)
|
|
1185
|
+
TreeViz.prototype.findRoot = function () {
|
|
1186
|
+
if (!this.cy) return null;
|
|
1187
|
+
var roots = this.cy.nodes().filter(function (n) { return n.indegree(false) === 0; });
|
|
1188
|
+
return roots.length > 0 ? roots[0] : null;
|
|
1189
|
+
};
|
|
1190
|
+
// 标记根节点
|
|
1191
|
+
TreeViz.prototype.markRoot = function () {
|
|
1192
|
+
if (!this.cy) return null;
|
|
1193
|
+
this.cy.nodes().removeClass('tree-root');
|
|
1194
|
+
var root = this.findRoot();
|
|
1195
|
+
if (root) {
|
|
1196
|
+
root.addClass('tree-root');
|
|
1197
|
+
return root.data();
|
|
1198
|
+
}
|
|
1199
|
+
return null;
|
|
1200
|
+
};
|
|
1201
|
+
// 从根到叶子的渐显动画(先全部置灰,然后逐层恢复原色)
|
|
1202
|
+
TreeViz.prototype.animateFromRoot = function (options) {
|
|
1203
|
+
var self = this;
|
|
1204
|
+
if (!this.cy) return;
|
|
1205
|
+
var opts = options || {};
|
|
1206
|
+
var delay = opts.delay || 120; // 每层延迟(毫秒)
|
|
1207
|
+
|
|
1208
|
+
var root = this.findRoot();
|
|
1209
|
+
if (!root) return;
|
|
1210
|
+
|
|
1211
|
+
// 第一步:全部置灰
|
|
1212
|
+
this.cy.elements().addClass('grayed-out');
|
|
1213
|
+
|
|
1214
|
+
// 使用 Cytoscape.js 官方 BFS API 按层分组
|
|
1215
|
+
var layers = [];
|
|
1216
|
+
this.cy.elements().bfs({
|
|
1217
|
+
roots: root,
|
|
1218
|
+
visit: function (v, e, u, i, depth) {
|
|
1219
|
+
if (!layers[depth]) layers[depth] = { nodes: [], edges: [] };
|
|
1220
|
+
layers[depth].nodes.push(v);
|
|
1221
|
+
// 将连接边归入当前层
|
|
1222
|
+
if (e) {
|
|
1223
|
+
layers[depth].edges.push(e);
|
|
1224
|
+
}
|
|
1225
|
+
},
|
|
1226
|
+
directed: true
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
// 第二步:分层逐渐恢复原色(移除 grayed-out 类)
|
|
1230
|
+
layers.forEach(function (layer, i) {
|
|
1231
|
+
setTimeout(function () {
|
|
1232
|
+
layer.nodes.forEach(function (n) { n.removeClass('grayed-out'); });
|
|
1233
|
+
layer.edges.forEach(function (e) { e.removeClass('grayed-out'); });
|
|
1234
|
+
}, i * delay);
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
return { layerCount: layers.length, totalNodes: this.cy.nodes().length };
|
|
1238
|
+
};
|
|
1239
|
+
global.TreeViz = TreeViz;
|
|
1240
|
+
})(window);
|
|
1241
|
+
</script>
|
|
920
1242
|
<script>
|
|
921
1243
|
// 动态计算标签页高度
|
|
922
1244
|
(function () {
|
|
@@ -936,6 +1258,12 @@
|
|
|
936
1258
|
statsTab.style.height = availableHeight + 'px';
|
|
937
1259
|
}
|
|
938
1260
|
|
|
1261
|
+
// 设置进化树标签页高度
|
|
1262
|
+
var treeTab = document.getElementById('trace-tree');
|
|
1263
|
+
if (treeTab) {
|
|
1264
|
+
treeTab.style.height = availableHeight + 'px';
|
|
1265
|
+
}
|
|
1266
|
+
|
|
939
1267
|
// 设置网络标签页内 network_tag 高度
|
|
940
1268
|
var networkTag = document.querySelector('#trace-results #network_tag');
|
|
941
1269
|
if (networkTag) {
|
|
@@ -1018,9 +1346,185 @@
|
|
|
1018
1346
|
parent_container = "#top_level_tab_container",
|
|
1019
1347
|
node_table = "#node_table",
|
|
1020
1348
|
user_graph,
|
|
1349
|
+
tree_viz = null, // 进化树可视化实例
|
|
1350
|
+
tree_data = null, // 进化树数据
|
|
1021
1351
|
countryCenersObject = null,
|
|
1022
1352
|
countryOutlines = null;
|
|
1023
1353
|
|
|
1354
|
+
// 进化树初始化函数
|
|
1355
|
+
function initTreeViz(treeData, options) {
|
|
1356
|
+
if (!treeData || !treeData.nodes || treeData.nodes.length === 0) {
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// 显示进化树 Tab
|
|
1361
|
+
var treeTab = document.getElementById('tree-tab');
|
|
1362
|
+
if (treeTab) {
|
|
1363
|
+
treeTab.style.display = '';
|
|
1364
|
+
treeTab.classList.remove('disabled');
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// 保存树数据
|
|
1368
|
+
window._treeData = treeData;
|
|
1369
|
+
|
|
1370
|
+
// 保存 cluster 标志用于显示格式化
|
|
1371
|
+
treeClusterFlag = treeData.cluster;
|
|
1372
|
+
|
|
1373
|
+
// 初始化 TreeViz 实例
|
|
1374
|
+
tree_viz = new TreeViz('tree_container', {
|
|
1375
|
+
onStatsChange: function (stats) {
|
|
1376
|
+
document.getElementById('tree_stat_nodes').textContent = stats.nodeCount;
|
|
1377
|
+
document.getElementById('tree_stat_leaves').textContent = stats.leafCount;
|
|
1378
|
+
document.getElementById('tree_stat_edges').textContent = stats.edgeCount;
|
|
1379
|
+
},
|
|
1380
|
+
onNodeSelect: function (node) {
|
|
1381
|
+
var infoPanel = document.getElementById('tree_node_info');
|
|
1382
|
+
if (node) {
|
|
1383
|
+
document.getElementById('tree_node_id').textContent = node.id;
|
|
1384
|
+
document.getElementById('tree_node_label').textContent = node.label || '-';
|
|
1385
|
+
document.getElementById('tree_node_type').textContent = node.isLeaf ? '叶节点' : '内部节点';
|
|
1386
|
+
document.getElementById('tree_node_branch').textContent = node.branchLength ? node.branchLength.toFixed(6) : '-';
|
|
1387
|
+
document.getElementById('tree_node_degree').textContent = node.degree;
|
|
1388
|
+
infoPanel.style.display = 'block';
|
|
1389
|
+
} else {
|
|
1390
|
+
infoPanel.style.display = 'none';
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
|
|
1395
|
+
tree_viz.init(treeData);
|
|
1396
|
+
// 自动标记根节点(红色菱形)
|
|
1397
|
+
tree_viz.markRoot();
|
|
1398
|
+
|
|
1399
|
+
// 获取可用簇
|
|
1400
|
+
var clusters = tree_viz.getAvailableClusters();
|
|
1401
|
+
var currentClusterIndex = -1; // -1 表示 "所有簇"
|
|
1402
|
+
|
|
1403
|
+
// 填充簇选择器菜单 - 先清空旧的簇选项(保留"所有簇")
|
|
1404
|
+
var menu = document.getElementById('tree_cluster_selector_menu');
|
|
1405
|
+
var label = document.getElementById('tree_cluster_selector_label');
|
|
1406
|
+
if (menu) {
|
|
1407
|
+
// 移除除了"所有簇"之外的所有选项
|
|
1408
|
+
var items = menu.querySelectorAll('li');
|
|
1409
|
+
for (var i = items.length - 1; i > 0; i--) {
|
|
1410
|
+
items[i].remove();
|
|
1411
|
+
}
|
|
1412
|
+
// 重置选择器标签
|
|
1413
|
+
if (label) label.textContent = '所有簇';
|
|
1414
|
+
// 使用 DocumentFragment 批量添加新簇选项(减少重排)
|
|
1415
|
+
if (clusters.length > 0) {
|
|
1416
|
+
var fragment = document.createDocumentFragment();
|
|
1417
|
+
clusters.forEach(function (cluster, index) {
|
|
1418
|
+
var li = document.createElement('li');
|
|
1419
|
+
var a = document.createElement('a');
|
|
1420
|
+
a.href = '#';
|
|
1421
|
+
a.setAttribute('data-value', cluster.id);
|
|
1422
|
+
a.setAttribute('data-index', index);
|
|
1423
|
+
a.textContent = '簇 ' + formatClusterLabel(cluster.id, treeClusterFlag) + ' (' + cluster.nodeCount + ' 节点)';
|
|
1424
|
+
li.appendChild(a);
|
|
1425
|
+
fragment.appendChild(li);
|
|
1426
|
+
});
|
|
1427
|
+
menu.appendChild(fragment);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// 更新导航按钮状态
|
|
1432
|
+
function updateTreeNavButtons() {
|
|
1433
|
+
var prevBtn = document.getElementById('tree_cluster_prev_btn');
|
|
1434
|
+
var nextBtn = document.getElementById('tree_cluster_next_btn');
|
|
1435
|
+
if (prevBtn) prevBtn.disabled = currentClusterIndex < 0;
|
|
1436
|
+
if (nextBtn) nextBtn.disabled = currentClusterIndex >= clusters.length - 1;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// 选择簇
|
|
1440
|
+
function selectTreeCluster(index) {
|
|
1441
|
+
currentClusterIndex = index;
|
|
1442
|
+
var label = document.getElementById('tree_cluster_selector_label');
|
|
1443
|
+
|
|
1444
|
+
if (index < 0) {
|
|
1445
|
+
if (label) label.textContent = '所有簇';
|
|
1446
|
+
tree_viz.highlightCluster(null);
|
|
1447
|
+
} else {
|
|
1448
|
+
var cluster = clusters[index];
|
|
1449
|
+
if (label) label.textContent = '簇 ' + formatClusterLabel(cluster.id, treeClusterFlag) + ' (' + cluster.nodeCount + ' 节点)';
|
|
1450
|
+
tree_viz.highlightCluster(cluster.id);
|
|
1451
|
+
}
|
|
1452
|
+
updateTreeNavButtons();
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// 默认高亮簇
|
|
1456
|
+
if (options && options.treeDefaultClusterId != null) {
|
|
1457
|
+
var defaultIndex = clusters.findIndex(function (c) {
|
|
1458
|
+
return String(c.id) === String(options.treeDefaultClusterId);
|
|
1459
|
+
});
|
|
1460
|
+
if (defaultIndex >= 0) {
|
|
1461
|
+
selectTreeCluster(defaultIndex);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
updateTreeNavButtons();
|
|
1466
|
+
|
|
1467
|
+
// 适应窗口按钮
|
|
1468
|
+
document.getElementById('tree_fit_btn').addEventListener('click', function () {
|
|
1469
|
+
tree_viz.fitView();
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
// 标签按钮
|
|
1473
|
+
document.getElementById('tree_labels_btn').addEventListener('click', function () {
|
|
1474
|
+
var showing = tree_viz.toggleLabels();
|
|
1475
|
+
document.getElementById('tree_labels_text').textContent = showing ? '隐藏标签' : '显示标签';
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
// 上一个簇
|
|
1479
|
+
document.getElementById('tree_cluster_prev_btn').addEventListener('click', function () {
|
|
1480
|
+
if (currentClusterIndex >= 0) {
|
|
1481
|
+
selectTreeCluster(currentClusterIndex - 1);
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
// 下一个簇
|
|
1486
|
+
document.getElementById('tree_cluster_next_btn').addEventListener('click', function () {
|
|
1487
|
+
if (currentClusterIndex < clusters.length - 1) {
|
|
1488
|
+
selectTreeCluster(currentClusterIndex + 1);
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
// 簇选择器下拉菜单点击
|
|
1493
|
+
document.getElementById('tree_cluster_selector_menu').addEventListener('click', function (e) {
|
|
1494
|
+
e.preventDefault();
|
|
1495
|
+
var link = e.target.closest('a[data-value]');
|
|
1496
|
+
if (!link) return;
|
|
1497
|
+
|
|
1498
|
+
var value = link.getAttribute('data-value');
|
|
1499
|
+
if (value === '') {
|
|
1500
|
+
selectTreeCluster(-1);
|
|
1501
|
+
} else {
|
|
1502
|
+
var index = parseInt(link.getAttribute('data-index'), 10);
|
|
1503
|
+
if (!isNaN(index)) {
|
|
1504
|
+
selectTreeCluster(index);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
// 导出 PNG
|
|
1510
|
+
document.getElementById('tree_export_png').addEventListener('click', function (e) {
|
|
1511
|
+
e.preventDefault();
|
|
1512
|
+
tree_viz.exportPNG(options && options.exportId ? options.exportId + '_tree' : 'tree');
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
// 导出 JSON
|
|
1516
|
+
document.getElementById('tree_export_json').addEventListener('click', function (e) {
|
|
1517
|
+
e.preventDefault();
|
|
1518
|
+
tree_viz.exportJSON(treeData, options && options.exportId ? options.exportId + '_tree' : 'tree');
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
// 播放动画按钮
|
|
1522
|
+
document.getElementById('tree_animate_btn').addEventListener('click', function (e) {
|
|
1523
|
+
e.preventDefault();
|
|
1524
|
+
tree_viz.animateFromRoot({ delay: 100 });
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1024
1528
|
function HandleAppError(err_string) {
|
|
1025
1529
|
showLoadingError(err_string);
|
|
1026
1530
|
}
|
|
@@ -1090,6 +1594,49 @@
|
|
|
1090
1594
|
};
|
|
1091
1595
|
}
|
|
1092
1596
|
|
|
1597
|
+
// 如果有进化树数据,添加内置的"在进化树中查看"菜单项
|
|
1598
|
+
if (options && options.treeData && options.treeData.nodes && options.treeData.nodes.length > 0) {
|
|
1599
|
+
// 添加到自定义菜单项列表
|
|
1600
|
+
if (!user_graph.customContextMenuItems) {
|
|
1601
|
+
user_graph.customContextMenuItems = [];
|
|
1602
|
+
}
|
|
1603
|
+
user_graph.customContextMenuItems.push({
|
|
1604
|
+
id: '__view_in_tree__',
|
|
1605
|
+
label: '在进化树中查看'
|
|
1606
|
+
});
|
|
1607
|
+
|
|
1608
|
+
// 包装原有回调
|
|
1609
|
+
var originalCallback = user_graph.onCustomMenuItemClick;
|
|
1610
|
+
user_graph.onCustomMenuItemClick = function (itemId, clusterInfo) {
|
|
1611
|
+
if (itemId === '__view_in_tree__') {
|
|
1612
|
+
// 切换到进化树 Tab
|
|
1613
|
+
var treeTabLink = document.querySelector('#tree-tab a[data-toggle="tab"]');
|
|
1614
|
+
if (treeTabLink) {
|
|
1615
|
+
$(treeTabLink).tab('show');
|
|
1616
|
+
// 高亮对应的簇
|
|
1617
|
+
setTimeout(function () {
|
|
1618
|
+
if (tree_viz) {
|
|
1619
|
+
tree_viz.highlightCluster(clusterInfo.cluster_id);
|
|
1620
|
+
// 更新簇选择器显示
|
|
1621
|
+
var clusters = tree_viz.getAvailableClusters();
|
|
1622
|
+
var index = clusters.findIndex(function (c) {
|
|
1623
|
+
return String(c.id) === String(clusterInfo.cluster_id);
|
|
1624
|
+
});
|
|
1625
|
+
if (index >= 0) {
|
|
1626
|
+
var label = document.getElementById('tree_cluster_selector_label');
|
|
1627
|
+
var cluster = clusters[index];
|
|
1628
|
+
if (label) label.textContent = '簇 ' + formatClusterLabel(cluster.id, treeClusterFlag) + ' (' + cluster.nodeCount + ' 节点)';
|
|
1629
|
+
}
|
|
1630
|
+
tree_viz.fitView();
|
|
1631
|
+
}
|
|
1632
|
+
}, 200);
|
|
1633
|
+
}
|
|
1634
|
+
} else if (originalCallback) {
|
|
1635
|
+
originalCallback(itemId, clusterInfo);
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1093
1640
|
if (user_graph.is_empty()) {
|
|
1094
1641
|
HandleAppError(
|
|
1095
1642
|
"This network contains no clusters and cannot be displayed"
|
|
@@ -1124,48 +1671,96 @@
|
|
|
1124
1671
|
// 设置属性页的簇过滤状态
|
|
1125
1672
|
user_graph.attributes_selected_cluster = null;
|
|
1126
1673
|
|
|
1674
|
+
// 当前选中的簇索引(-1 表示 "所有簇")
|
|
1675
|
+
var currentClusterIndex = -1;
|
|
1676
|
+
|
|
1677
|
+
// 获取导航按钮
|
|
1678
|
+
var prevBtn = document.getElementById('attrs_cluster_prev_btn');
|
|
1679
|
+
var nextBtn = document.getElementById('attrs_cluster_next_btn');
|
|
1680
|
+
|
|
1681
|
+
// 更新导航按钮状态
|
|
1682
|
+
function updateNavigationButtons() {
|
|
1683
|
+
if (prevBtn) {
|
|
1684
|
+
prevBtn.disabled = (currentClusterIndex === -1);
|
|
1685
|
+
}
|
|
1686
|
+
if (nextBtn) {
|
|
1687
|
+
nextBtn.disabled = (currentClusterIndex === clusters.length - 1);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// 选择簇的公共函数
|
|
1692
|
+
function selectCluster(index) {
|
|
1693
|
+
currentClusterIndex = index;
|
|
1694
|
+
var statsTitle = document.getElementById('attrs_stats_title');
|
|
1695
|
+
|
|
1696
|
+
if (index === -1) {
|
|
1697
|
+
// 显示所有簇
|
|
1698
|
+
label.textContent = '所有簇';
|
|
1699
|
+
user_graph.attributes_selected_cluster = null;
|
|
1700
|
+
if (statsTitle) statsTitle.textContent = '网络统计';
|
|
1701
|
+
hivtrace.graphSummary(user_graph, graph_summary_tag);
|
|
1702
|
+
hivtrace.histogramDistances(graph, histogram_tag, histogram_label);
|
|
1703
|
+
updatePillBadges(null, clusters);
|
|
1704
|
+
} else {
|
|
1705
|
+
// 显示单个 cluster
|
|
1706
|
+
var cluster = clusters[index];
|
|
1707
|
+
label.textContent = '簇 ' + cluster.cluster_id + ' (' + cluster.children.length + ' 节点)';
|
|
1708
|
+
user_graph.attributes_selected_cluster = String(cluster.cluster_id);
|
|
1709
|
+
if (statsTitle) statsTitle.textContent = '簇 ' + cluster.cluster_id + ' 统计';
|
|
1710
|
+
updateClusterStatistics(cluster);
|
|
1711
|
+
updatePillBadges(String(cluster.cluster_id), clusters);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// 重新触发当前选中的属性(如果有)
|
|
1715
|
+
if (user_graph.colorizer && user_graph.colorizer['category_id']) {
|
|
1716
|
+
if (user_graph.colorizer['continuous']) {
|
|
1717
|
+
user_graph.handle_attribute_continuous(user_graph.colorizer['category_id']);
|
|
1718
|
+
} else {
|
|
1719
|
+
user_graph.handle_attribute_categorical(user_graph.colorizer['category_id'], false);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
updateNavigationButtons();
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// 上一个簇按钮点击事件
|
|
1727
|
+
if (prevBtn) {
|
|
1728
|
+
prevBtn.addEventListener('click', function () {
|
|
1729
|
+
if (currentClusterIndex > -1) {
|
|
1730
|
+
selectCluster(currentClusterIndex - 1);
|
|
1731
|
+
}
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// 下一个簇按钮点击事件
|
|
1736
|
+
if (nextBtn) {
|
|
1737
|
+
nextBtn.addEventListener('click', function () {
|
|
1738
|
+
if (currentClusterIndex < clusters.length - 1) {
|
|
1739
|
+
selectCluster(currentClusterIndex + 1);
|
|
1740
|
+
}
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// 初始化按钮状态
|
|
1745
|
+
updateNavigationButtons();
|
|
1746
|
+
|
|
1127
1747
|
// 监听 dropdown 菜单项点击
|
|
1128
1748
|
menu.addEventListener('click', function (e) {
|
|
1129
1749
|
if (e.target.tagName === 'A') {
|
|
1130
1750
|
e.preventDefault();
|
|
1131
1751
|
var selectedClusterId = e.target.getAttribute('data-value');
|
|
1132
|
-
var selectedText = e.target.textContent;
|
|
1133
|
-
var statsTitle = document.getElementById('attrs_stats_title');
|
|
1134
|
-
|
|
1135
|
-
// 更新按钮文本
|
|
1136
|
-
label.textContent = selectedText;
|
|
1137
|
-
|
|
1138
|
-
// 设置属性页的簇过滤
|
|
1139
|
-
user_graph.attributes_selected_cluster = selectedClusterId || null;
|
|
1140
1752
|
|
|
1753
|
+
// 更新 currentClusterIndex
|
|
1141
1754
|
if (!selectedClusterId) {
|
|
1142
|
-
|
|
1143
|
-
if (statsTitle) statsTitle.textContent = '网络统计';
|
|
1144
|
-
hivtrace.graphSummary(user_graph, graph_summary_tag);
|
|
1145
|
-
hivtrace.histogramDistances(graph, histogram_tag, histogram_label);
|
|
1755
|
+
currentClusterIndex = -1;
|
|
1146
1756
|
} else {
|
|
1147
|
-
|
|
1148
|
-
var cluster = clusters.find(function (c) {
|
|
1757
|
+
currentClusterIndex = clusters.findIndex(function (c) {
|
|
1149
1758
|
return String(c.cluster_id) === String(selectedClusterId);
|
|
1150
1759
|
});
|
|
1151
|
-
|
|
1152
|
-
if (cluster) {
|
|
1153
|
-
if (statsTitle) statsTitle.textContent = '簇 ' + selectedClusterId + ' 统计';
|
|
1154
|
-
updateClusterStatistics(cluster);
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
// 重新触发当前选中的属性(如果有)
|
|
1159
|
-
if (user_graph.colorizer && user_graph.colorizer['category_id']) {
|
|
1160
|
-
if (user_graph.colorizer['continuous']) {
|
|
1161
|
-
user_graph.handle_attribute_continuous(user_graph.colorizer['category_id']);
|
|
1162
|
-
} else {
|
|
1163
|
-
user_graph.handle_attribute_categorical(user_graph.colorizer['category_id'], false);
|
|
1164
|
-
}
|
|
1165
1760
|
}
|
|
1166
1761
|
|
|
1167
|
-
//
|
|
1168
|
-
|
|
1762
|
+
// 使用公共函数选择簇
|
|
1763
|
+
selectCluster(currentClusterIndex);
|
|
1169
1764
|
}
|
|
1170
1765
|
});
|
|
1171
1766
|
|
|
@@ -1449,6 +2044,19 @@
|
|
|
1449
2044
|
}
|
|
1450
2045
|
);
|
|
1451
2046
|
|
|
2047
|
+
// 进化树 Tab 切换时自动适应窗口
|
|
2048
|
+
$("#tree-tab a[data-toggle='tab']").on(
|
|
2049
|
+
"shown.bs.tab",
|
|
2050
|
+
function (e) {
|
|
2051
|
+
if (tree_viz) {
|
|
2052
|
+
// 延迟一点确保容器已完全渲染
|
|
2053
|
+
setTimeout(function () {
|
|
2054
|
+
tree_viz.fitView();
|
|
2055
|
+
}, 100);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
);
|
|
2059
|
+
|
|
1452
2060
|
}
|
|
1453
2061
|
document
|
|
1454
2062
|
.getElementById("min_cluster_size_input")
|
|
@@ -1506,6 +2114,51 @@
|
|
|
1506
2114
|
}
|
|
1507
2115
|
}
|
|
1508
2116
|
|
|
2117
|
+
// 如果有进化树数据,确保"在进化树中查看"菜单项存在
|
|
2118
|
+
if (options && options.treeData && options.treeData.nodes && options.treeData.nodes.length > 0) {
|
|
2119
|
+
if (!user_graph.customContextMenuItems) {
|
|
2120
|
+
user_graph.customContextMenuItems = [];
|
|
2121
|
+
}
|
|
2122
|
+
// 检查是否已存在
|
|
2123
|
+
var exists = user_graph.customContextMenuItems.some(function (item) {
|
|
2124
|
+
return item.id === '__view_in_tree__';
|
|
2125
|
+
});
|
|
2126
|
+
if (!exists) {
|
|
2127
|
+
user_graph.customContextMenuItems.push({
|
|
2128
|
+
id: '__view_in_tree__',
|
|
2129
|
+
label: '在进化树中查看'
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
// 包装原有回调
|
|
2134
|
+
var originalCallback = user_graph.onCustomMenuItemClick;
|
|
2135
|
+
user_graph.onCustomMenuItemClick = function (itemId, clusterInfo) {
|
|
2136
|
+
if (itemId === '__view_in_tree__') {
|
|
2137
|
+
var treeTabLink = document.querySelector('#tree-tab a[data-toggle="tab"]');
|
|
2138
|
+
if (treeTabLink) {
|
|
2139
|
+
$(treeTabLink).tab('show');
|
|
2140
|
+
setTimeout(function () {
|
|
2141
|
+
if (tree_viz) {
|
|
2142
|
+
tree_viz.highlightCluster(clusterInfo.cluster_id);
|
|
2143
|
+
var clusters = tree_viz.getAvailableClusters();
|
|
2144
|
+
var index = clusters.findIndex(function (c) {
|
|
2145
|
+
return String(c.id) === String(clusterInfo.cluster_id);
|
|
2146
|
+
});
|
|
2147
|
+
if (index >= 0) {
|
|
2148
|
+
var label = document.getElementById('tree_cluster_selector_label');
|
|
2149
|
+
var cluster = clusters[index];
|
|
2150
|
+
if (label) label.textContent = '簇 ' + formatClusterLabel(cluster.id, treeClusterFlag) + ' (' + cluster.nodeCount + ' 节点)';
|
|
2151
|
+
}
|
|
2152
|
+
tree_viz.fitView();
|
|
2153
|
+
}
|
|
2154
|
+
}, 200);
|
|
2155
|
+
}
|
|
2156
|
+
} else if (originalCallback) {
|
|
2157
|
+
originalCallback(itemId, clusterInfo);
|
|
2158
|
+
}
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
|
|
1509
2162
|
// 执行必要的更新
|
|
1510
2163
|
user_graph.update(false, 0.4);
|
|
1511
2164
|
|
|
@@ -1632,6 +2285,13 @@
|
|
|
1632
2285
|
}
|
|
1633
2286
|
|
|
1634
2287
|
this.isFullscreen = isFullscreen;
|
|
2288
|
+
|
|
2289
|
+
// 全屏状态变化后,更新树可视化尺寸
|
|
2290
|
+
if (tree_viz) {
|
|
2291
|
+
setTimeout(function () {
|
|
2292
|
+
tree_viz.fitView();
|
|
2293
|
+
}, 100);
|
|
2294
|
+
}
|
|
1635
2295
|
},
|
|
1636
2296
|
|
|
1637
2297
|
init: function () {
|
|
@@ -1676,8 +2336,16 @@
|
|
|
1676
2336
|
|
|
1677
2337
|
if (data.type === 'HIVTRACE_DATA') {
|
|
1678
2338
|
init(data.graphData, data.options);
|
|
2339
|
+
// 如果提供了进化树数据,初始化进化树
|
|
2340
|
+
if (data.options && data.options.treeData) {
|
|
2341
|
+
initTreeViz(data.options.treeData, data.options);
|
|
2342
|
+
}
|
|
1679
2343
|
} else if (data.type === 'HIVTRACE_UPDATE_DATA') {
|
|
1680
2344
|
updateNetworkData(data.graphData, data.options);
|
|
2345
|
+
// 更新进化树数据(如果提供)
|
|
2346
|
+
if (data.options && data.options.treeData) {
|
|
2347
|
+
initTreeViz(data.options.treeData, data.options);
|
|
2348
|
+
}
|
|
1681
2349
|
} else if (data.type === 'HIVTRACE_ERROR') {
|
|
1682
2350
|
// 处理来自宿主应用的错误(如 API 请求失败)
|
|
1683
2351
|
showLoadingError(data.message || '数据加载失败');
|