TSUMUGI 1.0.1__py3-none-any.whl

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.
Files changed (64) hide show
  1. TSUMUGI/annotator.py +103 -0
  2. TSUMUGI/argparser.py +599 -0
  3. TSUMUGI/core.py +185 -0
  4. TSUMUGI/data/impc_phenodigm.csv +3406 -0
  5. TSUMUGI/data/mp.obo +143993 -0
  6. TSUMUGI/filterer.py +36 -0
  7. TSUMUGI/formatter.py +122 -0
  8. TSUMUGI/genewise_annotation_builder.py +94 -0
  9. TSUMUGI/io_handler.py +189 -0
  10. TSUMUGI/main.py +300 -0
  11. TSUMUGI/network_constructor.py +603 -0
  12. TSUMUGI/ontology_handler.py +62 -0
  13. TSUMUGI/pairwise_similarity_builder.py +66 -0
  14. TSUMUGI/report_generator.py +122 -0
  15. TSUMUGI/similarity_calculator.py +498 -0
  16. TSUMUGI/subcommands/count_filterer.py +47 -0
  17. TSUMUGI/subcommands/genes_filterer.py +89 -0
  18. TSUMUGI/subcommands/graphml_builder.py +158 -0
  19. TSUMUGI/subcommands/life_stage_filterer.py +48 -0
  20. TSUMUGI/subcommands/mp_filterer.py +142 -0
  21. TSUMUGI/subcommands/score_filterer.py +22 -0
  22. TSUMUGI/subcommands/sex_filterer.py +48 -0
  23. TSUMUGI/subcommands/webapp_builder.py +358 -0
  24. TSUMUGI/subcommands/zygosity_filterer.py +48 -0
  25. TSUMUGI/validator.py +65 -0
  26. TSUMUGI/web/app/css/app.css +1129 -0
  27. TSUMUGI/web/app/genelist/network_genelist.html +339 -0
  28. TSUMUGI/web/app/genelist/network_genelist.js +421 -0
  29. TSUMUGI/web/app/js/data/dataLoader.js +41 -0
  30. TSUMUGI/web/app/js/export/graphExporter.js +214 -0
  31. TSUMUGI/web/app/js/graph/centrality.js +495 -0
  32. TSUMUGI/web/app/js/graph/components.js +30 -0
  33. TSUMUGI/web/app/js/graph/filters.js +158 -0
  34. TSUMUGI/web/app/js/graph/highlighter.js +52 -0
  35. TSUMUGI/web/app/js/graph/layoutController.js +454 -0
  36. TSUMUGI/web/app/js/graph/valueScaler.js +43 -0
  37. TSUMUGI/web/app/js/search/geneSearcher.js +93 -0
  38. TSUMUGI/web/app/js/search/phenotypeSearcher.js +292 -0
  39. TSUMUGI/web/app/js/ui/dynamicFontSize.js +30 -0
  40. TSUMUGI/web/app/js/ui/mobilePanel.js +77 -0
  41. TSUMUGI/web/app/js/ui/slider.js +22 -0
  42. TSUMUGI/web/app/js/ui/tooltips.js +514 -0
  43. TSUMUGI/web/app/js/viewer/pageSetup.js +217 -0
  44. TSUMUGI/web/app/viewer.html +515 -0
  45. TSUMUGI/web/app/viewer.js +1593 -0
  46. TSUMUGI/web/css/sanitize.css +363 -0
  47. TSUMUGI/web/css/top.css +391 -0
  48. TSUMUGI/web/image/tsumugi-favicon.ico +0 -0
  49. TSUMUGI/web/image/tsumugi-icon.png +0 -0
  50. TSUMUGI/web/image/tsumugi-logo.png +0 -0
  51. TSUMUGI/web/image/tsumugi-logo.svg +69 -0
  52. TSUMUGI/web/js/genelist_formatter.js +123 -0
  53. TSUMUGI/web/js/top.js +338 -0
  54. TSUMUGI/web/open_webapp_linux.sh +25 -0
  55. TSUMUGI/web/open_webapp_mac.command +25 -0
  56. TSUMUGI/web/open_webapp_windows.bat +37 -0
  57. TSUMUGI/web/serve_index.py +110 -0
  58. TSUMUGI/web/template/template_index.html +197 -0
  59. TSUMUGI/web_deployer.py +150 -0
  60. tsumugi-1.0.1.dist-info/METADATA +504 -0
  61. tsumugi-1.0.1.dist-info/RECORD +64 -0
  62. tsumugi-1.0.1.dist-info/WHEEL +4 -0
  63. tsumugi-1.0.1.dist-info/entry_points.txt +3 -0
  64. tsumugi-1.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,495 @@
