@basic-genomics/hivtrace-viz 1.1.4 → 1.1.6

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.
@@ -285,6 +285,54 @@
285
285
  padding: 4px 12px;
286
286
  }
287
287
 
288
+ /* 簇选择器样式 */
289
+ .hivtrace-cluster-selector {
290
+ display: inline-flex;
291
+ align-items: stretch;
292
+ border: 1px solid #ccc;
293
+ border-radius: 4px;
294
+ background: #fff;
295
+ }
296
+
297
+ .hivtrace-cluster-selector-label {
298
+ display: flex;
299
+ align-items: center;
300
+ justify-content: center;
301
+ padding: 6px 12px;
302
+ font-size: 13px;
303
+ color: #555;
304
+ background: #f5f5f5;
305
+ border-right: 1px solid #ccc;
306
+ white-space: nowrap;
307
+ min-width: 60px;
308
+ }
309
+
310
+ .hivtrace-cluster-selector .input-group-btn {
311
+ display: flex;
312
+ }
313
+
314
+ .hivtrace-cluster-selector-btn {
315
+ border: none !important;
316
+ border-radius: 0 !important;
317
+ box-shadow: none !important;
318
+ min-width: 100px;
319
+ text-align: left;
320
+ display: flex;
321
+ align-items: center;
322
+ justify-content: space-between;
323
+ padding: 6px 12px;
324
+ font-size: 13px;
325
+ }
326
+
327
+ .hivtrace-cluster-selector-btn:hover,
328
+ .hivtrace-cluster-selector-btn:focus {
329
+ background: #f9f9f9;
330
+ }
331
+
332
+ .hivtrace-cluster-selector-btn .caret {
333
+ margin-left: 8px;
334
+ }
335
+
288
336
  /* ========================================
289
337
  属性标签页 - 新布局
290
338
  顶部横向 pills + 下方左图右表
@@ -779,11 +827,11 @@
779
827
  <div class="hivtrace-stats-container">
780
828
  <!-- 顶部:Cluster 选择器工具栏 -->
781
829
  <div class="hivtrace-stats-header">
782
- <div class="input-group input-group-sm" style="max-width: 280px;">
783
- <span class="input-group-addon">选择簇</span>
830
+ <div class="hivtrace-cluster-selector">
831
+ <span class="hivtrace-cluster-selector-label">选择簇</span>
784
832
  <div class="input-group-btn">
785
- <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"
786
- id="stats_cluster_selector_btn">
833
+ <button type="button" class="btn btn-default dropdown-toggle hivtrace-cluster-selector-btn"
834
+ data-toggle="dropdown" id="stats_cluster_selector_btn">
787
835
  <span id="stats_cluster_selector_label">总览</span> <span class="caret"></span>
788
836
  </button>
789
837
  <ul class="dropdown-menu" role="menu" id="stats_cluster_selector_menu">
@@ -918,8 +966,9 @@
918
966
  var attributes = null;
919
967
 
920
968
  // 从传入的options中获取配置
921
- var enableClusterTracking = options && options.enableClusterTracking === true;
922
969
  var expandClusters = options && Array.isArray(options.expand) ? options.expand : [];
970
+ // 获取自定义上下文菜单项配置
971
+ var customContextMenuItems = options && Array.isArray(options.customContextMenuItems) ? options.customContextMenuItems : [];
923
972
 
924
973
  // 获取基因距离阈值并动态更新簇标签
925
974
  var threshold = options && typeof options.threshold === 'number' ? options.threshold : 0.015;
@@ -953,6 +1002,61 @@
953
1002
  }
954
1003
  );
955
1004
 
1005
+ // 将自定义上下文菜单项赋值给 user_graph 实例
1006
+ if (customContextMenuItems.length > 0) {
1007
+ user_graph.customContextMenuItems = customContextMenuItems;
1008
+ // 设置点击回调,通过 postMessage 通知宿主应用
1009
+ user_graph.onCustomMenuItemClick = function (itemId, clusterInfo) {
1010
+ // 提取节点的关键信息(避免循环引用和不可序列化的对象)
1011
+ var extractNodeInfo = function (node) {
1012
+ if (!node) return null;
1013
+ return {
1014
+ id: node.id,
1015
+ // 基本属性
1016
+ degree: node.degree,
1017
+ // 节点属性(如果存在)
1018
+ attributes: node.patient_attributes || node.attributes || undefined,
1019
+ // 位置信息
1020
+ x: typeof node.x === 'number' ? node.x : undefined,
1021
+ y: typeof node.y === 'number' ? node.y : undefined,
1022
+ // 状态信息
1023
+ is_hidden: Boolean(node.is_hidden),
1024
+ match_filter: Boolean(node.match_filter),
1025
+ // 子簇信息(如果存在)
1026
+ subcluster_id: node.subcluster_id,
1027
+ subcluster_label: node.subcluster_label,
1028
+ };
1029
+ };
1030
+
1031
+ var essentialClusterData = {
1032
+ cluster_id: clusterInfo?.cluster_id ?? clusterInfo?.id ?? undefined,
1033
+ node_count: clusterInfo?.node_count ?? (Array.isArray(clusterInfo?.nodes) ? clusterInfo.nodes.length : 0),
1034
+ cluster_size: typeof clusterInfo?.cluster_size === 'number' ? clusterInfo.cluster_size : undefined,
1035
+ position: {
1036
+ x: typeof clusterInfo?.x === 'number' ? clusterInfo.x : undefined,
1037
+ y: typeof clusterInfo?.y === 'number' ? clusterInfo.y : undefined
1038
+ },
1039
+ state: {
1040
+ expanded: Boolean(clusterInfo?.expanded),
1041
+ fixed: Boolean(clusterInfo?.fixed)
1042
+ },
1043
+ // 节点 ID 列表(向后兼容)
1044
+ node_ids: Array.isArray(clusterInfo?.nodes)
1045
+ ? clusterInfo.nodes.map(function (node) { return node?.id }).filter(Boolean)
1046
+ : [],
1047
+ // 完整节点信息(用于扩展)
1048
+ nodes: Array.isArray(clusterInfo?.nodes)
1049
+ ? clusterInfo.nodes.map(extractNodeInfo).filter(Boolean)
1050
+ : []
1051
+ };
1052
+ window.parent.postMessage({
1053
+ type: 'CUSTOM_MENU_CLICK',
1054
+ itemId: itemId,
1055
+ clusterInfo: essentialClusterData
1056
+ }, '*');
1057
+ };
1058
+ }
1059
+
956
1060
  if (user_graph.is_empty()) {
957
1061
  HandleAppError(
958
1062
  "This network contains no clusters and cannot be displayed"
@@ -1125,69 +1229,6 @@
1125
1229
 
1126
1230
 
1127
1231
 
1128
- // 可配置的追踪簇回调功能 - 基于传入参数决定是否启用
1129
- if (enableClusterTracking) {
1130
- user_graph.onTrackCluster = function (clusterInfo, allData) {
1131
- try {
1132
- // 直接提取需要的数据,避免复杂的数据清理
1133
- const essentialClusterData = {
1134
- cluster_id: clusterInfo?.cluster_id ?? clusterInfo?.id ?? undefined,
1135
- node_count: clusterInfo?.node_count ?? (Array.isArray(clusterInfo?.nodes) ? clusterInfo.nodes.length : 0),
1136
- cluster_size: typeof clusterInfo?.cluster_size === 'number' ? clusterInfo.cluster_size : undefined,
1137
- position: {
1138
- x: typeof clusterInfo?.x === 'number' ? clusterInfo.x : undefined,
1139
- y: typeof clusterInfo?.y === 'number' ? clusterInfo.y : undefined
1140
- },
1141
- state: {
1142
- expanded: Boolean(clusterInfo?.expanded),
1143
- fixed: Boolean(clusterInfo?.fixed)
1144
- },
1145
- // 只提取节点ID列表,避免传递复杂对象
1146
- node_ids: Array.isArray(clusterInfo?.nodes)
1147
- ? clusterInfo.nodes.slice(0, 20).map(node => node?.id).filter(Boolean)
1148
- : []
1149
- };
1150
-
1151
- // 提取网络统计信息
1152
- const networkStats = {
1153
- total_nodes: allData?.network_info?.node_count ?? user_graph?.nodes?.length ?? 0,
1154
- total_edges: allData?.network_info?.edge_count ?? user_graph?.edges?.length ?? 0,
1155
- total_clusters: allData?.network_info?.cluster_count ?? 0
1156
- };
1157
-
1158
- // 发送轻量级、精确的数据
1159
- window.parent.postMessage({
1160
- type: 'TRACK_CLUSTER',
1161
- clusterInfo: essentialClusterData,
1162
- allData: { network_info: networkStats }
1163
- }, '*');
1164
-
1165
- } catch (error) {
1166
- console.warn('簇追踪回调处理失败:', error.message);
1167
-
1168
- // 最小化降级数据
1169
- window.parent.postMessage({
1170
- type: 'TRACK_CLUSTER',
1171
- clusterInfo: {
1172
- cluster_id: 'fallback',
1173
- node_count: 0,
1174
- cluster_size: undefined,
1175
- position: { x: undefined, y: undefined },
1176
- state: { expanded: false, fixed: false },
1177
- node_ids: []
1178
- },
1179
- allData: {
1180
- network_info: {
1181
- total_nodes: 0,
1182
- total_edges: 0,
1183
- total_clusters: 0
1184
- }
1185
- }
1186
- }, '*');
1187
- }
1188
- };
1189
- }
1190
-
1191
1232
  [
1192
1233
  "#main-tab",
1193
1234
  "#graph-tab",