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,421 @@
|
|
|
1
|
+
import { exportGraphAsPNG, exportGraphAsJPG, exportGraphAsCSV, exportGraphAsGraphML } from "../js/export/graphExporter.js";
|
|
2
|
+
import { scaleToOriginalRange, scaleValue, getColorForValue } from "../js/graph/valueScaler.js";
|
|
3
|
+
import { initInfoTooltips, removeTooltips, showTooltip } from "../js/ui/tooltips.js";
|
|
4
|
+
import { calculateConnectedComponents } from "../js/graph/components.js";
|
|
5
|
+
import { createSlider } from "../js/ui/slider.js";
|
|
6
|
+
import { filterElementsByGenotypeAndSex } from "../js/graph/filters.js";
|
|
7
|
+
import { loadJSON } from "../js/data/dataLoader.js";
|
|
8
|
+
import { setupGeneSearch } from "../js/search/geneSearcher.js";
|
|
9
|
+
import { highlightDiseaseAnnotation } from "../js/graph/highlighter.js";
|
|
10
|
+
import { setupPhenotypeSearch } from "../js/search/phenotypeSearcher.js";
|
|
11
|
+
import { initDynamicFontSize } from "../js/ui/dynamicFontSize.js";
|
|
12
|
+
import { initMobilePanel } from "../js/ui/mobilePanel.js";
|
|
13
|
+
|
|
14
|
+
// Initialize DOM-dependent UI helpers.
|
|
15
|
+
initInfoTooltips();
|
|
16
|
+
initDynamicFontSize();
|
|
17
|
+
initMobilePanel();
|
|
18
|
+
|
|
19
|
+
// ############################################################################
|
|
20
|
+
// Input handler
|
|
21
|
+
// ############################################################################
|
|
22
|
+
|
|
23
|
+
// REMOVE_FROM_THIS_LINE
|
|
24
|
+
|
|
25
|
+
// const elements = [
|
|
26
|
+
// { data: { id: 'Nanog', label: 'Nanog', phenotype: ['hoge', 'hooo'], node_color: 50, } },
|
|
27
|
+
// { data: { id: 'Pou5f1', label: 'Pou5f1', phenotype: 'fuga', node_color: 100, } },
|
|
28
|
+
// { data: { id: 'Sox2', label: 'Sox2', phenotype: 'foo', node_color: 3, } },
|
|
29
|
+
// { data: { source: 'Nanog', target: 'Pou5f1', phenotype: ['Foo', 'FooBar'], edge_size: 5 } },
|
|
30
|
+
// { data: { source: 'Nanog', target: 'Sox2', phenotype: 'FooBar', edge_size: 1 } },
|
|
31
|
+
// { data: { source: 'Sox2', target: 'Pou5f1', phenotype: 'FooBar', edge_size: 10 } },
|
|
32
|
+
// ];
|
|
33
|
+
|
|
34
|
+
// const mapSymbolToId = { 'Nanog': 'MGI:97281', 'Pou5f1': 'MGI:1352748', 'Sox2': 'MGI:96217' };
|
|
35
|
+
|
|
36
|
+
// REMOVE_TO_THIS_LINE
|
|
37
|
+
|
|
38
|
+
const elements = JSON.parse(localStorage.getItem("elements"));
|
|
39
|
+
const mapSymbolToId = loadJSON("../../data/marker_symbol_accession_id.json");
|
|
40
|
+
|
|
41
|
+
// ############################################################################
|
|
42
|
+
// Cytoscape Elements handler
|
|
43
|
+
// ############################################################################
|
|
44
|
+
|
|
45
|
+
const nodeColorValues = elements
|
|
46
|
+
.filter((ele) => ele.data.node_color !== undefined)
|
|
47
|
+
.map((ele) => ele.data.node_color);
|
|
48
|
+
const nodeColorMin = Math.min(...nodeColorValues); // Range used for color styling
|
|
49
|
+
const nodeColorMax = Math.max(...nodeColorValues); // Range used for color styling
|
|
50
|
+
|
|
51
|
+
// Copy the original range so filtering can adjust independently
|
|
52
|
+
let nodeMin = nodeColorMin;
|
|
53
|
+
let nodeMax = nodeColorMax;
|
|
54
|
+
|
|
55
|
+
const edgeSizes = elements.filter((ele) => ele.data.edge_size !== undefined).map((ele) => ele.data.edge_size);
|
|
56
|
+
|
|
57
|
+
const edgeMin = Math.min(...edgeSizes);
|
|
58
|
+
const edgeMax = Math.max(...edgeSizes);
|
|
59
|
+
|
|
60
|
+
// ############################################################################
|
|
61
|
+
// Initialize Cytoscape
|
|
62
|
+
// ############################################################################
|
|
63
|
+
|
|
64
|
+
let currentLayout = "cose";
|
|
65
|
+
|
|
66
|
+
const nodeRepulsionMin = 1;
|
|
67
|
+
const nodeRepulsionMax = 10000;
|
|
68
|
+
const componentSpacingMin = 1;
|
|
69
|
+
const componentSpacingMax = 200;
|
|
70
|
+
|
|
71
|
+
let nodeRepulsionValue = scaleToOriginalRange(
|
|
72
|
+
parseFloat(document.getElementById("nodeRepulsion-slider").value),
|
|
73
|
+
nodeRepulsionMin,
|
|
74
|
+
nodeRepulsionMax,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
let componentSpacingValue = scaleToOriginalRange(
|
|
78
|
+
parseFloat(document.getElementById("nodeRepulsion-slider").value),
|
|
79
|
+
componentSpacingMin,
|
|
80
|
+
componentSpacingMax,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
function getLayoutOptions() {
|
|
84
|
+
return {
|
|
85
|
+
name: currentLayout,
|
|
86
|
+
nodeRepulsion: nodeRepulsionValue,
|
|
87
|
+
componentSpacing: componentSpacingValue,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const cy = cytoscape({
|
|
92
|
+
container: document.querySelector(".cy"),
|
|
93
|
+
elements: elements,
|
|
94
|
+
style: [
|
|
95
|
+
{
|
|
96
|
+
selector: "node",
|
|
97
|
+
style: {
|
|
98
|
+
label: "data(label)",
|
|
99
|
+
"text-valign": "center",
|
|
100
|
+
"text-halign": "center",
|
|
101
|
+
"font-size": "20px",
|
|
102
|
+
width: 15,
|
|
103
|
+
height: 15,
|
|
104
|
+
"background-color": function (ele) {
|
|
105
|
+
const originalColor = ele.data("original_node_color") || ele.data("node_color");
|
|
106
|
+
const color_value = scaleValue(originalColor, nodeColorMin, nodeColorMax, 1, 100);
|
|
107
|
+
return getColorForValue(color_value, 1, 100);
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
selector: "edge",
|
|
113
|
+
style: {
|
|
114
|
+
"curve-style": "bezier",
|
|
115
|
+
"text-rotation": "autorotate",
|
|
116
|
+
width: function (ele) {
|
|
117
|
+
return scaleValue(ele.data("edge_size"), edgeMin, edgeMax, 0.5, 2);
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
selector: ".disease-highlight", // Class used for disease highlighting
|
|
123
|
+
style: {
|
|
124
|
+
"border-width": 3,
|
|
125
|
+
"border-color": "#fc4c00",
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
selector: ".gene-highlight", // Class used when highlighting gene search hits
|
|
130
|
+
style: {
|
|
131
|
+
color: "#028760",
|
|
132
|
+
"font-weight": "bold",
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
selector: ".phenotype-highlight", // Class used for phenotype search highlighting
|
|
137
|
+
style: {
|
|
138
|
+
"border-width": 3,
|
|
139
|
+
"border-color": "#28a745",
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
layout: getLayoutOptions(),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// * Expose cy globally for debugging convenience
|
|
147
|
+
window.cy = cy;
|
|
148
|
+
|
|
149
|
+
// * Improve Cytoscape rendering on mobile devices
|
|
150
|
+
function handleMobileResize() {
|
|
151
|
+
if (cy) {
|
|
152
|
+
// Re-render Cytoscape after layout tweaks on mobile
|
|
153
|
+
setTimeout(() => {
|
|
154
|
+
cy.resize();
|
|
155
|
+
cy.fit();
|
|
156
|
+
cy.center();
|
|
157
|
+
}, 300);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Adjust Cytoscape once initialization finishes on mobile
|
|
162
|
+
setTimeout(() => {
|
|
163
|
+
if (window.innerWidth <= 600) {
|
|
164
|
+
cy.resize();
|
|
165
|
+
cy.fit();
|
|
166
|
+
cy.center();
|
|
167
|
+
}
|
|
168
|
+
}, 500);
|
|
169
|
+
|
|
170
|
+
// Handle browser resize events
|
|
171
|
+
window.addEventListener("resize", handleMobileResize);
|
|
172
|
+
|
|
173
|
+
// Handle orientation changes on mobile
|
|
174
|
+
window.addEventListener("orientationchange", () => {
|
|
175
|
+
setTimeout(handleMobileResize, 500);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ############################################################################
|
|
179
|
+
// Control panel handler
|
|
180
|
+
// ############################################################################
|
|
181
|
+
|
|
182
|
+
// --------------------------------------------------------
|
|
183
|
+
// Network layout dropdown
|
|
184
|
+
// --------------------------------------------------------
|
|
185
|
+
document.getElementById("layout-dropdown").addEventListener("change", function () {
|
|
186
|
+
currentLayout = this.value;
|
|
187
|
+
cy.layout({ name: currentLayout }).run();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// =============================================================================
|
|
191
|
+
// Slider initialization and filtering helpers
|
|
192
|
+
// =============================================================================
|
|
193
|
+
|
|
194
|
+
// --------------------------------------------------------
|
|
195
|
+
// Edge size slider for Phenotypes similarity
|
|
196
|
+
// --------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
// Initialization of the Edge size slider
|
|
199
|
+
const edgeSlider = document.getElementById("filter-edge-slider");
|
|
200
|
+
noUiSlider.create(edgeSlider, { start: [1, 100], connect: true, range: { min: 1, max: 100 }, step: 1 });
|
|
201
|
+
|
|
202
|
+
// Update the slider values when the sliders are moved
|
|
203
|
+
edgeSlider.noUiSlider.on("update", function (values) {
|
|
204
|
+
const intValues = values.map((value) => Math.round(value));
|
|
205
|
+
document.getElementById("edge-size-value").textContent = intValues.join(" - ");
|
|
206
|
+
filterByNodeColorAndEdgeSize();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// --------------------------------------------------------
|
|
210
|
+
// Modify the filter function to handle upper and lower bounds
|
|
211
|
+
// --------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
function filterByNodeColorAndEdgeSize() {
|
|
214
|
+
const edgeSliderValues = edgeSlider.noUiSlider.get().map(Number);
|
|
215
|
+
const edgeMinValue = scaleToOriginalRange(edgeSliderValues[0], edgeMin, edgeMax, 1, 100);
|
|
216
|
+
const edgeMaxValue = scaleToOriginalRange(edgeSliderValues[1], edgeMin, edgeMax, 1, 100);
|
|
217
|
+
|
|
218
|
+
// 1. Start by showing every node
|
|
219
|
+
cy.nodes().forEach((node) => node.style("display", "element"));
|
|
220
|
+
|
|
221
|
+
// 2. Show or hide edges according to the edge_size range
|
|
222
|
+
cy.edges().forEach((edge) => {
|
|
223
|
+
const edgeSize = edge.data("edge_size");
|
|
224
|
+
const sourceVisible = cy.getElementById(edge.data("source")).style("display") === "element";
|
|
225
|
+
const targetVisible = cy.getElementById(edge.data("target")).style("display") === "element";
|
|
226
|
+
const isVisible =
|
|
227
|
+
sourceVisible &&
|
|
228
|
+
targetVisible &&
|
|
229
|
+
edgeSize >= Math.min(edgeMinValue, edgeMaxValue) &&
|
|
230
|
+
edgeSize <= Math.max(edgeMinValue, edgeMaxValue);
|
|
231
|
+
edge.style("display", isVisible ? "element" : "none");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// 3. Keep only the connected components that contain a node with node_color === 1
|
|
235
|
+
const components = calculateConnectedComponents(cy);
|
|
236
|
+
const validComponents = components.filter((comp) =>
|
|
237
|
+
Object.keys(comp).some((label) => {
|
|
238
|
+
const node = cy.$(`node[label="${label}"]`);
|
|
239
|
+
return node.data("node_color") === 1;
|
|
240
|
+
}),
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
// 4. Re-display nodes and edges for the retained components
|
|
244
|
+
validComponents.forEach((comp) => {
|
|
245
|
+
Object.keys(comp).forEach((label) => {
|
|
246
|
+
const node = cy.$(`node[label="${label}"]`);
|
|
247
|
+
node.style("display", "element");
|
|
248
|
+
node.connectedEdges().forEach((edge) => {
|
|
249
|
+
const edgeSize = edge.data("edge_size");
|
|
250
|
+
if (
|
|
251
|
+
edgeSize >= Math.min(edgeMinValue, edgeMaxValue) &&
|
|
252
|
+
edgeSize <= Math.max(edgeMinValue, edgeMaxValue)
|
|
253
|
+
) {
|
|
254
|
+
edge.style("display", "element");
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// 5. Hide isolated nodes
|
|
261
|
+
cy.nodes().forEach((node) => {
|
|
262
|
+
const visibleEdges = node.connectedEdges().filter((edge) => edge.style("display") === "element");
|
|
263
|
+
if (visibleEdges.length === 0) {
|
|
264
|
+
node.style("display", "none");
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// 6. Re-run the layout
|
|
269
|
+
cy.layout(getLayoutOptions()).run();
|
|
270
|
+
|
|
271
|
+
// 7. Refresh the phenotype list so only visible genes remain
|
|
272
|
+
if (window.refreshPhenotypeList) {
|
|
273
|
+
window.refreshPhenotypeList();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// =============================================================================
|
|
278
|
+
// Genotype, sex, and life-stage specific filtering
|
|
279
|
+
// =============================================================================
|
|
280
|
+
|
|
281
|
+
let targetPhenotype = "";
|
|
282
|
+
|
|
283
|
+
// Wrapper function that applies the filters
|
|
284
|
+
function applyFiltering() {
|
|
285
|
+
filterElementsByGenotypeAndSex(elements, cy, targetPhenotype, filterByNodeColorAndEdgeSize);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Reapply filters whenever the form values change
|
|
289
|
+
document.getElementById("genotype-filter-form").addEventListener("change", applyFiltering);
|
|
290
|
+
document.getElementById("sex-filter-form").addEventListener("change", applyFiltering);
|
|
291
|
+
document.getElementById("lifestage-filter-form").addEventListener("change", applyFiltering);
|
|
292
|
+
|
|
293
|
+
// =============================================================================
|
|
294
|
+
// Highlight human disease annotations
|
|
295
|
+
// =============================================================================
|
|
296
|
+
highlightDiseaseAnnotation({ cy });
|
|
297
|
+
|
|
298
|
+
// ############################################################################
|
|
299
|
+
// Cytoscape's visualization setting
|
|
300
|
+
// ############################################################################
|
|
301
|
+
|
|
302
|
+
// --------------------------------------------------------
|
|
303
|
+
// Gene name search
|
|
304
|
+
// --------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
setupGeneSearch({ cy });
|
|
307
|
+
|
|
308
|
+
// =============================================================================
|
|
309
|
+
// Phenotype highlighting (with search support)
|
|
310
|
+
// =============================================================================
|
|
311
|
+
setupPhenotypeSearch({ cy, elements });
|
|
312
|
+
|
|
313
|
+
// --------------------------------------------------------
|
|
314
|
+
// Slider for Font size
|
|
315
|
+
// --------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
createSlider("font-size-slider", 20, 1, 50, 1, (intValues) => {
|
|
318
|
+
document.getElementById("font-size-value").textContent = intValues;
|
|
319
|
+
cy.style()
|
|
320
|
+
.selector("node")
|
|
321
|
+
.style("font-size", intValues + "px")
|
|
322
|
+
.update();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// --------------------------------------------------------
|
|
326
|
+
// Slider for Edge width
|
|
327
|
+
// --------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
createSlider("edge-width-slider", 5, 1, 10, 1, (intValues) => {
|
|
330
|
+
document.getElementById("edge-width-value").textContent = intValues;
|
|
331
|
+
cy.style()
|
|
332
|
+
.selector("edge")
|
|
333
|
+
.style("width", function (ele) {
|
|
334
|
+
return scaleValue(ele.data("edge_size"), edgeMin, edgeMax, 0.5, 2) * intValues;
|
|
335
|
+
})
|
|
336
|
+
.update();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// --------------------------------------------------------
|
|
340
|
+
// Slider for Node repulsion
|
|
341
|
+
// --------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
const layoutDropdown = document.getElementById("layout-dropdown");
|
|
344
|
+
const nodeRepulsionContainer = document.getElementById("node-repulsion-container");
|
|
345
|
+
|
|
346
|
+
function updateNodeRepulsionVisibility() {
|
|
347
|
+
const selectedLayout = layoutDropdown.value;
|
|
348
|
+
nodeRepulsionContainer.style.display = selectedLayout === "cose" ? "block" : "none";
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
updateNodeRepulsionVisibility();
|
|
352
|
+
layoutDropdown.addEventListener("change", updateNodeRepulsionVisibility);
|
|
353
|
+
|
|
354
|
+
createSlider("nodeRepulsion-slider", 5, 1, 10, 1, (intValues) => {
|
|
355
|
+
nodeRepulsionValue = scaleToOriginalRange(intValues, nodeRepulsionMin, nodeRepulsionMax);
|
|
356
|
+
componentSpacingValue = scaleToOriginalRange(intValues, componentSpacingMin, componentSpacingMax);
|
|
357
|
+
document.getElementById("node-repulsion-value").textContent = intValues;
|
|
358
|
+
cy.layout(getLayoutOptions()).run();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// ############################################################################
|
|
362
|
+
// Tooltip handling
|
|
363
|
+
// ############################################################################
|
|
364
|
+
|
|
365
|
+
// Show tooltip on tap
|
|
366
|
+
cy.on("tap", "node, edge", function (event) {
|
|
367
|
+
showTooltip(event, cy, mapSymbolToId, targetPhenotype, { nodeColorValues });
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Hide tooltip when tapping on background
|
|
371
|
+
cy.on("tap", function (event) {
|
|
372
|
+
if (event.target === cy) {
|
|
373
|
+
removeTooltips();
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// ############################################################################
|
|
378
|
+
// Exporter
|
|
379
|
+
// ############################################################################
|
|
380
|
+
|
|
381
|
+
const fileName = "TSUMUGI_geneList";
|
|
382
|
+
|
|
383
|
+
// --------------------------------------------------------
|
|
384
|
+
// PNG Exporter
|
|
385
|
+
// --------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
const exportPngButton = document.getElementById("export-png");
|
|
388
|
+
if (exportPngButton) {
|
|
389
|
+
exportPngButton.addEventListener("click", function () {
|
|
390
|
+
exportGraphAsPNG(cy, fileName);
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const exportJpgButton = document.getElementById("export-jpg");
|
|
395
|
+
if (exportJpgButton) {
|
|
396
|
+
exportJpgButton.addEventListener("click", function () {
|
|
397
|
+
exportGraphAsJPG(cy, fileName);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// --------------------------------------------------------
|
|
402
|
+
// CSV Exporter
|
|
403
|
+
// --------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
const exportCsvButton = document.getElementById("export-csv");
|
|
406
|
+
if (exportCsvButton) {
|
|
407
|
+
exportCsvButton.addEventListener("click", function () {
|
|
408
|
+
exportGraphAsCSV(cy, fileName);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// --------------------------------------------------------
|
|
413
|
+
// GraphML Exporter (Desktop Cytoscape Compatible)
|
|
414
|
+
// --------------------------------------------------------
|
|
415
|
+
|
|
416
|
+
const exportGraphmlButton = document.getElementById("export-graphml");
|
|
417
|
+
if (exportGraphmlButton) {
|
|
418
|
+
exportGraphmlButton.addEventListener("click", function () {
|
|
419
|
+
exportGraphAsGraphML(cy, fileName);
|
|
420
|
+
});
|
|
421
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function loadJSONGz(url) {
|
|
2
|
+
const req = new XMLHttpRequest();
|
|
3
|
+
let result = null;
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
req.open("GET", url, false);
|
|
7
|
+
req.overrideMimeType("text/plain; charset=x-user-defined"); // Treat the response as binary data
|
|
8
|
+
req.send(null);
|
|
9
|
+
|
|
10
|
+
if (req.status === 200) {
|
|
11
|
+
const compressedData = new Uint8Array(req.responseText.split("").map((c) => c.charCodeAt(0) & 0xff));
|
|
12
|
+
result = JSON.parse(window.pako.ungzip(compressedData, { to: "string" }));
|
|
13
|
+
} else {
|
|
14
|
+
console.error("HTTP error!! status:", req.status);
|
|
15
|
+
}
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error("Failed to load or decode JSON.gz:", error);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function loadJSON(url) {
|
|
24
|
+
const req = new XMLHttpRequest();
|
|
25
|
+
let result = null;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
req.open("GET", url, false);
|
|
29
|
+
req.send(null);
|
|
30
|
+
|
|
31
|
+
if (req.status === 200) {
|
|
32
|
+
result = JSON.parse(req.responseText);
|
|
33
|
+
} else {
|
|
34
|
+
console.error("HTTP error!! status:", req.status);
|
|
35
|
+
}
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error("Failed to load JSON:", error);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { calculateConnectedComponents } from "../graph/components.js";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_EXPORT_SCALE = 6.25;
|
|
4
|
+
|
|
5
|
+
function normalizeScale(scale) {
|
|
6
|
+
const parsed = Number(scale);
|
|
7
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
8
|
+
return DEFAULT_EXPORT_SCALE;
|
|
9
|
+
}
|
|
10
|
+
return parsed;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function triggerDownloadFromBlob(blob, fileName) {
|
|
14
|
+
const url = URL.createObjectURL(blob);
|
|
15
|
+
const a = document.createElement("a");
|
|
16
|
+
a.href = url;
|
|
17
|
+
a.download = fileName;
|
|
18
|
+
document.body.appendChild(a);
|
|
19
|
+
a.click();
|
|
20
|
+
document.body.removeChild(a);
|
|
21
|
+
URL.revokeObjectURL(url);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// --------------------------------------------------------
|
|
25
|
+
// PNG Exporter
|
|
26
|
+
// --------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
export function exportGraphAsPNG(cy, fileName, scale = DEFAULT_EXPORT_SCALE) {
|
|
29
|
+
const pngContent = cy.png({
|
|
30
|
+
scale: normalizeScale(scale), // Scale to achieve desired DPI
|
|
31
|
+
full: true, // Set to true to include the entire graph, even the offscreen parts
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const a = document.createElement("a");
|
|
35
|
+
a.href = pngContent;
|
|
36
|
+
a.download = `${fileName}.png`;
|
|
37
|
+
document.body.appendChild(a);
|
|
38
|
+
a.click();
|
|
39
|
+
document.body.removeChild(a);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --------------------------------------------------------
|
|
43
|
+
// JPG Exporter
|
|
44
|
+
// --------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
export function exportGraphAsJPG(cy, fileName, scale = DEFAULT_EXPORT_SCALE) {
|
|
47
|
+
const jpgContent = cy.jpg({
|
|
48
|
+
scale: normalizeScale(scale),
|
|
49
|
+
full: true,
|
|
50
|
+
quality: 0.95,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const a = document.createElement("a");
|
|
54
|
+
a.href = jpgContent;
|
|
55
|
+
a.download = `${fileName}.jpg`;
|
|
56
|
+
document.body.appendChild(a);
|
|
57
|
+
a.click();
|
|
58
|
+
document.body.removeChild(a);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --------------------------------------------------------
|
|
62
|
+
// SVG Exporter
|
|
63
|
+
// --------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
export function exportGraphAsSVG(cy, fileName, scale = DEFAULT_EXPORT_SCALE) {
|
|
66
|
+
if (typeof cy.svg !== "function") {
|
|
67
|
+
console.error("SVG export requires the cytoscape-svg extension.");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const svgContent = cy.svg({
|
|
72
|
+
scale: normalizeScale(scale),
|
|
73
|
+
full: true,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const blob = new Blob([svgContent], { type: "image/svg+xml;charset=utf-8" });
|
|
77
|
+
triggerDownloadFromBlob(blob, `${fileName}.svg`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --------------------------------------------------------
|
|
81
|
+
// CSV Exporter
|
|
82
|
+
// --------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
export function exportGraphAsCSV(cy, fileName) {
|
|
85
|
+
// Use calculateConnectedComponents to gather connected components
|
|
86
|
+
const connectedComponents = calculateConnectedComponents(cy);
|
|
87
|
+
|
|
88
|
+
// CSV header row
|
|
89
|
+
let csvContent = "module,gene,phenotypes\n";
|
|
90
|
+
|
|
91
|
+
// Assign module numbers and format the data as CSV rows
|
|
92
|
+
connectedComponents.forEach((component, moduleIndex) => {
|
|
93
|
+
const moduleNumber = moduleIndex + 1;
|
|
94
|
+
|
|
95
|
+
Object.keys(component).forEach((gene) => {
|
|
96
|
+
const phenotypes = component[gene].join(";"); // Join phenotypes with semicolons
|
|
97
|
+
|
|
98
|
+
// Append each CSV row
|
|
99
|
+
csvContent += `${moduleNumber},${gene},"${phenotypes}"\n`;
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Generate and download the CSV file
|
|
104
|
+
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
|
105
|
+
const url = URL.createObjectURL(blob);
|
|
106
|
+
const a = document.createElement("a");
|
|
107
|
+
a.href = url;
|
|
108
|
+
a.download = `${fileName}.csv`;
|
|
109
|
+
document.body.appendChild(a);
|
|
110
|
+
a.click();
|
|
111
|
+
document.body.removeChild(a);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// --------------------------------------------------------
|
|
115
|
+
// GraphML Exporter for Desktop Cytoscape Compatibility
|
|
116
|
+
// --------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
export function exportGraphAsGraphML(cy, fileName) {
|
|
119
|
+
const nodes = cy.nodes();
|
|
120
|
+
const edges = cy.edges();
|
|
121
|
+
|
|
122
|
+
// GraphML header
|
|
123
|
+
let graphmlContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
124
|
+
<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
|
|
125
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
126
|
+
xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
|
|
127
|
+
http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
|
|
128
|
+
|
|
129
|
+
<!-- Node attributes -->
|
|
130
|
+
<key id="n0" for="node" attr.name="id" attr.type="string"/>
|
|
131
|
+
<key id="n1" for="node" attr.name="label" attr.type="string"/>
|
|
132
|
+
<key id="n2" for="node" attr.name="color" attr.type="double"/>
|
|
133
|
+
<key id="n3" for="node" attr.name="phenotypes" attr.type="string"/>
|
|
134
|
+
|
|
135
|
+
<!-- Edge attributes -->
|
|
136
|
+
<key id="e0" for="edge" attr.name="interaction" attr.type="string"/>
|
|
137
|
+
<key id="e1" for="edge" attr.name="width" attr.type="double"/>
|
|
138
|
+
<key id="e2" for="edge" attr.name="shared_phenotypes" attr.type="string"/>
|
|
139
|
+
<key id="e3" for="edge" attr.name="similarity" attr.type="double"/>
|
|
140
|
+
|
|
141
|
+
<graph id="TSUMUGI_Network" edgedefault="undirected">
|
|
142
|
+
`;
|
|
143
|
+
|
|
144
|
+
// Add nodes
|
|
145
|
+
nodes.forEach((node) => {
|
|
146
|
+
const data = node.data();
|
|
147
|
+
const id = data.id || "";
|
|
148
|
+
const label = data.label || id;
|
|
149
|
+
const color = data.node_color || 0;
|
|
150
|
+
const phenotypes = Array.isArray(data.phenotype) ? data.phenotype.join(";") : data.phenotype || "";
|
|
151
|
+
|
|
152
|
+
graphmlContent += ` <node id="${escapeXml(id)}">
|
|
153
|
+
<data key="n0">${escapeXml(id)}</data>
|
|
154
|
+
<data key="n1">${escapeXml(label)}</data>
|
|
155
|
+
<data key="n2">${color}</data>
|
|
156
|
+
<data key="n3">${escapeXml(phenotypes)}</data>
|
|
157
|
+
</node>
|
|
158
|
+
`;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Add edges
|
|
162
|
+
edges.forEach((edge, index) => {
|
|
163
|
+
const data = edge.data();
|
|
164
|
+
const source = data.source || "";
|
|
165
|
+
const target = data.target || "";
|
|
166
|
+
const width = data.edge_size || 1;
|
|
167
|
+
const sharedPhenotypes = Array.isArray(data.phenotype) ? data.phenotype.join(";") : data.phenotype || "";
|
|
168
|
+
const similarity = data.similarity || 0;
|
|
169
|
+
|
|
170
|
+
graphmlContent += ` <edge id="e${index}" source="${escapeXml(source)}" target="${escapeXml(target)}">
|
|
171
|
+
<data key="e0">interaction</data>
|
|
172
|
+
<data key="e1">${width}</data>
|
|
173
|
+
<data key="e2">${escapeXml(sharedPhenotypes)}</data>
|
|
174
|
+
<data key="e3">${similarity}</data>
|
|
175
|
+
</edge>
|
|
176
|
+
`;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// GraphML footer
|
|
180
|
+
graphmlContent += ` </graph>
|
|
181
|
+
</graphml>`;
|
|
182
|
+
|
|
183
|
+
// Download GraphML file
|
|
184
|
+
const blob = new Blob([graphmlContent], { type: "application/xml;charset=utf-8;" });
|
|
185
|
+
const url = URL.createObjectURL(blob);
|
|
186
|
+
const a = document.createElement("a");
|
|
187
|
+
a.href = url;
|
|
188
|
+
a.download = `${fileName}.graphml`;
|
|
189
|
+
document.body.appendChild(a);
|
|
190
|
+
a.click();
|
|
191
|
+
document.body.removeChild(a);
|
|
192
|
+
URL.revokeObjectURL(url);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// --------------------------------------------------------
|
|
196
|
+
// Utility function for XML escaping
|
|
197
|
+
// --------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
function escapeXml(unsafe) {
|
|
200
|
+
return unsafe.replace(/[<>&'"]/g, function (c) {
|
|
201
|
+
switch (c) {
|
|
202
|
+
case "<":
|
|
203
|
+
return "<";
|
|
204
|
+
case ">":
|
|
205
|
+
return ">";
|
|
206
|
+
case "&":
|
|
207
|
+
return "&";
|
|
208
|
+
case "'":
|
|
209
|
+
return "'";
|
|
210
|
+
case '"':
|
|
211
|
+
return """;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|