DoctorLego2003/DataVis_Forces
0
1import marimo2 3__generated_with = "0.11.20"4app = marimo.App(width="full")5 6 7@app.cell8async def _():9 try:10 import micropip11 await micropip.install('svg-py')12 except ImportError:13 pass # Handle the error or provide an alternative solution14 return (micropip,)15 16 17@app.cell18def _():19 import marimo as mo20 import math21 import pandas as pd22 import random23 from svg import SVG, G, Circle, Path, Title, Text, Rect, Desc24 return Circle, Desc, G, Path, Rect, SVG, Text, Title, math, mo, pd, random25 26 27@app.cell28def _(pd):29 df2 = pd.read_csv("macrophages_new.tsv", sep="\t")30 return (df2,)31 32 33@app.cell34def _(pd):35 def separate_phages(df):36 # Create a copy of the original dataframe37 result_df = df.copy()38 39 # Function to split the phage string into a list40 def split_phages(phage_str):41 if pd.isna(phage_str):42 return []43 44 # Replace " and " with "," for consistent splitting45 phage_str = str(phage_str).replace(" and ", ", ")46 47 # Split by comma and strip whitespace48 phages = [p.strip() for p in phage_str.split(",")]49 50 # Remove empty strings51 phages = [p for p in phages if p]52 53 # Correct the "14-Jan" issue if present54 phages = ["14-1" if p == "14-Jan" else p for p in phages]55 56 return phages57 58 # Apply the function to create a new column with lists of phages59 result_df['phage_list'] = df['bacteriophage'].apply(split_phages)60 61 # Add n_phages column that counts the number of phages for each row62 result_df['n_phages'] = result_df['phage_list'].apply(len)63 64 # Find the maximum number of phages in any row65 max_phages = result_df['n_phages'].max()66 67 # Create individual columns for each phage68 for i in range(max_phages):69 result_df[f'phage{i+1}'] = result_df['phage_list'].apply(70 lambda x: x[i] if i < len(x) else None71 )72 73 # Drop the temporary list column74 result_df.drop('phage_list', axis=1, inplace=True)75 76 return result_df77 return (separate_phages,)78 79 80@app.cell81def _(df2, separate_phages):82 df20 = separate_phages(df2)83 return (df20,)84 85 86@app.cell87def _():88 svg_width = 70089 svg_height = 53090 border_size = 10091 return border_size, svg_height, svg_width92 93 94@app.cell95def _(mo):96 mo.md(97 r"""98 <style>99 circle {100 fill-opacity: 0.5;101 cursor: pointer;102 }103 circle:hover {104 fill-opacity: 1;105 }106 </style>107 """108 )109 return110 111 112@app.cell113def _(df21, pd):114 def get_unique_phages(df):115 # Function to split the phage string into a list116 def split_phages(phage_str):117 if pd.isna(phage_str):118 return []119 120 # Replace " and " with "," for consistent splitting121 phage_str = str(phage_str).replace(" and ", ", ")122 123 # Split by comma and strip whitespace124 phages = [p.strip() for p in phage_str.split(",")]125 126 # Remove empty strings127 phages = [p for p in phages if p]128 129 # Correct the "14-Jan" issue if present130 phages = ["14-1" if p == "14-Jan" else p for p in phages]131 132 return phages133 134 # Create a set to store unique phages135 all_phages = set()136 137 # Process each row to extract phages138 for phage_str in df['bacteriophage']:139 phages_in_row = split_phages(phage_str)140 all_phages.update(phages_in_row)141 142 # Convert to a sorted list143 unique_phages = sorted(list(all_phages))144 145 return unique_phages146 147 # Example usage:148 phage_list = get_unique_phages(df21)149 #print(f"Found {len(phage_list)} unique bacteriophages: {phage_list}")150 return get_unique_phages, phage_list151 152 153@app.cell154def _():155 return156 157 158@app.cell159def _():160 def separate_bacteria(df):161 import pandas as pd162 163 # Map abbreviations to full names164 abbrev_map = {165 "S": "Staphylococcus",166 "P": "Pseudomonas",167 "E": "Escherichia",168 "H": "Haemophilus",169 "M": "Moraxella",170 "K": "Klebsiella",171 "A": "Acinetobacter",172 "B": "Bacteroides",173 "C": "Clostridium",174 "L": "Listeria",175 "N": "Neisseria",176 "Y": "Yersinia"177 }178 179 # Function to expand abbreviation if necessary180 def expand_abbreviation(name):181 parts = name.split()182 if not parts:183 return name184 # If the first part is in the abbreviation map, expand it and ensure it has a dot185 if parts[0][0] in abbrev_map:186 expanded_name = abbrev_map[parts[0][0]] # Get the full name for the first letter187 # If abbreviation does not have a dot, add it188 if not parts[0].endswith("."):189 parts[0] = parts[0] + "."190 return f"{expanded_name} {' '.join(parts[1:])}"191 return name # Return the name as-is if no abbreviation192 193 def split_bacteria(bacteria_str):194 if pd.isna(bacteria_str):195 return []196 bacteria_str = bacteria_str.replace(" and ", ", ").replace("*", "") # Clean the string197 bacteria = [expand_abbreviation(b.strip()) for b in bacteria_str.split(",")]198 return [b for b in bacteria if b]199 200 # Copy the dataframe to not modify the original201 result_df = df.copy()202 203 # Apply the split_bacteria function to create a list of bacteria204 result_df['bacteria_list'] = result_df['bacterial_species'].apply(split_bacteria)205 206 # Count the number of bacteria in each row and find the max number of bacteria207 result_df['n_bacteria'] = result_df['bacteria_list'].apply(len)208 max_bacteria = result_df['n_bacteria'].max()209 210 # Create individual columns for each bacteria211 for i in range(max_bacteria):212 result_df[f'bacteria_{i+1}'] = result_df['bacteria_list'].apply(213 lambda x: x[i] if i < len(x) else None214 )215 216 # Drop temporary columns217 result_df.drop(['bacteria_list', 'n_bacteria'], axis=1, inplace=True)218 219 return result_df220 return (separate_bacteria,)221 222 223@app.cell224def _(df20, separate_bacteria):225 df201 = separate_bacteria(df20)226 return (df201,)227 228 229@app.cell230def _(pd):231 def add_all_bacteria_column(df):232 # Identify bacteria columns (e.g., bacteria_1, bacteria_2, ...)233 bacteria_cols = [col for col in df.columns if col.startswith("bacteria_")]234 235 # Combine non-null bacteria entries into a comma-separated string236 df["all_bacteria"] = df[bacteria_cols].apply(237 lambda row: ", ".join([b for b in row if pd.notna(b)]), axis=1238 )239 240 return df241 return (add_all_bacteria_column,)242 243 244@app.cell245def _(add_all_bacteria_column, df201):246 df202 = add_all_bacteria_column(df201)247 return (df202,)248 249 250@app.cell251def _():252 return253 254 255@app.cell256def get_unique_bacteria():257 def get_unique_bacteria(df):258 # Initialize a set to store unique bacteria names259 all_bacteria = set()260 261 # Iterate over the columns containing bacteria (e.g., 'bacteria1', 'bacteria2', ...)262 for col in df.columns:263 if col.startswith('bacteria_'):264 # Add each non-null bacteria to the set265 all_bacteria.update(df[col].dropna())266 267 # Convert to a sorted list268 unique_bacteria = sorted(list(all_bacteria))269 270 return unique_bacteria271 return (get_unique_bacteria,)272 273 274@app.cell275def _(pd):276 def add_all_phages_column(df):277 # Identify bacteria columns (e.g., bacteria_1, bacteria_2, ...)278 phage_cols = [col for col in df.columns if col.startswith("phage")]279 280 # Combine non-null bacteria entries into a comma-separated string281 df["all_phages"] = df[phage_cols].apply(282 lambda row: ", ".join([b for b in row if pd.notna(b)]), axis=1283 )284 285 return df286 return (add_all_phages_column,)287 288 289@app.cell290def _(add_all_phages_column, df202):291 df21 = add_all_phages_column(df202)292 return (df21,)293 294 295@app.cell296def _(Rect, border_size):297 def create_rect_edge_points(phages, svg_width, svg_height, radius=4, margin=border_size/2):298 assert len(phages) == 28, "Expected exactly 28 phages"299 300 phage_squares = []301 phage_coords = {}302 idx = 0303 304 def add_square(x, y):305 nonlocal idx306 phage = phages[idx]307 phage_coords[phage] = (x, y)308 # phage_squares.append(Circle(cx=x, cy=y, r=radius, fill="blue", stroke="black"))309 phage_squares.append(Rect(x=x, y=y, width=radius, height=radius, fill="black", stroke="black", class_= phage))310 idx += 1311 312 for i in range(9):313 x = margin + i * (svg_width - 2 * margin) / (8)314 add_square(x, margin)315 for i in range(5):316 y = margin + (i+1) * (svg_height - 2 * margin) / (6)317 add_square(svg_width - margin, y)318 for i in range(9):319 x = svg_width - margin - i * (svg_width - 2 * margin) / (8)320 add_square(x, svg_height - margin)321 for i in range(5):322 y = svg_height - margin - (i+1) * (svg_height - 2 * margin) / (6)323 add_square(margin, y)324 325 return phage_squares, phage_coords326 return (create_rect_edge_points,)327 328 329@app.cell330def clamp():331 def clamp(val, min_val, max_val, distance):332 return max(min(val, max_val*distance), min_val*distance)333 return (clamp,)334 335 336@app.cell337def compute_forces(clamp):338 def compute_forces(row_coords, df, phage_coords, *,339 row_phages, infection_groups,340 row_infection_type, row_phage_attract,341 infection_attract, repulsion,342 damping=0.04,343 svg_width=800, svg_height=600):344 new_coords = {}345 346 for idx, (x, y) in row_coords.items():347 fx, fy = 0, 0348 349 # Phage attraction350 for phage in row_phages.get(idx, []):351 if phage in phage_coords:352 px, py = phage_coords[phage]353 dx, dy = px - x, py - y354 fx += row_phage_attract[idx] * dx355 fy += row_phage_attract[idx] * dy356 357 # Attraction to same infection type358 count = 0359 for jdx in infection_groups.get(row_infection_type[idx], []):360 if jdx == idx:361 continue362 ox, oy = row_coords[jdx]363 dx, dy = ox - x, oy - y364 fx += infection_attract * dx365 fy += infection_attract * dy366 count += 1367 368 # Repulsion from all other nodes369 for jdx, (ox, oy) in row_coords.items():370 if jdx == idx:371 continue372 dx, dy = x - ox, y - oy373 dist_sq = dx**2 + dy**2 + 1e-4 # prevent division by zero374 fx += repulsion * dx / dist_sq375 fy += repulsion * dy / dist_sq376 377 # Apply net force with damping378 new_x = x + fx * damping / count379 new_y = y + fy * damping / count380 381 # Clamp to bounds382 new_x = clamp(new_x, 0.1, 0.9, svg_width)383 new_y = clamp(new_y, 0.15, 0.85, svg_height)384 385 new_coords[idx] = (int(new_x), int(new_y))386 387 return new_coords388 return (compute_forces,)389 390 391@app.cell392def enforce_minimum_distance(clamp):393 def enforce_minimum_distance(coords, min_distance, svg_width=None, svg_height=None):394 coords = coords.copy()395 updated = True396 max_iter = 10397 iteration = 0398 399 while updated and iteration < max_iter:400 updated = False401 iteration += 1402 403 for idx1, (x1, y1) in coords.items():404 for idx2, (x2, y2) in coords.items():405 if idx1 >= idx2:406 continue407 dx, dy = x1 - x2, y1 - y2408 dist_sq = dx**2 + dy**2 + 1e-4409 dist = dist_sq**0.5410 if dist < min_distance:411 updated = True412 overlap = (min_distance - dist) / 2413 push_x = (dx / dist) * overlap414 push_y = (dy / dist) * overlap415 coords[idx1] = (x1 + push_x, y1 + push_y)416 coords[idx2] = (x2 - push_x, y2 - push_y)417 418 # Optional: clamp again if needed419 if svg_width and svg_height:420 coords[idx1] = (421 clamp(coords[idx1][0], 0.1, 0.9, svg_width),422 clamp(coords[idx1][1], 0.2, 0.8, svg_height)423 )424 coords[idx2] = (425 clamp(coords[idx2][0], 0.1, 0.9, svg_width),426 clamp(coords[idx2][1], 0.2, 0.8, svg_height)427 )428 return coords429 return (enforce_minimum_distance,)430 431 432@app.cell433def _(Path):434 def create_links(df, phage_coords, row_coords, radius):435 lines = []436 for idx, row in df.iterrows():437 # Determine stroke color based on row logic438 if row["outcome"] == "Died":439 stroke_color = "black"440 elif row["eradication_targeted_bacteria"] == "Yes":441 stroke_color = "green"442 elif row["eradication_targeted_bacteria"] == "No":443 stroke_color = "red"444 else:445 stroke_color = "orange"446 447 for phage_col in ["phage1", "phage2", "phage3", "phage4", "phage5"]:448 phage = row.get(phage_col)449 if phage and phage in phage_coords:450 x1, y1 = phage_coords[phage]451 x2, y2 = row_coords.get(idx, (None, None))452 if x2 is not None:453 d = f"M{x1+radius/2},{y1+radius/2} L{x2},{y2}"454 lines.append(Path(455 d=d,456 stroke=stroke_color,457 stroke_width=2 / row["n_phages"],458 opacity=0.2,459 stroke_opacity=0.5,460 class_=row["patient"],461 id=phage462 ))463 464 return lines465 return (create_links,)466 467 468@app.cell469def _(Text, border_size):470 def create_phage_labels(phage_coords, svg_width, svg_height, margin=border_size/2, delta=10):471 labels = []472 for phage_name, (x, y) in phage_coords.items():473 # Determine position: top, right, bottom, left474 if abs(y - margin) < 1e-2:475 # Top row476 dx, dy = 5, -delta477 anchor = "middle"478 elif abs(x - (svg_width - margin)) < 1e-2:479 # Right column480 dx, dy = delta+8, 8481 anchor = "start"482 elif abs(y - (svg_height - margin)) < 1e-2:483 # Bottom row484 dx, dy = 5, 15+delta485 anchor = "middle"486 elif abs(x - margin) < 1e-2:487 # Left column488 dx, dy = -delta+2, 8489 anchor = "end"490 else:491 dx, dy = 0, -delta # fallback492 anchor = "middle"493 494 labels.append(Text(495 x=x + dx,496 y=y + dy,497 text=phage_name,498 text_anchor=anchor,499 font_size="8px",500 fill="black" 501 ))502 return labels503 return (create_phage_labels,)504 505 506@app.cell507def _(Text):508 from collections import defaultdict509 510 511 def create_infection_type_labels(df, row_coords, dx=0, dy=0, min_distance=8):512 type_coords = defaultdict(list)513 514 # Group row coordinates by infection type515 for idx, row in df.iterrows():516 coords = row_coords.get(idx)517 if coords:518 type_coords[row["primary_infection_type"]].append(coords)519 520 labels = []521 for infection_type, coords in type_coords.items():522 if not coords:523 continue524 525 avg_x = sum(x for x, _ in coords) / len(coords)526 avg_y = sum(y for _, y in coords) / len(coords)527 528 # Now enforce the minimum distance for the label placement529 label_x = avg_x + dx530 label_y = avg_y + dy531 532 # Check for overlap and move the label right if necessary533 while any(534 abs(label_x - x) < min_distance and abs(label_y - y) < min_distance535 for (x, y) in coords536 ):537 label_x += min_distance # Move the label right by the minimum distance538 539 540 label_x += min_distance # Move the label right by the minimum distance541 labels.append(Text(542 x=label_x,543 y=label_y, # slightly above the cluster544 text=infection_type,545 text_anchor="middle",546 font_size="10px",547 fill="black",548 opacity=1549 ))550 551 return labels552 return create_infection_type_labels, defaultdict553 554 555@app.cell556def _():557 custom_phage_list = ['BUCT700', 'UZM3', '14-1', '4P', '8UZL', 'DP1', 'APC 1.1', 'APC 2.1', 'BFC 2', 'BE06', '4029', '4032', '4034', 'BFC 1', 'E4', 'Efs7', 'JWX', 'JWDelta', 'ISP', 'IntestiPhage', 'M1', 'EFgrKN', 'EFgrNG', 'KN', 'PNM', 'PT07', 'Phage C', 'PyoPhage']558 return (custom_phage_list,)559 560 561@app.cell562def _(563 Circle,564 Desc,565 G,566 SVG,567 Text,568 Title,569 border_size,570 compute_forces,571 create_infection_type_labels,572 create_links,573 create_phage_labels,574 create_rect_edge_points,575 custom_phage_list,576 defaultdict,577 df21,578 enforce_minimum_distance,579 mo,580 random,581 svg_height,582 svg_width,583):584 _script = """<script>585 document.addEventListener("DOMContentLoaded", function () {586 let tooltip = document.getElementById("tooltip");587 let tooltip_text = document.getElementById("tooltiptext");588 let circles = document.querySelectorAll("circle");589 let phageSquares = document.querySelectorAll("rect");590 let links = document.querySelectorAll("path");591 592 let activeElement = null;593 594 function resetHighlights() {595 circles.forEach(c => c.style.opacity = 1);596 phageSquares.forEach(sq => sq.style.opacity = 0.8);597 links.forEach(link => link.style.opacity = 0.2); // Reset links' opacity to the default value598 }599 600 function highlightByCircle(circle) {601 602 let currentBacteria = circle.attributes[2].value;603 let currentPhages = circle.attributes[4] ? circle.attributes[4].value : "";604 let rowId = circle.querySelector("desc")?.textContent.trim();605 606 let currentBacteriaList = currentBacteria ? currentBacteria.split(", ").map(b => b.trim()) : [];607 let currentPhageList = currentPhages ? currentPhages.split(",").map(p => p.trim()) : [];608 609 tooltip.style.opacity = 1;610 tooltip_text.textContent = "Bacteria: " + currentBacteria;611 612 circles.forEach((c) => {613 let otherBacteria = c.attributes[2].value;614 if (!otherBacteria) return;615 let otherList = otherBacteria.split(", ").map(b => b.trim());616 let hasOverlap = currentBacteriaList.some(b => otherList.includes(b));617 c.style.opacity = hasOverlap ? 1 : 0.2;618 });619 620 phageSquares.forEach(square => {621 let phage = square.attributes[1].value;622 let isLinked = currentPhageList.includes(phage);623 square.style.opacity = isLinked ? 1 : 0.2;624 });625 626 // Highlight associated links (only modify style.opacity, not stroke_opacity)627 links.forEach(link => {628 let connectedPatient = link.attributes[3].value;629 if (connectedPatient === rowId) { 630 link.style.opacity = 1;631 } else {632 link.style.opacity = 0.05;633 }634 });635 }636 637 function highlightByPhage(square) {638 let phage = square.attributes[1].value;639 640 tooltip.style.opacity = 1;641 tooltip_text.textContent = phage;642 643 // Highlight related circles644 circles.forEach(c => {645 let dataPhages = c.attributes[4] ? c.attributes[4].value : "";646 if (!dataPhages) return;647 let phageList = dataPhages.split(",").map(p => p.trim());648 c.style.opacity = phageList.includes(phage) ? 1 : 0.2;649 });650 651 // Highlight phage squares652 phageSquares.forEach(sq => sq.style.opacity = 0.2);653 square.style.opacity = 1;654 655 // Highlight links with matching id (only modify style.opacity, not stroke_opacity)656 links.forEach(link => {657 if (link.id === phage) {658 link.style.opacity = 1;659 } else {660 link.style.opacity = 0.05;661 }662 });663 }664 665 // Circle event bindings666 circles.forEach(circle => {667 circle.addEventListener("mousemove", function () {668 if (activeElement) return;669 highlightByCircle(circle);670 });671 672 circle.addEventListener("mouseleave", function () {673 if (!activeElement) resetHighlights();674 });675 676 circle.addEventListener("click", function () {677 activeElement = circle;678 highlightByCircle(circle);679 });680 });681 682 // Phage square event bindings683 phageSquares.forEach(square => {684 square.style.opacity = 0.8;685 686 square.addEventListener("mousemove", function () {687 if (activeElement) return;688 highlightByPhage(square);689 });690 691 square.addEventListener("mouseleave", function () {692 if (!activeElement) resetHighlights();693 });694 695 square.addEventListener("click", function () {696 activeElement = square;697 highlightByPhage(square);698 });699 });700 701 // Click anywhere else to clear702 document.addEventListener("click", function (e) {703 if (!e.target.closest("circle") && !e.target.closest("rect")) {704 activeElement = null;705 resetHighlights();706 tooltip.style.opacity = 0;707 }708 });709 });710 </script>711 712 713 """714 715 716 717 718 # Assume: df21, phage_list, svg_width, svg_height, border_size, create_rect_edge_points, compute_forces, create_links are defined719 720 # Step 1: Create phage edge points and store coordinates721 722 radius = 10723 _edge_points, phage_coords = create_rect_edge_points(724 custom_phage_list, svg_width, svg_height, radius725 )726 727 # Step 2: Initialize row_coords based on linked phage positions728 row_coords = {}729 730 731 def _initialize_circle_coords(row):732 # Collect coordinates of all linked phages733 linked_phage_coords = []734 for col in ["phage1", "phage2", "phage3", "phage4", "phage5"]:735 phage = row.get(col)736 if phage in phage_coords:737 linked_phage_coords.append(phage_coords[phage])738 739 if linked_phage_coords:740 avg_x = sum(x for x, _ in linked_phage_coords) / len(741 linked_phage_coords742 )743 avg_y = sum(y for _, y in linked_phage_coords) / len(744 linked_phage_coords745 )746 # Add jitter747 cx = int(avg_x + random.uniform(-10, 10))748 cy = int(avg_y + random.uniform(-10, 10))749 else:750 # fallback: random position751 cx = random.randint(border_size, int(svg_width) - border_size)752 cy = random.randint(border_size, int(svg_height) - border_size)753 754 row_coords[row.name] = (cx, cy)755 756 757 # Initialize coordinates758 df21.apply(_initialize_circle_coords, axis=1)759 760 761 # 1. Row -> list of phages762 row_phages = {763 idx: [row.get(f"phage{i}") for i in range(1, 6) if row.get(f"phage{i}")]764 for idx, row in df21.iterrows()765 }766 767 # 2. Infection type -> list of row indices768 infection_groups = defaultdict(list)769 for idx, row in df21.iterrows():770 infection_groups[row["primary_infection_type"]].append(idx)771 772 # 3. Row -> infection type773 row_infection_type = df21["primary_infection_type"].to_dict()774 775 # 4. Row -> effective phage attraction776 row_phage_attract = {777 idx: (778 0.3 if row["eradication_targeted_bacteria"] == "Yes" else 0.15779 )780 for idx, row in df21.iterrows()781 }782 # Step 1: Run simulation with only force-based updates783 it = 80784 for _ in range(it):785 row_coords = compute_forces(786 row_coords,787 df21,788 phage_coords,789 row_phages=row_phages,790 infection_groups=infection_groups,791 row_infection_type=row_infection_type,792 row_phage_attract=row_phage_attract,793 infection_attract=1,794 repulsion=50,795 )796 797 # Step 2: Enforce minimum spacing between nodes once798 row_coords = enforce_minimum_distance(row_coords, min_distance=10)799 800 801 # Step 4: Generate final circle elements based on updated coordinates802 def _regenerate_circle(row):803 cx, cy = row_coords[row.name]804 return Circle(805 cx=cx,806 cy=cy,807 r=2.5,808 # class_=row["primary_infection_type"],809 class_=row["all_bacteria"],810 id=row["all_phages"],811 fill="black"812 if (row["outcome"] == "Died")813 else (814 "green"815 if (row["eradication_targeted_bacteria"] == "Yes")816 else (817 "red"818 if (row["eradication_targeted_bacteria"] == "No")819 else "orange"820 )821 ),822 stroke_width=1.5,823 stroke="green" if (row["clinical_improvement"] == "Yes") else "red",824 opacity=1, 825 # attrib={"data-bacteria": row["all_bacteria"]},826 elements=[827 Title(elements=[f"Patient ID: {row['patient']}\nInfection Type: {row['primary_infection_type']}"]),828 Desc(elements=[f"{row['patient']}"])829 ]830 831 )832 833 834 _circles = df21.apply(_regenerate_circle, axis=1).tolist()835 836 # Step 5: Create links based on updated coordinates837 _links = create_links(df21, phage_coords, row_coords, radius)838 839 840 _tooltip = G(841 id="tooltip",842 elements=[843 Text(x=0, y=20, text="", id="tooltiptext", font_size=10),844 ],845 )846 _circles.append(_tooltip)847 848 _phage_labels = create_phage_labels(849 phage_coords, svg_width, svg_height, delta=10850 )851 # , dy = -10)852 853 _infection_labels = create_infection_type_labels(df21, row_coords)854 855 _plot = SVG(856 width=svg_width,857 height=svg_height,858 elements=_links859 + _circles860 + _edge_points861 + _phage_labels862 + _infection_labels,863 class_="notebook",864 )865 866 # mo.Html(_plot.as_str())867 868 mo.iframe(_plot.as_str() + _script, width = 1000, height = 550)869 # mo.Html(_plot.as_str())870 return (871 idx,872 infection_groups,873 it,874 phage_coords,875 radius,876 row,877 row_coords,878 row_infection_type,879 row_phage_attract,880 row_phages,881 )882 883 884@app.cell885def _():886 return887 888 889@app.cell890def _():891 return892 893 894@app.cell895def _(mo):896 mo.md(897 r"""898 <!-- <style>899 circle {900 fill-opacity: 0.5;901 cursor: pointer;902 }903 circle:hover {904 fill-opacity: 1;905 }906 </style> -->907 """908 )909 return910 911 912@app.cell913def _():914 return915 916 917@app.cell918def _():919 return920 921 922if __name__ == "__main__":923 app.run()924 