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,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 "&lt;";
204
+ case ">":
205
+ return "&gt;";
206
+ case "&":
207
+ return "&amp;";
208
+ case "'":
209
+ return "&apos;";
210
+ case '"':
211
+ return "&quot;";
212
+ }
213
+ });
214
+ }