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.
- TSUMUGI/annotator.py +103 -0
- TSUMUGI/argparser.py +599 -0
- TSUMUGI/core.py +185 -0
- TSUMUGI/data/impc_phenodigm.csv +3406 -0
- TSUMUGI/data/mp.obo +143993 -0
- TSUMUGI/filterer.py +36 -0
- TSUMUGI/formatter.py +122 -0
- TSUMUGI/genewise_annotation_builder.py +94 -0
- TSUMUGI/io_handler.py +189 -0
- TSUMUGI/main.py +300 -0
- TSUMUGI/network_constructor.py +603 -0
- TSUMUGI/ontology_handler.py +62 -0
- TSUMUGI/pairwise_similarity_builder.py +66 -0
- TSUMUGI/report_generator.py +122 -0
- TSUMUGI/similarity_calculator.py +498 -0
- TSUMUGI/subcommands/count_filterer.py +47 -0
- TSUMUGI/subcommands/genes_filterer.py +89 -0
- TSUMUGI/subcommands/graphml_builder.py +158 -0
- TSUMUGI/subcommands/life_stage_filterer.py +48 -0
- TSUMUGI/subcommands/mp_filterer.py +142 -0
- TSUMUGI/subcommands/score_filterer.py +22 -0
- TSUMUGI/subcommands/sex_filterer.py +48 -0
- TSUMUGI/subcommands/webapp_builder.py +358 -0
- TSUMUGI/subcommands/zygosity_filterer.py +48 -0
- TSUMUGI/validator.py +65 -0
- TSUMUGI/web/app/css/app.css +1129 -0
- TSUMUGI/web/app/genelist/network_genelist.html +339 -0
- TSUMUGI/web/app/genelist/network_genelist.js +421 -0
- TSUMUGI/web/app/js/data/dataLoader.js +41 -0
- TSUMUGI/web/app/js/export/graphExporter.js +214 -0
- TSUMUGI/web/app/js/graph/centrality.js +495 -0
- TSUMUGI/web/app/js/graph/components.js +30 -0
- TSUMUGI/web/app/js/graph/filters.js +158 -0
- TSUMUGI/web/app/js/graph/highlighter.js +52 -0
- TSUMUGI/web/app/js/graph/layoutController.js +454 -0
- TSUMUGI/web/app/js/graph/valueScaler.js +43 -0
- TSUMUGI/web/app/js/search/geneSearcher.js +93 -0
- TSUMUGI/web/app/js/search/phenotypeSearcher.js +292 -0
- TSUMUGI/web/app/js/ui/dynamicFontSize.js +30 -0
- TSUMUGI/web/app/js/ui/mobilePanel.js +77 -0
- TSUMUGI/web/app/js/ui/slider.js +22 -0
- TSUMUGI/web/app/js/ui/tooltips.js +514 -0
- TSUMUGI/web/app/js/viewer/pageSetup.js +217 -0
- TSUMUGI/web/app/viewer.html +515 -0
- TSUMUGI/web/app/viewer.js +1593 -0
- TSUMUGI/web/css/sanitize.css +363 -0
- TSUMUGI/web/css/top.css +391 -0
- TSUMUGI/web/image/tsumugi-favicon.ico +0 -0
- TSUMUGI/web/image/tsumugi-icon.png +0 -0
- TSUMUGI/web/image/tsumugi-logo.png +0 -0
- TSUMUGI/web/image/tsumugi-logo.svg +69 -0
- TSUMUGI/web/js/genelist_formatter.js +123 -0
- TSUMUGI/web/js/top.js +338 -0
- TSUMUGI/web/open_webapp_linux.sh +25 -0
- TSUMUGI/web/open_webapp_mac.command +25 -0
- TSUMUGI/web/open_webapp_windows.bat +37 -0
- TSUMUGI/web/serve_index.py +110 -0
- TSUMUGI/web/template/template_index.html +197 -0
- TSUMUGI/web_deployer.py +150 -0
- tsumugi-1.0.1.dist-info/METADATA +504 -0
- tsumugi-1.0.1.dist-info/RECORD +64 -0
- tsumugi-1.0.1.dist-info/WHEEL +4 -0
- tsumugi-1.0.1.dist-info/entry_points.txt +3 -0
- 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
|
+
}
|