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,514 @@
1
+ // ============================================================
2
+ // Tooltip + Info Tooltip Utilities
3
+ // ============================================================
4
+
5
+ const DEFAULT_TOOLTIP_HEIGHT = 220;
6
+ const MIN_TOOLTIP_WIDTH = 220;
7
+ const MIN_TOOLTIP_HEIGHT = 140;
8
+ const MIN_SECTION_HEIGHT = 80;
9
+ const TOOLTIP_OFFSET = 10;
10
+ const DEFAULT_SECTION_HEIGHTS = {
11
+ phenotypes: 120,
12
+ diseases: 90,
13
+ modules: 160,
14
+ };
15
+
16
+ let infoTooltipsInitialized = false;
17
+
18
+ function formatPhenotypesWithHighlight(phenotypes, targetPhenotype) {
19
+ const safePhenotypes = Array.isArray(phenotypes) ? phenotypes : [];
20
+ const cleanedPhenotypes = safePhenotypes.filter((phenotype) => phenotype && phenotype !== "");
21
+ if (!targetPhenotype) {
22
+ return cleanedPhenotypes.map((anno) => "・ " + anno).join("<br>");
23
+ }
24
+
25
+ const matching = [];
26
+ const others = [];
27
+
28
+ for (const phenotype of cleanedPhenotypes) {
29
+ if (phenotype.startsWith(targetPhenotype)) {
30
+ matching.push(phenotype);
31
+ } else {
32
+ others.push(phenotype);
33
+ }
34
+ }
35
+
36
+ const ordered = [...matching, ...others];
37
+
38
+ return ordered
39
+ .map((phenotype) =>
40
+ phenotype.startsWith(targetPhenotype) ? `▶ ${phenotype}` : "・ " + phenotype,
41
+ )
42
+ .join("<br>");
43
+ }
44
+
45
+ function buildNodeTooltipContent({ data, mapSymbolToId, targetPhenotype, nodeColorValues }) {
46
+ const geneId = mapSymbolToId[data.id] || "UNKNOWN";
47
+ const urlImpc = `https://www.mousephenotype.org/data/genes/${geneId}`;
48
+ const shouldHideSeverity = Boolean(data.hide_severity);
49
+ const rawSeverity = Number.isFinite(data.original_node_color) ? data.original_node_color : data.node_color;
50
+ const nodeColorSet = Array.isArray(nodeColorValues) ? new Set(nodeColorValues) : new Set();
51
+ const uniqueValues = [...nodeColorSet];
52
+ const isBinary =
53
+ uniqueValues.length === 1 &&
54
+ ["0", "1", "100"].includes(String(Math.round(Number(uniqueValues[0]))));
55
+ const severityValue =
56
+ !shouldHideSeverity && !isBinary && Number.isFinite(rawSeverity) ? Math.round(rawSeverity) : null;
57
+ const severityText = severityValue !== null ? ` (Severity: ${severityValue})` : "";
58
+
59
+ const phenotypes = Array.isArray(data.phenotype)
60
+ ? data.phenotype
61
+ : data.phenotype
62
+ ? [data.phenotype]
63
+ : [];
64
+ const diseases = Array.isArray(data.disease)
65
+ ? data.disease
66
+ : data.disease
67
+ ? [data.disease]
68
+ : [];
69
+ const phenotypesHtml = formatPhenotypesWithHighlight(phenotypes, targetPhenotype);
70
+ const phenotypeSection = `
71
+ <div class="cy-tooltip__section cy-tooltip__section--phenotypes" data-section="phenotypes">
72
+ <div class="cy-tooltip__section-title">
73
+ <b>Phenotypes of <a href="${urlImpc}" target="_blank">${data.id} KO mice</a>${severityText}</b>
74
+ </div>
75
+ <div class="cy-tooltip__section-body">${phenotypesHtml}</div>
76
+ </div>
77
+ `;
78
+
79
+ const cleanedDiseases = diseases.filter((disease) => disease && disease !== "");
80
+ let diseaseSection = "";
81
+ if (cleanedDiseases.length > 0) {
82
+ const diseasesHtml = cleanedDiseases.map((disease) => "・ " + disease).join("<br>");
83
+ diseaseSection = `
84
+ <div class="cy-tooltip__section cy-tooltip__section--diseases" data-section="diseases">
85
+ <div class="cy-tooltip__section-title"><b>Associated Human Diseases</b></div>
86
+ <div class="cy-tooltip__section-body">${diseasesHtml}</div>
87
+ </div>
88
+ `;
89
+ }
90
+
91
+ return `${phenotypeSection}${diseaseSection}`;
92
+ }
93
+
94
+ function buildEdgeTooltipContent({ data, cy, targetPhenotype }) {
95
+ const phenotypes = Array.isArray(data.phenotype)
96
+ ? data.phenotype
97
+ : data.phenotype
98
+ ? [data.phenotype]
99
+ : [];
100
+ const sourceNode = cy.getElementById(data.source).data("label");
101
+ const targetNode = cy.getElementById(data.target).data("label");
102
+ const hasSimilarityValue = Number.isFinite(data.edge_size);
103
+ const similarityText = hasSimilarityValue ? ` (Similarity: ${Math.round(data.edge_size)})` : "";
104
+
105
+ let tooltipText = `<div><b>Shared phenotypes of ${sourceNode} and ${targetNode} KOs${similarityText}</b><br>`;
106
+ tooltipText += formatPhenotypesWithHighlight(phenotypes, targetPhenotype);
107
+ tooltipText += "</div>";
108
+
109
+ const sourcePos = cy.getElementById(data.source).renderedPosition();
110
+ const targetPos = cy.getElementById(data.target).renderedPosition();
111
+ const position = {
112
+ x: (sourcePos.x + targetPos.x) / 2,
113
+ y: (sourcePos.y + targetPos.y) / 2,
114
+ };
115
+
116
+ return { content: tooltipText, position };
117
+ }
118
+
119
+ function createTooltipContent(event, cy, mapSymbolToId, targetPhenotype, { nodeColorValues } = {}) {
120
+ const data = event.target.data();
121
+
122
+ if (event.target.isNode()) {
123
+ return {
124
+ content: buildNodeTooltipContent({ data, mapSymbolToId, targetPhenotype, nodeColorValues }),
125
+ position: event.target.renderedPosition(),
126
+ };
127
+ }
128
+
129
+ if (event.target.isEdge()) {
130
+ return buildEdgeTooltipContent({ data, cy, targetPhenotype });
131
+ }
132
+
133
+ return { content: "", position: { x: 0, y: 0 } };
134
+ }
135
+
136
+ function setTooltipPosition(tooltip, position) {
137
+ tooltip.style.left = `${Math.round(position.x + TOOLTIP_OFFSET)}px`;
138
+ tooltip.style.top = `${Math.round(position.y + TOOLTIP_OFFSET)}px`;
139
+ }
140
+
141
+ function enableTooltipDrag(tooltip, containerElement = null) {
142
+ let isDragging = false;
143
+ let dragOffset = { x: 0, y: 0 };
144
+ const container = containerElement || tooltip.parentElement || document.querySelector(".cy");
145
+ if (!container) return;
146
+
147
+ const startDrag = (clientX, clientY) => {
148
+ const rect = tooltip.getBoundingClientRect();
149
+ dragOffset = {
150
+ x: clientX - rect.left,
151
+ y: clientY - rect.top,
152
+ };
153
+ isDragging = true;
154
+ tooltip.style.cursor = "grabbing";
155
+ };
156
+
157
+ const handleDrag = (clientX, clientY) => {
158
+ if (!isDragging) return;
159
+ const containerRect = container.getBoundingClientRect();
160
+ tooltip.style.left = `${clientX - dragOffset.x - containerRect.left}px`;
161
+ tooltip.style.top = `${clientY - dragOffset.y - containerRect.top}px`;
162
+ };
163
+
164
+ const stopDrag = () => {
165
+ isDragging = false;
166
+ tooltip.style.cursor = "move";
167
+ };
168
+
169
+ // Mouse events
170
+ tooltip.addEventListener("mousedown", (event) => {
171
+ if (event.target.closest(".cy-tooltip__resize-handle")) return;
172
+ event.stopPropagation();
173
+ startDrag(event.clientX, event.clientY);
174
+ });
175
+
176
+ document.addEventListener("mousemove", (event) => {
177
+ handleDrag(event.clientX, event.clientY);
178
+ });
179
+
180
+ document.addEventListener("mouseup", stopDrag);
181
+
182
+ // Touch events for tablet/mobile support
183
+ tooltip.addEventListener("touchstart", (event) => {
184
+ if (event.target.closest(".cy-tooltip__resize-handle")) return;
185
+ event.stopPropagation();
186
+ event.preventDefault();
187
+ const touch = event.touches[0];
188
+ startDrag(touch.clientX, touch.clientY);
189
+ });
190
+
191
+ document.addEventListener("touchmove", (event) => {
192
+ if (!isDragging) return;
193
+ event.preventDefault();
194
+ const touch = event.touches[0];
195
+ handleDrag(touch.clientX, touch.clientY);
196
+ });
197
+
198
+ document.addEventListener("touchend", stopDrag);
199
+ }
200
+
201
+ function updateTooltipSectionHeights(tooltip, tooltipHeight = DEFAULT_TOOLTIP_HEIGHT) {
202
+ const safeHeight = Math.max(MIN_TOOLTIP_HEIGHT, tooltipHeight);
203
+ const sections = Array.from(tooltip.querySelectorAll(".cy-tooltip__section"));
204
+ if (sections.length === 0) return;
205
+
206
+ // Distribute space by section weights, while keeping each section readable.
207
+ const styles = window.getComputedStyle(tooltip);
208
+ const paddingY = parseFloat(styles.paddingTop || "0") + parseFloat(styles.paddingBottom || "0");
209
+ const gapY = parseFloat(styles.rowGap || styles.gap || "0");
210
+ const reservedHeight = paddingY + gapY * Math.max(0, sections.length - 1);
211
+ const availableHeight = Math.max(MIN_SECTION_HEIGHT * sections.length, safeHeight - reservedHeight);
212
+
213
+ const weights = sections.map((section) => {
214
+ const key = section.dataset.section;
215
+ return DEFAULT_SECTION_HEIGHTS[key] || MIN_SECTION_HEIGHT;
216
+ });
217
+ const totalWeight = weights.reduce((sum, w) => sum + w, 0) || 1;
218
+
219
+ sections.forEach((section, idx) => {
220
+ const weight = weights[idx];
221
+ const target = Math.max(
222
+ MIN_SECTION_HEIGHT,
223
+ Math.round((weight / totalWeight) * availableHeight),
224
+ );
225
+
226
+ section.style.maxHeight = `${target}px`;
227
+
228
+ const key = section.dataset.section;
229
+ if (key) {
230
+ tooltip.style.setProperty(`--cy-tooltip-${key}-max`, `${target}px`);
231
+ }
232
+ });
233
+
234
+ tooltip.style.setProperty("--cy-tooltip-height", `${Math.round(safeHeight)}px`);
235
+ }
236
+
237
+ function applyInitialTooltipSize(tooltip) {
238
+ const rect = tooltip.getBoundingClientRect();
239
+ const width = Math.max(MIN_TOOLTIP_WIDTH, Math.round(rect.width));
240
+ const height = Math.max(MIN_TOOLTIP_HEIGHT, Math.round(rect.height || DEFAULT_TOOLTIP_HEIGHT));
241
+
242
+ tooltip.style.width = `${width}px`;
243
+ tooltip.style.height = `${height}px`;
244
+ updateTooltipSectionHeights(tooltip, height);
245
+ }
246
+
247
+ function enableTooltipResize(tooltip, containerElement = null) {
248
+ const container = containerElement || tooltip.parentElement || document.querySelector(".cy");
249
+ if (!container) return;
250
+
251
+ const resizeHandle = document.createElement("div");
252
+ resizeHandle.classList.add("cy-tooltip__resize-handle");
253
+ tooltip.appendChild(resizeHandle);
254
+
255
+ resizeHandle.addEventListener("pointerdown", (event) => {
256
+ event.preventDefault();
257
+ event.stopPropagation();
258
+
259
+ const startRect = tooltip.getBoundingClientRect();
260
+ const containerRect = container.getBoundingClientRect();
261
+ const startLeft = tooltip.offsetLeft;
262
+ const startTop = tooltip.offsetTop;
263
+ const startX = event.clientX;
264
+ const startY = event.clientY;
265
+
266
+ const handlePointerMove = (moveEvent) => {
267
+ // Dragging left increases width while anchoring the right edge.
268
+ const deltaX = startX - moveEvent.clientX;
269
+ const deltaY = moveEvent.clientY - startY;
270
+
271
+ let newWidth = startRect.width + deltaX;
272
+ let newLeft = startLeft - deltaX;
273
+ let newHeight = startRect.height + deltaY;
274
+
275
+ const containerWidth = containerRect.width;
276
+ const containerHeight = containerRect.height;
277
+ const minWidth = Math.min(MIN_TOOLTIP_WIDTH, containerWidth);
278
+ const minHeight = Math.min(MIN_TOOLTIP_HEIGHT, containerHeight);
279
+
280
+ newWidth = Math.max(minWidth, newWidth);
281
+ newHeight = Math.max(minHeight, newHeight);
282
+
283
+ if (newLeft < 0) {
284
+ newWidth += newLeft;
285
+ newLeft = 0;
286
+ }
287
+
288
+ const maxLeft = Math.max(0, containerWidth - minWidth);
289
+ if (newLeft > maxLeft) {
290
+ newLeft = maxLeft;
291
+ }
292
+
293
+ const maxWidth = containerWidth - newLeft;
294
+ newWidth = Math.min(maxWidth, newWidth);
295
+ if (newWidth < minWidth) {
296
+ newWidth = minWidth;
297
+ newLeft = Math.max(0, containerWidth - newWidth);
298
+ }
299
+
300
+ const maxHeight = Math.max(minHeight, containerHeight - startTop);
301
+ newHeight = Math.min(maxHeight, newHeight);
302
+
303
+ tooltip.style.width = `${Math.round(newWidth)}px`;
304
+ tooltip.style.left = `${Math.round(newLeft)}px`;
305
+ tooltip.style.height = `${Math.round(newHeight)}px`;
306
+ updateTooltipSectionHeights(tooltip, newHeight);
307
+ };
308
+
309
+ const stopResize = () => {
310
+ document.removeEventListener("pointermove", handlePointerMove);
311
+ document.removeEventListener("pointerup", stopResize);
312
+ };
313
+
314
+ document.addEventListener("pointermove", handlePointerMove);
315
+ document.addEventListener("pointerup", stopResize);
316
+ });
317
+ }
318
+
319
+ function isolateTooltipScroll(tooltip, cyInstance = null) {
320
+ void cyInstance;
321
+ const stopScrollPropagation = (event) => {
322
+ event.stopPropagation();
323
+ };
324
+
325
+ // Capture-phase listeners ensure Cytoscape does not see wheel/touch events.
326
+ tooltip.addEventListener("wheel", stopScrollPropagation, { passive: true, capture: true });
327
+ tooltip.addEventListener("mousewheel", stopScrollPropagation, { passive: true, capture: true });
328
+ tooltip.addEventListener("touchstart", stopScrollPropagation, { passive: true, capture: true });
329
+ tooltip.addEventListener("touchmove", stopScrollPropagation, { passive: true, capture: true });
330
+
331
+ // Allow cleanup if the tooltip is removed while hovered.
332
+ tooltip.__restoreCyInteractions = () => {};
333
+ }
334
+
335
+ function addCopyButtonToTooltip(tooltip) {
336
+ const copyWrapper = document.createElement("div");
337
+ copyWrapper.classList.add("cy-tooltip__copy");
338
+ copyWrapper.innerHTML =
339
+ '<button class="cy-tooltip__copy-btn" title="Copy to clipboard" aria-label="Copy to clipboard">' +
340
+ '<i class="fa-regular fa-copy"></i>' +
341
+ "</button>";
342
+ tooltip.appendChild(copyWrapper);
343
+
344
+ const button = copyWrapper.querySelector("button");
345
+ button.addEventListener("mousedown", (event) => event.stopPropagation());
346
+ button.addEventListener("click", (event) => {
347
+ event.stopPropagation();
348
+
349
+ // Clone the tooltip so the copy button can be stripped before exporting text.
350
+ const clone = tooltip.cloneNode(true);
351
+ const buttonInClone = clone.querySelector(".cy-tooltip__copy");
352
+ if (buttonInClone) {
353
+ buttonInClone.remove();
354
+ }
355
+
356
+ let extractedHtml = clone.innerHTML;
357
+ extractedHtml = extractedHtml.replace(/<br\s*\/?>/gi, "\n");
358
+ extractedHtml = extractedHtml.replace(/<\/div>/gi, "</div>\n");
359
+
360
+ const tempElement = document.createElement("div");
361
+ tempElement.innerHTML = extractedHtml;
362
+ let text = tempElement.textContent || tempElement.innerText || "";
363
+ text = text.replace(/\n\s*\n/g, "\n").trim();
364
+
365
+ if (navigator.clipboard) {
366
+ navigator.clipboard
367
+ .writeText(text)
368
+ .then(() => {
369
+ button.innerHTML = '<i class="fa-solid fa-check"></i>';
370
+ setTimeout(() => (button.innerHTML = '<i class="fa-regular fa-copy"></i>'), 2000);
371
+ })
372
+ .catch((error) => {
373
+ console.error("Failed to copy:", error);
374
+ alert("Failed to copy to clipboard");
375
+ });
376
+ } else {
377
+ alert("Clipboard API not available");
378
+ }
379
+ });
380
+ }
381
+
382
+ function createTooltipElement({ content, position, containerSelector = ".cy", cyInstance = null }) {
383
+ removeTooltips();
384
+
385
+ const container = document.querySelector(containerSelector);
386
+ if (!container) {
387
+ console.warn(`Container "${containerSelector}" not found; tooltip not rendered.`);
388
+ return null;
389
+ }
390
+
391
+ const tooltip = document.createElement("div");
392
+ tooltip.classList.add("cy-tooltip");
393
+ tooltip.innerHTML = content;
394
+ setTooltipPosition(tooltip, position);
395
+
396
+ container.appendChild(tooltip);
397
+ applyInitialTooltipSize(tooltip);
398
+ addCopyButtonToTooltip(tooltip);
399
+ enableTooltipDrag(tooltip, container);
400
+ enableTooltipResize(tooltip, container);
401
+ isolateTooltipScroll(tooltip, cyInstance);
402
+
403
+ return tooltip;
404
+ }
405
+
406
+ function closeInfoTooltips(except = null) {
407
+ document.querySelectorAll(".info-tooltip-container.active").forEach((el) => {
408
+ if (el !== except) {
409
+ el.classList.remove("active");
410
+ }
411
+ });
412
+ }
413
+
414
+ /**
415
+ * Enable click-to-toggle info tooltips using a single delegated listener.
416
+ */
417
+ export function initInfoTooltips() {
418
+ if (infoTooltipsInitialized) return;
419
+ infoTooltipsInitialized = true;
420
+
421
+ // Event delegation keeps newly injected tooltip icons working.
422
+ document.addEventListener("click", (event) => {
423
+ const icon = event.target.closest(".info-tooltip-icon");
424
+ const container = event.target.closest(".info-tooltip-container");
425
+
426
+ if (icon && container) {
427
+ event.preventDefault();
428
+ const isActive = container.classList.toggle("active");
429
+ if (isActive) {
430
+ closeInfoTooltips(container);
431
+ }
432
+ return;
433
+ }
434
+
435
+ if (!container) {
436
+ closeInfoTooltips();
437
+ }
438
+ });
439
+ }
440
+
441
+ /**
442
+ * Render a tooltip for the current Cytoscape event target.
443
+ */
444
+ export function showTooltip(event, cy, mapSymbolToId, targetPhenotype = null, options = {}) {
445
+ const { content, position } = createTooltipContent(event, cy, mapSymbolToId, targetPhenotype, options);
446
+
447
+ if (!content) return;
448
+
449
+ createTooltipElement({
450
+ content,
451
+ position,
452
+ containerSelector: ".cy",
453
+ cyInstance: cy,
454
+ });
455
+ }
456
+
457
+ export function removeTooltips() {
458
+ document.querySelectorAll(".cy-tooltip").forEach((el) => {
459
+ if (typeof el.__restoreCyInteractions === "function") {
460
+ el.__restoreCyInteractions();
461
+ }
462
+ el.remove();
463
+ });
464
+ }
465
+
466
+ /**
467
+ * Render a custom tooltip at a fixed rendered position.
468
+ */
469
+ export function showCustomTooltip({ content, position, containerSelector = ".cy", cyInstance = null }) {
470
+ createTooltipElement({ content, position, containerSelector, cyInstance });
471
+ }
472
+
473
+ /**
474
+ * Render a module summary tooltip for a connected component.
475
+ */
476
+ export function showSubnetworkTooltip({ component, renderedPos, containerSelector = ".cy", cyInstance = null }) {
477
+ if (!component) return;
478
+
479
+ const lines =
480
+ component.phenotypes && component.phenotypes.length > 0
481
+ ? component.phenotypes.map(([name, count]) => `・ ${name} (${count})`)
482
+ : ["No shared phenotypes on visible edges."];
483
+
484
+ const infoIcon = `
485
+ <div class="info-tooltip-container">
486
+ <div class="info-tooltip-icon" aria-label="Tooltip: shared phenotype counts">i</div>
487
+ <div class="info-tooltip-content">
488
+ The number in parentheses indicates the count of shared phenotypes within the module.
489
+ </div>
490
+ </div>
491
+ `;
492
+
493
+ const header = `
494
+ <div class="cy-tooltip__header">
495
+ <b>Phenotypes shared in Module ${component.id}</b>
496
+ ${infoIcon}
497
+ </div>
498
+ `;
499
+ const linesHtml = lines.join("<br>");
500
+ const bodySection = `
501
+ <div class="cy-tooltip__section cy-tooltip__section--modules" data-section="modules">
502
+ <div class="cy-tooltip__section-body">${linesHtml}</div>
503
+ </div>
504
+ `;
505
+ const tooltipContent = `${header}${bodySection}`;
506
+ const anchor =
507
+ renderedPos ||
508
+ {
509
+ x: (component.bbox.x1 + component.bbox.x2) / 2,
510
+ y: (component.bbox.y1 + component.bbox.y2) / 2,
511
+ };
512
+
513
+ createTooltipElement({ content: tooltipContent, position: anchor, containerSelector, cyInstance });
514
+ }