CoolFace
Apppublic

DoctorLego2003/DataVis_Forces

sourceHugging Facemitupdated 1y agoView on Hugging Face
0likes
app.py924 linesDownload Raw Back to root
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