1
+ /**
2
+ * Count the number of phenotypes for a node, excluding empty entries.
3
+ * @param {Object} node - Cytoscape node
4
+ * @returns {number} Number of phenotypes associated with the node
5
+ */
6
+ function countNodePhenotypes(node) {
7
+ const nodeData = node.data();
8
+ const phenotypes = nodeData.phenotype || [];
9
+ const phenotypeArray = Array.isArray(phenotypes) ? phenotypes : [phenotypes];
10
+ return phenotypeArray.filter((p) => p && p !== "").length;
11
+ }
12
+
13
+ /**
14
+ * Calculate degree centrality for all visible nodes in the network
15
+ * @param {Object} cy - Cytoscape instance
16
+ * @returns {Map} Map of node id to degree centrality value
17
+ */
18
+ export function calculateDegreeCentrality(cy) {
19
+ const degreeCentrality = new Map();
20
+
21
+ // Get only visible nodes
22
+ const visibleNodes = cy.nodes().filter(node => node.style('display') === 'element');
23
+
24
+ visibleNodes.forEach(node => {
25
+ // Count only visible edges
26
+ const visibleEdges = node.connectedEdges().filter(edge => edge.style('display') === 'element');
27
+ const degree = visibleEdges.length;
28
+ degreeCentrality.set(node.id(), degree);
29
+ });
30
+
31
+ return degreeCentrality;
32
+ }
33
+
34
+ /**
35
+ * Calculate betweenness centrality using Brandes algorithm
36
+ * Brandes' algorithm is O(n*m) for unweighted graphs where n = nodes, m = edges
37
+ * @param {Object} cy - Cytoscape instance
38
+ * @returns {Map} Map of node id to betweenness centrality value
39
+ */
40
+ export function calculateBetweennessCentrality(cy) {
41
+ const visibleNodes = cy.nodes().filter(node => node.style('display') === 'element');
42
+ const betweennessCentrality = new Map();
43
+
44
+ // Initialize all nodes with 0
45
+ visibleNodes.forEach(node => {
46
+ betweennessCentrality.set(node.id(), 0);
47
+ });
48
+
49
+ // If there are less than 3 nodes, betweenness centrality is 0 for all
50
+ if (visibleNodes.length < 3) {
51
+ return betweennessCentrality;
52
+ }
53
+
54
+ // Build adjacency list for visible nodes only
55
+ const adjacencyList = new Map();
56
+ visibleNodes.forEach(node => {
57
+ adjacencyList.set(node.id(), []);
58
+ });
59
+
60
+ // Add edges to adjacency list (only if both nodes are visible)
61
+ cy.edges().filter(edge => edge.style('display') === 'element').forEach(edge => {
62
+ const source = edge.source().id();
63
+ const target = edge.target().id();
64
+ if (adjacencyList.has(source) && adjacencyList.has(target)) {
65
+ adjacencyList.get(source).push(target);
66
+ adjacencyList.get(target).push(source); // Undirected graph
67
+ }
68
+ });
69
+
70
+ // Brandes algorithm main loop
71
+ visibleNodes.forEach(s => {
72
+ const S = []; // Stack
73
+ const P = new Map(); // Predecessors
74
+ const sigma = new Map(); // Number of shortest paths
75
+ const d = new Map(); // Distance
76
+ const delta = new Map(); // Dependency
77
+
78
+ // Initialize
79
+ visibleNodes.forEach(node => {
80
+ P.set(node.id(), []);
81
+ sigma.set(node.id(), 0);
82
+ d.set(node.id(), -1);
83
+ delta.set(node.id(), 0);
84
+ });
85
+
86
+ sigma.set(s.id(), 1);
87
+ d.set(s.id(), 0);
88
+
89
+ // BFS
90
+ const Q = [s.id()];
91
+ while (Q.length > 0) {
92
+ const v = Q.shift();
93
+ S.push(v);
94
+
95
+ const neighbors = adjacencyList.get(v) || [];
96
+ neighbors.forEach(w => {
97
+ // First time we reach w?
98
+ if (d.get(w) < 0) {
99
+ Q.push(w);
100
+ d.set(w, d.get(v) + 1);
101
+ }
102
+ // Shortest path to w via v?
103
+ if (d.get(w) === d.get(v) + 1) {
104
+ sigma.set(w, sigma.get(w) + sigma.get(v));
105
+ P.get(w).push(v);
106
+ }
107
+ });
108
+ }
109
+
110
+ // Accumulation - back propagation of dependencies
111
+ while (S.length > 0) {
112
+ const w = S.pop();
113
+ const predecessors = P.get(w) || [];
114
+ predecessors.forEach(v => {
115
+ const sigmav = sigma.get(v) || 1;
116
+ const sigmaw = sigma.get(w) || 1;
117
+ const deltav = delta.get(v) || 0;
118
+ const deltaw = delta.get(w) || 0;
119
+ delta.set(v, deltav + (sigmav / sigmaw) * (1 + deltaw));
120
+ });
121
+ if (w !== s.id()) {
122
+ const current = betweennessCentrality.get(w) || 0;
123
+ betweennessCentrality.set(w, current + delta.get(w));
124
+ }
125
+ }
126
+ });
127
+
128
+ // For undirected graphs, divide by 2
129
+ betweennessCentrality.forEach((value, key) => {
130
+ betweennessCentrality.set(key, value / 2);
131
+ });
132
+
133
+ // Normalize by the maximum possible betweenness centrality: (n-1)(n-2)/2 for undirected graphs
134
+ const n = visibleNodes.length;
135
+ if (n > 2) {
136
+ const normalizationFactor = ((n - 1) * (n - 2)) / 2;
137
+ betweennessCentrality.forEach((value, key) => {
138
+ betweennessCentrality.set(key, value / normalizationFactor);
139
+ });
140
+ }
141
+
142
+ return betweennessCentrality;
143
+ }
144
+
145
+ /**
146
+ * Calculate normalized degree centrality (degree divided by phenotype count)
147
+ * @param {Object} cy - Cytoscape instance
148
+ * @returns {Map} Map of node id to normalized degree centrality value
149
+ */
150
+ export function calculateNormalizedDegreeCentrality(cy) {
151
+ const normalizedDegreeCentrality = new Map();
152
+ const degreeCentrality = calculateDegreeCentrality(cy);
153
+ const visibleNodes = cy.nodes().filter(node => node.style('display') === 'element');
154
+
155
+ visibleNodes.forEach(node => {
156
+ const degree = degreeCentrality.get(node.id()) || 0;
157
+ const phenotypeCount = countNodePhenotypes(node);
158
+ const normalizedDegree = phenotypeCount > 0 ? degree / phenotypeCount : 0;
159
+ normalizedDegreeCentrality.set(node.id(), normalizedDegree);
160
+ });
161
+
162
+ return normalizedDegreeCentrality;
163
+ }
164
+
165
+ /**
166
+ * Calculate normalized betweenness centrality (betweenness divided by phenotype count)
167
+ * @param {Object} cy - Cytoscape instance
168
+ * @returns {Map} Map of node id to normalized betweenness centrality value
169
+ */
170
+ export function calculateNormalizedBetweennessCentrality(cy) {
171
+ const normalizedBetweennessCentrality = new Map();
172
+ const betweennessCentrality = calculateBetweennessCentrality(cy);
173
+ const visibleNodes = cy.nodes().filter(node => node.style('display') === 'element');
174
+
175
+ visibleNodes.forEach(node => {
176
+ const betweenness = betweennessCentrality.get(node.id()) || 0;
177
+ const phenotypeCount = countNodePhenotypes(node);
178
+ const normalizedBetweenness = phenotypeCount > 0 ? betweenness / phenotypeCount : 0;
179
+ normalizedBetweennessCentrality.set(node.id(), normalizedBetweenness);
180
+ });
181
+
182
+ return normalizedBetweennessCentrality;
183
+ }
184
+
185
+ /**
186
+ * Update node data with centrality values
187
+ * @param {Object} cy - Cytoscape instance
188
+ * @param {Map} centralityMap - Map of node id to centrality value
189
+ * @param {string} centralityType - Type of centrality ('degree' or 'betweenness')
190
+ */
191
+ export function updateNodeCentrality(cy, centralityMap, centralityType) {
192
+ cy.nodes().forEach(node => {
193
+ const centralityValue = centralityMap.get(node.id()) || 0;
194
+ node.data(`${centralityType}_centrality`, centralityValue);
195
+ });
196
+ }
197
+
198
+ /**
199
+ * Get min and max centrality values from visible nodes
200
+ * @param {Object} cy - Cytoscape instance
201
+ * @param {string} centralityType - Type of centrality ('degree' or 'betweenness')
202
+ * @returns {Object} Object with min and max values
203
+ */
204
+ export function getCentralityRange(cy, centralityType) {
205
+ const visibleNodes = cy.nodes().filter(node => node.style('display') === 'element');
206
+ const values = visibleNodes.map(node => node.data(`${centralityType}_centrality`) || 0);
207
+
208
+ if (values.length === 0) {
209
+ return { min: 0, max: 0 };
210
+ }
211
+
212
+ return {
213
+ min: Math.min(...values),
214
+ max: Math.max(...values)
215
+ };
216
+ }
217
+
218
+ // ############################################################################
219
+ // Centrality UI state and management
220
+ // ############################################################################
221
+
222
+ let centralityType = 'normalized_degree'; // active options: none, degree, betweenness, normalized_degree, normalized_betweenness
223
+ let centralityScale = 0; // 0 to 1 scale factor
224
+ let cytoscapeInstance = null;
225
+ let createSliderFunction = null;
226
+ let isCentralityInputBound = false;
227
+
228
+ function clampNumber(value, min, max) {
229
+ if (!Number.isFinite(value)) {
230
+ return min;
231
+ }
232
+ return Math.min(Math.max(value, min), max);
233
+ }
234
+
235
+ function setupCentralityInput(sliderInstance) {
236
+ if (!sliderInstance || isCentralityInputBound) return;
237
+ const input = document.getElementById("centrality-scale-input");
238
+ if (!input) return;
239
+
240
+ const rangeMin = 0;
241
+ const rangeMax = 100;
242
+
243
+ input.min = rangeMin;
244
+ input.max = rangeMax;
245
+ input.step = 1;
246
+
247
+ const initial = Math.round(Number(sliderInstance.get()));
248
+ if (Number.isFinite(initial)) {
249
+ input.value = initial;
250
+ }
251
+
252
+ const commit = () => {
253
+ const currentValue = Math.round(Number(sliderInstance.get()));
254
+ let value = Number(input.value);
255
+ if (!Number.isFinite(value)) {
256
+ value = currentValue;
257
+ }
258
+ value = clampNumber(value, rangeMin, rangeMax);
259
+ value = Math.round(value);
260
+ input.value = value;
261
+ sliderInstance.set(value);
262
+ };
263
+
264
+ ["change", "blur"].forEach((eventName) => {
265
+ input.addEventListener(eventName, () => commit());
266
+ });
267
+
268
+ input.addEventListener("keydown", (event) => {
269
+ if (event.key === "Enter") {
270
+ event.preventDefault();
271
+ commit();
272
+ }
273
+ });
274
+
275
+ sliderInstance.on("update", (value) => {
276
+ const nextValue = Math.round(Number(Array.isArray(value) ? value[0] : value));
277
+ if (Number.isFinite(nextValue) && input.value !== String(nextValue)) {
278
+ input.value = nextValue;
279
+ }
280
+ });
281
+
282
+ isCentralityInputBound = true;
283
+ }
284
+
285
+ /**
286
+ * Initialize centrality system with dependencies
287
+ * @param {Object} cy - Cytoscape instance
288
+ * @param {Function} createSlider - Slider creation function
289
+ */
290
+ export function initializeCentralitySystem(cy, createSlider) {
291
+ cytoscapeInstance = cy;
292
+ createSliderFunction = createSlider;
293
+
294
+ // Set up global handler for HTML onchange
295
+ window.handleCentralityTypeChange = handleCentralityTypeChange;
296
+
297
+ // Initialize controls
298
+ initializeCentralityControls();
299
+ const centralityDropdown = document.getElementById("centrality-type-dropdown");
300
+ if (centralityDropdown) {
301
+ centralityDropdown.value = centralityType;
302
+ }
303
+ handleCentralityTypeChange(centralityType);
304
+
305
+ // Calculate initial centrality values
306
+ setTimeout(() => {
307
+ recalculateCentrality();
308
+ }, 500);
309
+ }
310
+
311
+ /**
312
+ * Handle centrality type change from dropdown
313
+ * @param {string} value - Selected centrality type
314
+ */
315
+ function handleCentralityTypeChange(value) {
316
+ centralityType = value;
317
+
318
+ const container = document.getElementById("centrality-slider-container");
319
+ if (container) {
320
+ if (value === 'none') {
321
+ container.style.display = 'none';
322
+ centralityScale = 0;
323
+ if (window.centralitySliderInstance) {
324
+ window.centralitySliderInstance.set(0);
325
+ }
326
+ } else {
327
+ container.style.display = 'block';
328
+ // Initialize slider if not already done
329
+ if (!window.centralitySliderInstance) {
330
+ initializeCentralitySlider();
331
+ }
332
+ }
333
+ updateNodeSizeByCentrality();
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Initialize the centrality scale slider
339
+ */
340
+ function initializeCentralitySlider() {
341
+ const sliderElement = document.getElementById("centrality-scale-slider");
342
+ if (sliderElement && createSliderFunction) {
343
+ const sliderInstance = createSliderFunction("centrality-scale-slider", 0, 0, 100, 1, (value) => {
344
+ // Convert 0-100 integer to 0.00-1.00 for internal calculation
345
+ centralityScale = parseInt(value) / 100;
346
+ const input = document.getElementById("centrality-scale-input");
347
+ if (input) {
348
+ input.value = parseInt(value);
349
+ }
350
+ updateNodeSizeByCentrality();
351
+ });
352
+ window.centralitySliderInstance = sliderInstance;
353
+ setupCentralityInput(sliderInstance);
354
+ } else {
355
+ console.error("Centrality slider element not found or createSlider function not available");
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Calculate and update centrality values for all visible nodes
361
+ */
362
+ export function recalculateCentrality() {
363
+ if (!cytoscapeInstance) {
364
+ console.error("Cytoscape instance not available for centrality calculation");
365
+ return;
366
+ }
367
+
368
+ // Calculate centrality for visible nodes
369
+ const degreeCentrality = calculateDegreeCentrality(cytoscapeInstance);
370
+ const betweennessCentrality = calculateBetweennessCentrality(cytoscapeInstance);
371
+ const normalizedDegreeCentrality = calculateNormalizedDegreeCentrality(cytoscapeInstance);
372
+ const normalizedBetweennessCentrality = calculateNormalizedBetweennessCentrality(cytoscapeInstance);
373
+
374
+ // Update node data with centrality values
375
+ updateNodeCentrality(cytoscapeInstance, degreeCentrality, 'degree');
376
+ updateNodeCentrality(cytoscapeInstance, betweennessCentrality, 'betweenness');
377
+ updateNodeCentrality(cytoscapeInstance, normalizedDegreeCentrality, 'normalized_degree');
378
+ updateNodeCentrality(cytoscapeInstance, normalizedBetweennessCentrality, 'normalized_betweenness');
379
+
380
+ // Apply node size updates if sliders are active
381
+ updateNodeSizeByCentrality();
382
+ }
383
+
384
+ /**
385
+ * Update node sizes based on current centrality settings
386
+ */
387
+ function updateNodeSizeByCentrality() {
388
+ if (!cytoscapeInstance) {
389
+ return;
390
+ }
391
+
392
+ const baseSize = 15; // Consistent base size
393
+
394
+ if (centralityType === 'none' || centralityScale === 0) {
395
+ // Reset all nodes to default size
396
+ cytoscapeInstance.nodes().forEach(node => {
397
+ node.style({
398
+ 'width': baseSize,
399
+ 'height': baseSize
400
+ });
401
+ });
402
+ return;
403
+ }
404
+
405
+ const centralityRange = getCentralityRange(cytoscapeInstance, centralityType);
406
+
407
+ cytoscapeInstance.nodes().forEach(node => {
408
+ let size = baseSize; // Start with base size
409
+
410
+ if (centralityType === 'degree' && centralityRange.max > centralityRange.min) {
411
+ const degreeCentrality = node.data('degree_centrality') || 0;
412
+ const normalizedDegree = (degreeCentrality - centralityRange.min) / (centralityRange.max - centralityRange.min);
413
+ // Add scaling on top of base size
414
+ size = baseSize + (normalizedDegree * 35 * centralityScale);
415
+ } else if (centralityType === 'betweenness') {
416
+ const betweennessCentrality = node.data('betweenness_centrality') || 0;
417
+
418
+ // Use logarithmic scaling for better differentiation
419
+ // Add 1 to avoid log(0) and ensure nodes with 0 centrality have minimum size
420
+ const logCentrality = Math.log10(betweennessCentrality + 1);
421
+ const maxLogCentrality = Math.log10((centralityRange.max || 1) + 1);
422
+
423
+ // Normalize using log scale
424
+ const normalizedBetweenness = maxLogCentrality > 0 ? logCentrality / maxLogCentrality : 0;
425
+
426
+ // Add scaling on top of base size
427
+ const scalingFactor = normalizedBetweenness * 35 * centralityScale;
428
+ size = baseSize + scalingFactor;
429
+
430
+ // Ensure nodes with centrality > 0 are visually distinct from those with 0
431
+ // Only apply minimum boost if there's actual scaling happening
432
+ if (betweennessCentrality > 0 && centralityScale > 0 && scalingFactor < 2) {
433
+ size = baseSize + 2; // Minimum visible increase for non-zero centrality
434
+ }
435
+ } else if (centralityType === 'normalized_degree' && centralityRange.max > centralityRange.min) {
436
+ const normalizedDegreeCentrality = node.data('normalized_degree_centrality') || 0;
437
+ const normalizedValue = (normalizedDegreeCentrality - centralityRange.min) / (centralityRange.max - centralityRange.min);
438
+ size = baseSize + (normalizedValue * 35 * centralityScale);
439
+ } else if (centralityType === 'normalized_betweenness') {
440
+ const normalizedBetweennessCentrality = node.data('normalized_betweenness_centrality') || 0;
441
+ const epsilon = 0.001;
442
+ const logCentrality = Math.log10(normalizedBetweennessCentrality + epsilon);
443
+ const maxLogCentrality = Math.log10((centralityRange.max || epsilon) + epsilon);
444
+ const minLogCentrality = Math.log10(epsilon);
445
+ const normalizedLog = maxLogCentrality > minLogCentrality
446
+ ? (logCentrality - minLogCentrality) / (maxLogCentrality - minLogCentrality)
447
+ : 0;
448
+ const scalingFactor = normalizedLog * 35 * centralityScale;
449
+ size = baseSize + scalingFactor;
450
+
451
+ if (normalizedBetweennessCentrality > 0 && centralityScale > 0 && scalingFactor < 2) {
452
+ size = baseSize + 2;
453
+ }
454
+ }
455
+
456
+ node.style({
457
+ 'width': size,
458
+ 'height': size
459
+ });
460
+ });
461
+ }
462
+
463
+ /**
464
+ * Initialize centrality controls in the DOM
465
+ */
466
+ function initializeCentralityControls() {
467
+
468
+ const centralityDropdown = document.getElementById("centrality-type-dropdown");
469
+
470
+ if (centralityDropdown) {
471
+ if (typeof window.handleCentralityTypeChange !== 'function') {
472
+ console.error("Global handler not available");
473
+ }
474
+ } else {
475
+ console.error("Centrality dropdown not found");
476
+ }
477
+ }
478
+
479
+ /**
480
+ * Set up DOM event listeners for centrality controls
481
+ */
482
+ function setupCentralityEventListeners() {
483
+ // Initialize centrality controls
484
+ if (document.readyState === 'loading') {
485
+ document.addEventListener('DOMContentLoaded', initializeCentralityControls);
486
+ } else {
487
+ initializeCentralityControls();
488
+ }
489
+
490
+ // Backup initialization
491
+ setTimeout(initializeCentralityControls, 500);
492
+ }
493
+
494
+ // Set up event listeners when module is imported
495
+ setupCentralityEventListeners();
@@ -0,0 +1,30 @@
1
+ function getComponentSortKey(component) {
2
+ const labels = component
3
+ .nodes()
4
+ .map((node) => node.data("label") || node.id())
5
+ .filter(Boolean)
6
+ .sort((a, b) => a.localeCompare(b));
7
+ return labels[0] || "";
8
+ }
9
+
10
+ export function getOrderedComponents(cy) {
11
+ const visibleElements = cy.elements(":visible");
12
+ const connectedComponents = visibleElements.components();
13
+ return connectedComponents.sort((a, b) => getComponentSortKey(a).localeCompare(getComponentSortKey(b)));
14
+ }
15
+
16
+ export function calculateConnectedComponents(cy) {
17
+ const orderedComponents = getOrderedComponents(cy);
18
+
19
+ return orderedComponents.map((component) => {
20
+ let componentObject = {};
21
+ component.nodes().forEach((node) => {
22
+ const nodeLabel = node.data("label");
23
+ const nodePhenotypes = Array.isArray(node.data("phenotype"))
24
+ ? node.data("phenotype")
25
+ : [node.data("phenotype")];
26
+ componentObject[nodeLabel] = nodePhenotypes;
27
+ });
28
+ return componentObject;
29
+ });
30
+ }
@@ -0,0 +1,158 @@
1
+ import { highlightDiseaseNodes } from "./highlighter.js";
2
+
3
+ // ========================================
4
+ // Helpers for restoring highlight states
5
+ // ========================================
6
+
7
+ function restoreHighlightStates(cy) {
8
+ // Restore Human Disease highlighting if it was enabled
9
+ const isDiseaseChecked = document.querySelector('#human-disease-filter-form input[type="checkbox"]:checked');
10
+ if (isDiseaseChecked) {
11
+ // Reapply highlighting by delegating to highlighter.js
12
+ highlightDiseaseNodes(cy);
13
+ }
14
+
15
+ // Restore highlights from the gene search box
16
+ const geneSearchInput = document.getElementById("gene-search");
17
+ if (geneSearchInput && geneSearchInput.value.trim() !== "") {
18
+ const searchTerm = geneSearchInput.value.trim().toLowerCase();
19
+ const matchedNode = cy.nodes().filter((node) => node.data("label").toLowerCase() === searchTerm);
20
+
21
+ if (matchedNode.length > 0) {
22
+ matchedNode.addClass("gene-highlight");
23
+ }
24
+ }
25
+
26
+ // Restore phenotype-based highlights
27
+ if (window.updatePhenotypeHighlight) {
28
+ window.updatePhenotypeHighlight();
29
+ }
30
+
31
+ // Refresh the phenotype list so it reflects the filtered genes
32
+ if (window.refreshPhenotypeList) {
33
+ window.refreshPhenotypeList();
34
+ }
35
+ }
36
+
37
+ function getActiveFilterValues(formSelector, allValues) {
38
+ const checkedInputs = Array.from(document.querySelectorAll(`${formSelector} input[type="checkbox"]:checked`));
39
+ if (checkedInputs.length === 0) {
40
+ return allValues;
41
+ }
42
+
43
+ const checkedValues = checkedInputs.map((input) => input.value);
44
+ if (checkedValues.includes("All")) {
45
+ return allValues;
46
+ }
47
+
48
+ return checkedValues;
49
+ }
50
+
51
+ /**
52
+ * Apply genotype/sex/life-stage filters to phenotypes.
53
+ */
54
+ export function filterElementsByGenotypeAndSex(elements, cy, targetPhenotype, filterElements) {
55
+ const allSexes = ["Female", "Male"];
56
+ const allGenotypes = ["Homo", "Hetero", "Hemi"];
57
+ const allLifeStages = ["Embryo", "Early", "Interval", "Late"];
58
+
59
+ const checkedSexes = getActiveFilterValues("#sex-filter-form", allSexes);
60
+ const checkedGenotypes = getActiveFilterValues("#genotype-filter-form", allGenotypes);
61
+ const checkedLifeStages = getActiveFilterValues("#lifestage-filter-form", allLifeStages);
62
+
63
+ let filteredElements = elements.map((item) => {
64
+ const basePhenotypes = Array.isArray(item.data.originalPhenotypes)
65
+ ? item.data.originalPhenotypes
66
+ : item.data.phenotype;
67
+ const phenotypeList = Array.isArray(basePhenotypes)
68
+ ? [...basePhenotypes]
69
+ : basePhenotypes
70
+ ? [basePhenotypes]
71
+ : [];
72
+
73
+ return {
74
+ ...item,
75
+ data: {
76
+ ...item.data,
77
+ originalPhenotypes: phenotypeList, // Preserve the original phenotype list
78
+ phenotype: phenotypeList,
79
+ },
80
+ };
81
+ });
82
+
83
+ // Apply sex filters
84
+ if (checkedSexes.length !== allSexes.length) {
85
+ filteredElements = filteredElements
86
+ .map((item) => {
87
+ const filtered = item.data.phenotype.filter((phenotype) =>
88
+ checkedSexes.some((sex) => phenotype.includes(sex)),
89
+ );
90
+ return {
91
+ ...item,
92
+ data: { ...item.data, phenotype: filtered },
93
+ };
94
+ })
95
+ .filter((item) => item.data.phenotype.length > 0);
96
+ }
97
+
98
+ // Apply genotype filters
99
+ if (checkedGenotypes.length !== allGenotypes.length) {
100
+ filteredElements = filteredElements
101
+ .map((item) => {
102
+ const filtered = item.data.phenotype.filter((phenotype) =>
103
+ checkedGenotypes.some((gt) => phenotype.includes(gt)),
104
+ );
105
+ return {
106
+ ...item,
107
+ data: { ...item.data, phenotype: filtered },
108
+ };
109
+ })
110
+ .filter((item) => item.data.phenotype.length > 0);
111
+ }
112
+
113
+ // Apply life-stage filters
114
+ if (checkedLifeStages.length !== allLifeStages.length) {
115
+ filteredElements = filteredElements
116
+ .map((item) => {
117
+ const filtered = item.data.phenotype.filter((phenotype) =>
118
+ checkedLifeStages.some((stage) => phenotype.includes(stage)),
119
+ );
120
+ return {
121
+ ...item,
122
+ data: { ...item.data, phenotype: filtered },
123
+ };
124
+ })
125
+ .filter((item) => item.data.phenotype.length > 0);
126
+ }
127
+
128
+ // Keep elements with at least one phenotype to avoid dropping valid single-annotation nodes
129
+ filteredElements = filteredElements.filter((item) => item.data.phenotype && item.data.phenotype.length > 0);
130
+
131
+ // Remove nodes that do not contain the target phenotype (edges are filtered later)
132
+ if (targetPhenotype) {
133
+ filteredElements = filteredElements.filter((item) => {
134
+ if (item.data && item.data.source) {
135
+ return true;
136
+ }
137
+ return item.data.phenotype.some((anno) => anno.includes(targetPhenotype));
138
+ });
139
+ }
140
+
141
+ // Remove edges that are disconnected from the remaining nodes
142
+ const nodeIds = new Set(
143
+ filteredElements.filter((item) => item.data && item.data.id).map((item) => item.data.id),
144
+ );
145
+ filteredElements = filteredElements.filter((item) => {
146
+ if (!item.data || !item.data.source) {
147
+ return true;
148
+ }
149
+ return nodeIds.has(item.data.source) && nodeIds.has(item.data.target);
150
+ });
151
+
152
+ // Replace the Cytoscape elements and apply the filter-specific adjustments
153
+ cy.elements().remove();
154
+ cy.add(filteredElements);
155
+ filterElements();
156
+
157
+ restoreHighlightStates(cy);
158
+ }