@basic-genomics/hivtrace-viz 1.2.6 → 1.3.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 +602 -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,91 @@
|
|
|
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
|
+
<!-- 适应窗口 -->
|
|
959
|
+
<button type="button" class="btn btn-default btn-sm" id="tree_fit_btn" title="适应窗口">
|
|
960
|
+
<i class="fa fa-expand"></i> 适应窗口
|
|
961
|
+
</button>
|
|
962
|
+
<!-- 显示/隐藏标签 -->
|
|
963
|
+
<button type="button" class="btn btn-default btn-sm" id="tree_labels_btn" title="显示/隐藏标签">
|
|
964
|
+
<i class="fa fa-tag"></i> <span id="tree_labels_text">显示标签</span>
|
|
965
|
+
</button>
|
|
966
|
+
<!-- 簇选择器 -->
|
|
967
|
+
<div class="input-group input-group-sm" style="max-width: 320px;">
|
|
968
|
+
<span class="input-group-btn" style="width: auto;">
|
|
969
|
+
<button type="button" class="btn btn-default" id="tree_cluster_prev_btn" title="上一个簇" disabled
|
|
970
|
+
style="border-top-right-radius: 0; border-bottom-right-radius: 0;">
|
|
971
|
+
<i class="fa fa-chevron-left"></i>
|
|
972
|
+
</button>
|
|
973
|
+
</span>
|
|
974
|
+
<div class="input-group-btn" style="width: auto;">
|
|
975
|
+
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"
|
|
976
|
+
id="tree_cluster_selector_btn"
|
|
977
|
+
style="min-width: 140px; text-align: left; border-radius: 0; margin-left: -1px;">
|
|
978
|
+
<span id="tree_cluster_selector_label">所有簇</span> <span class="caret"
|
|
979
|
+
style="float: right; margin-top: 8px;"></span>
|
|
980
|
+
</button>
|
|
981
|
+
<ul class="dropdown-menu" role="menu" id="tree_cluster_selector_menu">
|
|
982
|
+
<li><a href="#" data-value="">所有簇</a></li>
|
|
983
|
+
</ul>
|
|
984
|
+
</div>
|
|
985
|
+
<span class="input-group-btn" style="width: auto;">
|
|
986
|
+
<button type="button" class="btn btn-default" id="tree_cluster_next_btn" title="下一个簇"
|
|
987
|
+
style="border-top-left-radius: 0; border-bottom-left-radius: 0; margin-left: -1px;">
|
|
988
|
+
<i class="fa fa-chevron-right"></i>
|
|
989
|
+
</button>
|
|
990
|
+
</span>
|
|
991
|
+
</div>
|
|
992
|
+
<!-- 导出按钮 -->
|
|
993
|
+
<div class="input-group-btn">
|
|
994
|
+
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown"
|
|
995
|
+
id="tree_export_btn">
|
|
996
|
+
<i class="fa fa-download"></i> 导出 <span class="caret"></span>
|
|
997
|
+
</button>
|
|
998
|
+
<ul class="dropdown-menu" role="menu">
|
|
999
|
+
<li><a href="#" id="tree_export_png">PNG 图片</a></li>
|
|
1000
|
+
<li><a href="#" id="tree_export_json">JSON 数据</a></li>
|
|
1001
|
+
</ul>
|
|
1002
|
+
</div>
|
|
1003
|
+
<!-- 统计信息 -->
|
|
1004
|
+
<div style="flex: 1;"></div>
|
|
1005
|
+
<div class="hivtrace-tree-stats" style="display: flex; gap: 12px; font-size: 13px; color: #6b7280;">
|
|
1006
|
+
<span>节点: <strong id="tree_stat_nodes">0</strong></span>
|
|
1007
|
+
<span>叶节点: <strong id="tree_stat_leaves">0</strong></span>
|
|
1008
|
+
<span>连边: <strong id="tree_stat_edges">0</strong></span>
|
|
1009
|
+
</div>
|
|
1010
|
+
</div>
|
|
1011
|
+
<!-- 进化树可视化容器 -->
|
|
1012
|
+
<div id="tree_container"
|
|
1013
|
+
style="flex: 1; background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; position: relative; min-height: 400px;">
|
|
1014
|
+
</div>
|
|
1015
|
+
<!-- 节点信息面板 -->
|
|
1016
|
+
<div id="tree_node_info" class="hivtrace-tree-node-info"
|
|
1017
|
+
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;">
|
|
1018
|
+
<h4 style="margin: 0 0 8px; font-size: 14px; font-weight: 600;">节点信息</h4>
|
|
1019
|
+
<div style="font-size: 12px; color: #374151;">
|
|
1020
|
+
<div style="display: flex; justify-content: space-between; padding: 4px 0;"><span
|
|
1021
|
+
style="color: #6b7280;">ID:</span> <span id="tree_node_id"
|
|
1022
|
+
style="font-family: monospace;">-</span></div>
|
|
1023
|
+
<div style="display: flex; justify-content: space-between; padding: 4px 0;"><span
|
|
1024
|
+
style="color: #6b7280;">标签:</span> <span id="tree_node_label">-</span></div>
|
|
1025
|
+
<div style="display: flex; justify-content: space-between; padding: 4px 0;"><span
|
|
1026
|
+
style="color: #6b7280;">类型:</span> <span id="tree_node_type">-</span></div>
|
|
1027
|
+
<div style="display: flex; justify-content: space-between; padding: 4px 0;"><span
|
|
1028
|
+
style="color: #6b7280;">分支长度:</span> <span id="tree_node_branch">-</span></div>
|
|
1029
|
+
<div style="display: flex; justify-content: space-between; padding: 4px 0;"><span
|
|
1030
|
+
style="color: #6b7280;">连接数:</span> <span id="tree_node_degree">-</span></div>
|
|
1031
|
+
</div>
|
|
1032
|
+
</div>
|
|
1033
|
+
</div>
|
|
1034
|
+
</div>
|
|
909
1035
|
</div>
|
|
910
1036
|
</div>
|
|
911
1037
|
</div>
|
|
@@ -917,6 +1043,109 @@
|
|
|
917
1043
|
|
|
918
1044
|
<script src="./hivtrace.js"></script>
|
|
919
1045
|
<!-- <script src="dist/hivtrace.es.js"></script> -->
|
|
1046
|
+
|
|
1047
|
+
<!-- Tree Visualization Configuration -->
|
|
1048
|
+
<script>
|
|
1049
|
+
// Cytoscape Stylesheet for Tree
|
|
1050
|
+
window.TREE_STYLESHEET = [
|
|
1051
|
+
{ selector: 'node', style: { 'font-size': 11, 'text-valign': 'center', 'text-halign': 'right', 'text-margin-x': 5, 'color': '#333', 'min-zoomed-font-size': 8 } },
|
|
1052
|
+
{ selector: 'node[?is_leaf]', style: { 'background-color': '#2563eb', 'width': 10, 'height': 10 } },
|
|
1053
|
+
{ selector: 'node[!is_leaf]', style: { 'background-color': '#CBD5E1', 'width': 4, 'height': 4, 'opacity': 0.7, 'label': '', 'border-width': 0 } },
|
|
1054
|
+
{ selector: 'node[!is_leaf][support]', style: { 'background-color': '#94A3B8', 'width': 5, 'height': 5, 'opacity': 0.8 } },
|
|
1055
|
+
{ selector: 'edge', style: { 'width': 2, 'line-color': '#333', 'target-arrow-color': '#333', 'curve-style': 'bezier', 'opacity': 0.8 } },
|
|
1056
|
+
{ selector: 'node:selected', style: { 'background-color': '#FFD700', 'border-width': 3, 'border-color': '#FF6B6B' } },
|
|
1057
|
+
{ selector: 'edge:selected', style: { 'line-color': '#FF6B6B', 'width': 3, 'opacity': 1 } },
|
|
1058
|
+
{ selector: 'node.grayed-out', style: { 'background-color': '#94A3B8', 'border-color': '#CBD5E1', 'opacity': 0.5 } },
|
|
1059
|
+
{ selector: 'edge.grayed-out', style: { 'line-color': '#94A3B8', 'opacity': 0.4 } }
|
|
1060
|
+
];
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* 根据 cluster 标志格式化 cluster_id 的显示值
|
|
1064
|
+
* - cluster = 0: cluster_id 显示时 +1
|
|
1065
|
+
* - cluster = 1: cluster_id 直接显示原值
|
|
1066
|
+
*/
|
|
1067
|
+
function formatClusterLabel(clusterId, clusterFlag) {
|
|
1068
|
+
if (clusterFlag === 0) {
|
|
1069
|
+
var numId = typeof clusterId === 'string' ? parseInt(clusterId, 10) : clusterId;
|
|
1070
|
+
return isNaN(numId) ? clusterId : numId + 1;
|
|
1071
|
+
}
|
|
1072
|
+
return clusterId;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// 保存全局 clusterFlag 用于显示
|
|
1076
|
+
var treeClusterFlag = null;
|
|
1077
|
+
|
|
1078
|
+
// TreeViz Class
|
|
1079
|
+
(function (global) {
|
|
1080
|
+
function TreeViz(containerId, opts) {
|
|
1081
|
+
this.container = document.getElementById(containerId);
|
|
1082
|
+
this.cy = null;
|
|
1083
|
+
this.showLabels = false;
|
|
1084
|
+
this.onStatsChange = opts.onStatsChange || function () { };
|
|
1085
|
+
this.onNodeSelect = opts.onNodeSelect || function () { };
|
|
1086
|
+
}
|
|
1087
|
+
TreeViz.prototype.init = function (treeData) {
|
|
1088
|
+
if (!treeData || !treeData.nodes) return;
|
|
1089
|
+
if (this.cy) { this.cy.destroy(); this.cy = null; }
|
|
1090
|
+
var elements = treeData.nodes.map(function (n) {
|
|
1091
|
+
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 };
|
|
1092
|
+
}).concat(treeData.edges.map(function (e) {
|
|
1093
|
+
return { data: { id: e.data.id, source: e.data.source, target: e.data.target, branch_length: e.data.branch_length } };
|
|
1094
|
+
}));
|
|
1095
|
+
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 });
|
|
1096
|
+
this.onStatsChange({ nodeCount: this.cy.nodes().length, edgeCount: this.cy.edges().length, leafCount: this.cy.nodes('[?is_leaf]').length });
|
|
1097
|
+
var self = this;
|
|
1098
|
+
this.cy.on('tap', 'node', function (evt) {
|
|
1099
|
+
var d = evt.target.data();
|
|
1100
|
+
self.onNodeSelect({ id: d.id, label: d.label, isLeaf: d.is_leaf, branchLength: d.branch_length, support: d.support, degree: evt.target.degree() });
|
|
1101
|
+
});
|
|
1102
|
+
this.cy.on('tap', function (evt) { if (evt.target === self.cy) self.onNodeSelect(null); });
|
|
1103
|
+
};
|
|
1104
|
+
TreeViz.prototype.toggleLabels = function () {
|
|
1105
|
+
this.showLabels = !this.showLabels;
|
|
1106
|
+
var show = this.showLabels;
|
|
1107
|
+
this.cy && this.cy.nodes().forEach(function (n) { if (n.data('is_leaf')) n.style('label', show ? (n.data('label') || '') : ''); });
|
|
1108
|
+
return this.showLabels;
|
|
1109
|
+
};
|
|
1110
|
+
TreeViz.prototype.highlightCluster = function (clusterId) {
|
|
1111
|
+
if (!this.cy) return;
|
|
1112
|
+
if (clusterId != null) {
|
|
1113
|
+
var matchIds = new Set();
|
|
1114
|
+
this.cy.nodes().forEach(function (n) { if (n.data('cluster_id') != null && String(n.data('cluster_id')) === String(clusterId)) matchIds.add(n.id()); });
|
|
1115
|
+
this.cy.nodes().forEach(function (n) { n[matchIds.has(n.id()) ? 'removeClass' : 'addClass']('grayed-out'); });
|
|
1116
|
+
var edgesToHighlight = new Set();
|
|
1117
|
+
var matchingNodes = this.cy.nodes().filter(function (n) { return matchIds.has(n.id()); });
|
|
1118
|
+
if (matchingNodes.length > 1) {
|
|
1119
|
+
var dijk = this.cy.elements().dijkstra({ root: matchingNodes[0], directed: false });
|
|
1120
|
+
for (var i = 1; i < matchingNodes.length; i++) dijk.pathTo(matchingNodes[i]).edges().forEach(function (e) { edgesToHighlight.add(e.id()); });
|
|
1121
|
+
}
|
|
1122
|
+
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'); });
|
|
1123
|
+
} else {
|
|
1124
|
+
this.cy.nodes().removeClass('grayed-out'); this.cy.edges().removeClass('grayed-out');
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
TreeViz.prototype.fitView = function () { if (this.cy) { this.cy.resize(); this.cy.fit(undefined, 50); } };
|
|
1128
|
+
TreeViz.prototype.getAvailableClusters = function () {
|
|
1129
|
+
if (!this.cy) return [];
|
|
1130
|
+
var counts = new Map();
|
|
1131
|
+
this.cy.nodes().forEach(function (n) { var c = n.data('cluster_id'); if (c != null) counts.set(c, (counts.get(c) || 0) + 1); });
|
|
1132
|
+
var arr = []; counts.forEach(function (cnt, id) { arr.push({ id: id, nodeCount: cnt }); });
|
|
1133
|
+
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)); });
|
|
1134
|
+
return arr;
|
|
1135
|
+
};
|
|
1136
|
+
TreeViz.prototype.exportPNG = function (fname) {
|
|
1137
|
+
if (!this.cy) return;
|
|
1138
|
+
var png = this.cy.png({ output: 'blob', bg: '#fff', scale: 2, full: true });
|
|
1139
|
+
var a = document.createElement('a'); a.href = URL.createObjectURL(png); a.download = (fname || 'tree') + '.png'; a.click(); URL.revokeObjectURL(a.href);
|
|
1140
|
+
};
|
|
1141
|
+
TreeViz.prototype.exportJSON = function (data, fname) {
|
|
1142
|
+
if (!data) return;
|
|
1143
|
+
var blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
1144
|
+
var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = (fname || 'tree') + '.json'; a.click(); URL.revokeObjectURL(a.href);
|
|
1145
|
+
};
|
|
1146
|
+
global.TreeViz = TreeViz;
|
|
1147
|
+
})(window);
|
|
1148
|
+
</script>
|
|
920
1149
|
<script>
|
|
921
1150
|
// 动态计算标签页高度
|
|
922
1151
|
(function () {
|
|
@@ -936,6 +1165,12 @@
|
|
|
936
1165
|
statsTab.style.height = availableHeight + 'px';
|
|
937
1166
|
}
|
|
938
1167
|
|
|
1168
|
+
// 设置进化树标签页高度
|
|
1169
|
+
var treeTab = document.getElementById('trace-tree');
|
|
1170
|
+
if (treeTab) {
|
|
1171
|
+
treeTab.style.height = availableHeight + 'px';
|
|
1172
|
+
}
|
|
1173
|
+
|
|
939
1174
|
// 设置网络标签页内 network_tag 高度
|
|
940
1175
|
var networkTag = document.querySelector('#trace-results #network_tag');
|
|
941
1176
|
if (networkTag) {
|
|
@@ -1018,9 +1253,177 @@
|
|
|
1018
1253
|
parent_container = "#top_level_tab_container",
|
|
1019
1254
|
node_table = "#node_table",
|
|
1020
1255
|
user_graph,
|
|
1256
|
+
tree_viz = null, // 进化树可视化实例
|
|
1257
|
+
tree_data = null, // 进化树数据
|
|
1021
1258
|
countryCenersObject = null,
|
|
1022
1259
|
countryOutlines = null;
|
|
1023
1260
|
|
|
1261
|
+
// 进化树初始化函数
|
|
1262
|
+
function initTreeViz(treeData, options) {
|
|
1263
|
+
if (!treeData || !treeData.nodes || treeData.nodes.length === 0) {
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// 显示进化树 Tab
|
|
1268
|
+
var treeTab = document.getElementById('tree-tab');
|
|
1269
|
+
if (treeTab) {
|
|
1270
|
+
treeTab.style.display = '';
|
|
1271
|
+
treeTab.classList.remove('disabled');
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// 保存树数据
|
|
1275
|
+
window._treeData = treeData;
|
|
1276
|
+
|
|
1277
|
+
// 保存 cluster 标志用于显示格式化
|
|
1278
|
+
treeClusterFlag = treeData.cluster;
|
|
1279
|
+
|
|
1280
|
+
// 初始化 TreeViz 实例
|
|
1281
|
+
tree_viz = new TreeViz('tree_container', {
|
|
1282
|
+
onStatsChange: function (stats) {
|
|
1283
|
+
document.getElementById('tree_stat_nodes').textContent = stats.nodeCount;
|
|
1284
|
+
document.getElementById('tree_stat_leaves').textContent = stats.leafCount;
|
|
1285
|
+
document.getElementById('tree_stat_edges').textContent = stats.edgeCount;
|
|
1286
|
+
},
|
|
1287
|
+
onNodeSelect: function (node) {
|
|
1288
|
+
var infoPanel = document.getElementById('tree_node_info');
|
|
1289
|
+
if (node) {
|
|
1290
|
+
document.getElementById('tree_node_id').textContent = node.id;
|
|
1291
|
+
document.getElementById('tree_node_label').textContent = node.label || '-';
|
|
1292
|
+
document.getElementById('tree_node_type').textContent = node.isLeaf ? '叶节点' : '内部节点';
|
|
1293
|
+
document.getElementById('tree_node_branch').textContent = node.branchLength ? node.branchLength.toFixed(6) : '-';
|
|
1294
|
+
document.getElementById('tree_node_degree').textContent = node.degree;
|
|
1295
|
+
infoPanel.style.display = 'block';
|
|
1296
|
+
} else {
|
|
1297
|
+
infoPanel.style.display = 'none';
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
tree_viz.init(treeData);
|
|
1303
|
+
|
|
1304
|
+
// 获取可用簇
|
|
1305
|
+
var clusters = tree_viz.getAvailableClusters();
|
|
1306
|
+
var currentClusterIndex = -1; // -1 表示 "所有簇"
|
|
1307
|
+
|
|
1308
|
+
// 填充簇选择器菜单 - 先清空旧的簇选项(保留"所有簇")
|
|
1309
|
+
var menu = document.getElementById('tree_cluster_selector_menu');
|
|
1310
|
+
var label = document.getElementById('tree_cluster_selector_label');
|
|
1311
|
+
if (menu) {
|
|
1312
|
+
// 移除除了"所有簇"之外的所有选项
|
|
1313
|
+
var items = menu.querySelectorAll('li');
|
|
1314
|
+
for (var i = items.length - 1; i > 0; i--) {
|
|
1315
|
+
items[i].remove();
|
|
1316
|
+
}
|
|
1317
|
+
// 重置选择器标签
|
|
1318
|
+
if (label) label.textContent = '所有簇';
|
|
1319
|
+
// 使用 DocumentFragment 批量添加新簇选项(减少重排)
|
|
1320
|
+
if (clusters.length > 0) {
|
|
1321
|
+
var fragment = document.createDocumentFragment();
|
|
1322
|
+
clusters.forEach(function (cluster, index) {
|
|
1323
|
+
var li = document.createElement('li');
|
|
1324
|
+
var a = document.createElement('a');
|
|
1325
|
+
a.href = '#';
|
|
1326
|
+
a.setAttribute('data-value', cluster.id);
|
|
1327
|
+
a.setAttribute('data-index', index);
|
|
1328
|
+
a.textContent = '簇 ' + formatClusterLabel(cluster.id, treeClusterFlag) + ' (' + cluster.nodeCount + ' 节点)';
|
|
1329
|
+
li.appendChild(a);
|
|
1330
|
+
fragment.appendChild(li);
|
|
1331
|
+
});
|
|
1332
|
+
menu.appendChild(fragment);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// 更新导航按钮状态
|
|
1337
|
+
function updateTreeNavButtons() {
|
|
1338
|
+
var prevBtn = document.getElementById('tree_cluster_prev_btn');
|
|
1339
|
+
var nextBtn = document.getElementById('tree_cluster_next_btn');
|
|
1340
|
+
if (prevBtn) prevBtn.disabled = currentClusterIndex < 0;
|
|
1341
|
+
if (nextBtn) nextBtn.disabled = currentClusterIndex >= clusters.length - 1;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// 选择簇
|
|
1345
|
+
function selectTreeCluster(index) {
|
|
1346
|
+
currentClusterIndex = index;
|
|
1347
|
+
var label = document.getElementById('tree_cluster_selector_label');
|
|
1348
|
+
|
|
1349
|
+
if (index < 0) {
|
|
1350
|
+
if (label) label.textContent = '所有簇';
|
|
1351
|
+
tree_viz.highlightCluster(null);
|
|
1352
|
+
} else {
|
|
1353
|
+
var cluster = clusters[index];
|
|
1354
|
+
if (label) label.textContent = '簇 ' + formatClusterLabel(cluster.id, treeClusterFlag) + ' (' + cluster.nodeCount + ' 节点)';
|
|
1355
|
+
tree_viz.highlightCluster(cluster.id);
|
|
1356
|
+
}
|
|
1357
|
+
updateTreeNavButtons();
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// 默认高亮簇
|
|
1361
|
+
if (options && options.treeDefaultClusterId != null) {
|
|
1362
|
+
var defaultIndex = clusters.findIndex(function (c) {
|
|
1363
|
+
return String(c.id) === String(options.treeDefaultClusterId);
|
|
1364
|
+
});
|
|
1365
|
+
if (defaultIndex >= 0) {
|
|
1366
|
+
selectTreeCluster(defaultIndex);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
updateTreeNavButtons();
|
|
1371
|
+
|
|
1372
|
+
// 适应窗口按钮
|
|
1373
|
+
document.getElementById('tree_fit_btn').addEventListener('click', function () {
|
|
1374
|
+
tree_viz.fitView();
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
// 标签按钮
|
|
1378
|
+
document.getElementById('tree_labels_btn').addEventListener('click', function () {
|
|
1379
|
+
var showing = tree_viz.toggleLabels();
|
|
1380
|
+
document.getElementById('tree_labels_text').textContent = showing ? '隐藏标签' : '显示标签';
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
// 上一个簇
|
|
1384
|
+
document.getElementById('tree_cluster_prev_btn').addEventListener('click', function () {
|
|
1385
|
+
if (currentClusterIndex >= 0) {
|
|
1386
|
+
selectTreeCluster(currentClusterIndex - 1);
|
|
1387
|
+
}
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
// 下一个簇
|
|
1391
|
+
document.getElementById('tree_cluster_next_btn').addEventListener('click', function () {
|
|
1392
|
+
if (currentClusterIndex < clusters.length - 1) {
|
|
1393
|
+
selectTreeCluster(currentClusterIndex + 1);
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
// 簇选择器下拉菜单点击
|
|
1398
|
+
document.getElementById('tree_cluster_selector_menu').addEventListener('click', function (e) {
|
|
1399
|
+
e.preventDefault();
|
|
1400
|
+
var link = e.target.closest('a[data-value]');
|
|
1401
|
+
if (!link) return;
|
|
1402
|
+
|
|
1403
|
+
var value = link.getAttribute('data-value');
|
|
1404
|
+
if (value === '') {
|
|
1405
|
+
selectTreeCluster(-1);
|
|
1406
|
+
} else {
|
|
1407
|
+
var index = parseInt(link.getAttribute('data-index'), 10);
|
|
1408
|
+
if (!isNaN(index)) {
|
|
1409
|
+
selectTreeCluster(index);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
// 导出 PNG
|
|
1415
|
+
document.getElementById('tree_export_png').addEventListener('click', function (e) {
|
|
1416
|
+
e.preventDefault();
|
|
1417
|
+
tree_viz.exportPNG(options && options.exportId ? options.exportId + '_tree' : 'tree');
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
// 导出 JSON
|
|
1421
|
+
document.getElementById('tree_export_json').addEventListener('click', function (e) {
|
|
1422
|
+
e.preventDefault();
|
|
1423
|
+
tree_viz.exportJSON(treeData, options && options.exportId ? options.exportId + '_tree' : 'tree');
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1024
1427
|
function HandleAppError(err_string) {
|
|
1025
1428
|
showLoadingError(err_string);
|
|
1026
1429
|
}
|
|
@@ -1090,6 +1493,49 @@
|
|
|
1090
1493
|
};
|
|
1091
1494
|
}
|
|
1092
1495
|
|
|
1496
|
+
// 如果有进化树数据,添加内置的"在进化树中查看"菜单项
|
|
1497
|
+
if (options && options.treeData && options.treeData.nodes && options.treeData.nodes.length > 0) {
|
|
1498
|
+
// 添加到自定义菜单项列表
|
|
1499
|
+
if (!user_graph.customContextMenuItems) {
|
|
1500
|
+
user_graph.customContextMenuItems = [];
|
|
1501
|
+
}
|
|
1502
|
+
user_graph.customContextMenuItems.push({
|
|
1503
|
+
id: '__view_in_tree__',
|
|
1504
|
+
label: '在进化树中查看'
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
// 包装原有回调
|
|
1508
|
+
var originalCallback = user_graph.onCustomMenuItemClick;
|
|
1509
|
+
user_graph.onCustomMenuItemClick = function (itemId, clusterInfo) {
|
|
1510
|
+
if (itemId === '__view_in_tree__') {
|
|
1511
|
+
// 切换到进化树 Tab
|
|
1512
|
+
var treeTabLink = document.querySelector('#tree-tab a[data-toggle="tab"]');
|
|
1513
|
+
if (treeTabLink) {
|
|
1514
|
+
$(treeTabLink).tab('show');
|
|
1515
|
+
// 高亮对应的簇
|
|
1516
|
+
setTimeout(function () {
|
|
1517
|
+
if (tree_viz) {
|
|
1518
|
+
tree_viz.highlightCluster(clusterInfo.cluster_id);
|
|
1519
|
+
// 更新簇选择器显示
|
|
1520
|
+
var clusters = tree_viz.getAvailableClusters();
|
|
1521
|
+
var index = clusters.findIndex(function (c) {
|
|
1522
|
+
return String(c.id) === String(clusterInfo.cluster_id);
|
|
1523
|
+
});
|
|
1524
|
+
if (index >= 0) {
|
|
1525
|
+
var label = document.getElementById('tree_cluster_selector_label');
|
|
1526
|
+
var cluster = clusters[index];
|
|
1527
|
+
if (label) label.textContent = '簇 ' + formatClusterLabel(cluster.id, treeClusterFlag) + ' (' + cluster.nodeCount + ' 节点)';
|
|
1528
|
+
}
|
|
1529
|
+
tree_viz.fitView();
|
|
1530
|
+
}
|
|
1531
|
+
}, 200);
|
|
1532
|
+
}
|
|
1533
|
+
} else if (originalCallback) {
|
|
1534
|
+
originalCallback(itemId, clusterInfo);
|
|
1535
|
+
}
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1093
1539
|
if (user_graph.is_empty()) {
|
|
1094
1540
|
HandleAppError(
|
|
1095
1541
|
"This network contains no clusters and cannot be displayed"
|
|
@@ -1124,48 +1570,96 @@
|
|
|
1124
1570
|
// 设置属性页的簇过滤状态
|
|
1125
1571
|
user_graph.attributes_selected_cluster = null;
|
|
1126
1572
|
|
|
1573
|
+
// 当前选中的簇索引(-1 表示 "所有簇")
|
|
1574
|
+
var currentClusterIndex = -1;
|
|
1575
|
+
|
|
1576
|
+
// 获取导航按钮
|
|
1577
|
+
var prevBtn = document.getElementById('attrs_cluster_prev_btn');
|
|
1578
|
+
var nextBtn = document.getElementById('attrs_cluster_next_btn');
|
|
1579
|
+
|
|
1580
|
+
// 更新导航按钮状态
|
|
1581
|
+
function updateNavigationButtons() {
|
|
1582
|
+
if (prevBtn) {
|
|
1583
|
+
prevBtn.disabled = (currentClusterIndex === -1);
|
|
1584
|
+
}
|
|
1585
|
+
if (nextBtn) {
|
|
1586
|
+
nextBtn.disabled = (currentClusterIndex === clusters.length - 1);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// 选择簇的公共函数
|
|
1591
|
+
function selectCluster(index) {
|
|
1592
|
+
currentClusterIndex = index;
|
|
1593
|
+
var statsTitle = document.getElementById('attrs_stats_title');
|
|
1594
|
+
|
|
1595
|
+
if (index === -1) {
|
|
1596
|
+
// 显示所有簇
|
|
1597
|
+
label.textContent = '所有簇';
|
|
1598
|
+
user_graph.attributes_selected_cluster = null;
|
|
1599
|
+
if (statsTitle) statsTitle.textContent = '网络统计';
|
|
1600
|
+
hivtrace.graphSummary(user_graph, graph_summary_tag);
|
|
1601
|
+
hivtrace.histogramDistances(graph, histogram_tag, histogram_label);
|
|
1602
|
+
updatePillBadges(null, clusters);
|
|
1603
|
+
} else {
|
|
1604
|
+
// 显示单个 cluster
|
|
1605
|
+
var cluster = clusters[index];
|
|
1606
|
+
label.textContent = '簇 ' + cluster.cluster_id + ' (' + cluster.children.length + ' 节点)';
|
|
1607
|
+
user_graph.attributes_selected_cluster = String(cluster.cluster_id);
|
|
1608
|
+
if (statsTitle) statsTitle.textContent = '簇 ' + cluster.cluster_id + ' 统计';
|
|
1609
|
+
updateClusterStatistics(cluster);
|
|
1610
|
+
updatePillBadges(String(cluster.cluster_id), clusters);
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// 重新触发当前选中的属性(如果有)
|
|
1614
|
+
if (user_graph.colorizer && user_graph.colorizer['category_id']) {
|
|
1615
|
+
if (user_graph.colorizer['continuous']) {
|
|
1616
|
+
user_graph.handle_attribute_continuous(user_graph.colorizer['category_id']);
|
|
1617
|
+
} else {
|
|
1618
|
+
user_graph.handle_attribute_categorical(user_graph.colorizer['category_id'], false);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
updateNavigationButtons();
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// 上一个簇按钮点击事件
|
|
1626
|
+
if (prevBtn) {
|
|
1627
|
+
prevBtn.addEventListener('click', function () {
|
|
1628
|
+
if (currentClusterIndex > -1) {
|
|
1629
|
+
selectCluster(currentClusterIndex - 1);
|
|
1630
|
+
}
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// 下一个簇按钮点击事件
|
|
1635
|
+
if (nextBtn) {
|
|
1636
|
+
nextBtn.addEventListener('click', function () {
|
|
1637
|
+
if (currentClusterIndex < clusters.length - 1) {
|
|
1638
|
+
selectCluster(currentClusterIndex + 1);
|
|
1639
|
+
}
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// 初始化按钮状态
|
|
1644
|
+
updateNavigationButtons();
|
|
1645
|
+
|
|
1127
1646
|
// 监听 dropdown 菜单项点击
|
|
1128
1647
|
menu.addEventListener('click', function (e) {
|
|
1129
1648
|
if (e.target.tagName === 'A') {
|
|
1130
1649
|
e.preventDefault();
|
|
1131
1650
|
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
1651
|
|
|
1652
|
+
// 更新 currentClusterIndex
|
|
1141
1653
|
if (!selectedClusterId) {
|
|
1142
|
-
|
|
1143
|
-
if (statsTitle) statsTitle.textContent = '网络统计';
|
|
1144
|
-
hivtrace.graphSummary(user_graph, graph_summary_tag);
|
|
1145
|
-
hivtrace.histogramDistances(graph, histogram_tag, histogram_label);
|
|
1654
|
+
currentClusterIndex = -1;
|
|
1146
1655
|
} else {
|
|
1147
|
-
|
|
1148
|
-
var cluster = clusters.find(function (c) {
|
|
1656
|
+
currentClusterIndex = clusters.findIndex(function (c) {
|
|
1149
1657
|
return String(c.cluster_id) === String(selectedClusterId);
|
|
1150
1658
|
});
|
|
1151
|
-
|
|
1152
|
-
if (cluster) {
|
|
1153
|
-
if (statsTitle) statsTitle.textContent = '簇 ' + selectedClusterId + ' 统计';
|
|
1154
|
-
updateClusterStatistics(cluster);
|
|
1155
|
-
}
|
|
1156
1659
|
}
|
|
1157
1660
|
|
|
1158
|
-
//
|
|
1159
|
-
|
|
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
|
-
}
|
|
1166
|
-
|
|
1167
|
-
// 更新 pill badges(唯一值数量)
|
|
1168
|
-
updatePillBadges(selectedClusterId, clusters);
|
|
1661
|
+
// 使用公共函数选择簇
|
|
1662
|
+
selectCluster(currentClusterIndex);
|
|
1169
1663
|
}
|
|
1170
1664
|
});
|
|
1171
1665
|
|
|
@@ -1449,6 +1943,19 @@
|
|
|
1449
1943
|
}
|
|
1450
1944
|
);
|
|
1451
1945
|
|
|
1946
|
+
// 进化树 Tab 切换时自动适应窗口
|
|
1947
|
+
$("#tree-tab a[data-toggle='tab']").on(
|
|
1948
|
+
"shown.bs.tab",
|
|
1949
|
+
function (e) {
|
|
1950
|
+
if (tree_viz) {
|
|
1951
|
+
// 延迟一点确保容器已完全渲染
|
|
1952
|
+
setTimeout(function () {
|
|
1953
|
+
tree_viz.fitView();
|
|
1954
|
+
}, 100);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
);
|
|
1958
|
+
|
|
1452
1959
|
}
|
|
1453
1960
|
document
|
|
1454
1961
|
.getElementById("min_cluster_size_input")
|
|
@@ -1506,6 +2013,51 @@
|
|
|
1506
2013
|
}
|
|
1507
2014
|
}
|
|
1508
2015
|
|
|
2016
|
+
// 如果有进化树数据,确保"在进化树中查看"菜单项存在
|
|
2017
|
+
if (options && options.treeData && options.treeData.nodes && options.treeData.nodes.length > 0) {
|
|
2018
|
+
if (!user_graph.customContextMenuItems) {
|
|
2019
|
+
user_graph.customContextMenuItems = [];
|
|
2020
|
+
}
|
|
2021
|
+
// 检查是否已存在
|
|
2022
|
+
var exists = user_graph.customContextMenuItems.some(function (item) {
|
|
2023
|
+
return item.id === '__view_in_tree__';
|
|
2024
|
+
});
|
|
2025
|
+
if (!exists) {
|
|
2026
|
+
user_graph.customContextMenuItems.push({
|
|
2027
|
+
id: '__view_in_tree__',
|
|
2028
|
+
label: '在进化树中查看'
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// 包装原有回调
|
|
2033
|
+
var originalCallback = user_graph.onCustomMenuItemClick;
|
|
2034
|
+
user_graph.onCustomMenuItemClick = function (itemId, clusterInfo) {
|
|
2035
|
+
if (itemId === '__view_in_tree__') {
|
|
2036
|
+
var treeTabLink = document.querySelector('#tree-tab a[data-toggle="tab"]');
|
|
2037
|
+
if (treeTabLink) {
|
|
2038
|
+
$(treeTabLink).tab('show');
|
|
2039
|
+
setTimeout(function () {
|
|
2040
|
+
if (tree_viz) {
|
|
2041
|
+
tree_viz.highlightCluster(clusterInfo.cluster_id);
|
|
2042
|
+
var clusters = tree_viz.getAvailableClusters();
|
|
2043
|
+
var index = clusters.findIndex(function (c) {
|
|
2044
|
+
return String(c.id) === String(clusterInfo.cluster_id);
|
|
2045
|
+
});
|
|
2046
|
+
if (index >= 0) {
|
|
2047
|
+
var label = document.getElementById('tree_cluster_selector_label');
|
|
2048
|
+
var cluster = clusters[index];
|
|
2049
|
+
if (label) label.textContent = '簇 ' + formatClusterLabel(cluster.id, treeClusterFlag) + ' (' + cluster.nodeCount + ' 节点)';
|
|
2050
|
+
}
|
|
2051
|
+
tree_viz.fitView();
|
|
2052
|
+
}
|
|
2053
|
+
}, 200);
|
|
2054
|
+
}
|
|
2055
|
+
} else if (originalCallback) {
|
|
2056
|
+
originalCallback(itemId, clusterInfo);
|
|
2057
|
+
}
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
|
|
1509
2061
|
// 执行必要的更新
|
|
1510
2062
|
user_graph.update(false, 0.4);
|
|
1511
2063
|
|
|
@@ -1632,6 +2184,13 @@
|
|
|
1632
2184
|
}
|
|
1633
2185
|
|
|
1634
2186
|
this.isFullscreen = isFullscreen;
|
|
2187
|
+
|
|
2188
|
+
// 全屏状态变化后,更新树可视化尺寸
|
|
2189
|
+
if (tree_viz) {
|
|
2190
|
+
setTimeout(function () {
|
|
2191
|
+
tree_viz.fitView();
|
|
2192
|
+
}, 100);
|
|
2193
|
+
}
|
|
1635
2194
|
},
|
|
1636
2195
|
|
|
1637
2196
|
init: function () {
|
|
@@ -1676,8 +2235,16 @@
|
|
|
1676
2235
|
|
|
1677
2236
|
if (data.type === 'HIVTRACE_DATA') {
|
|
1678
2237
|
init(data.graphData, data.options);
|
|
2238
|
+
// 如果提供了进化树数据,初始化进化树
|
|
2239
|
+
if (data.options && data.options.treeData) {
|
|
2240
|
+
initTreeViz(data.options.treeData, data.options);
|
|
2241
|
+
}
|
|
1679
2242
|
} else if (data.type === 'HIVTRACE_UPDATE_DATA') {
|
|
1680
2243
|
updateNetworkData(data.graphData, data.options);
|
|
2244
|
+
// 更新进化树数据(如果提供)
|
|
2245
|
+
if (data.options && data.options.treeData) {
|
|
2246
|
+
initTreeViz(data.options.treeData, data.options);
|
|
2247
|
+
}
|
|
1681
2248
|
} else if (data.type === 'HIVTRACE_ERROR') {
|
|
1682
2249
|
// 处理来自宿主应用的错误(如 API 请求失败)
|
|
1683
2250
|
showLoadingError(data.message || '数据加载失败');
|