{"spec_id":"network-force-directed","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nnetwork-force-directed: Force-Directed Graph\nLibrary: pygal 3.1.3 | Python 3.13.14\nQuality: 83/100 | Created: 2026-07-01\n\"\"\"\n\nimport sys\nfrom pathlib import Path\n\n\n# Remove script directory from path to avoid name collision with pygal package\n_script_dir = str(Path(__file__).parent)\nsys.path = [p for p in sys.path if p != _script_dir]\n\nimport os\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme-adaptive tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint categorical palette — first data series always #009E73\nIMPRINT = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\")\n\nnp.random.seed(42)\n\n# Data: Protein-protein interaction network with three functional modules\n# Nodes = proteins; edges = confirmed physical interactions from co-IP experiments\nmodule_sizes = [15, 13, 10]  # Metabolism, Signaling, Gene Regulation\nmodule_names = [\"Metabolism\", \"Signaling\", \"Gene Regulation\"]\nnodes = []\nedges = []\n\nnode_id = 0\nfor mod_idx, size in enumerate(module_sizes):\n    for _ in range(size):\n        nodes.append({\"id\": node_id, \"module\": mod_idx})\n        node_id += 1\n\n# Intra-module edges (dense within functional groups)\nfor i in range(15):\n    for j in range(i + 1, 15):\n        if np.random.random() < 0.25:\n            edges.append((i, j))\n\nfor i in range(15, 28):\n    for j in range(i + 1, 28):\n        if np.random.random() < 0.25:\n            edges.append((i, j))\n\nfor i in range(28, 38):\n    for j in range(i + 1, 38):\n        if np.random.random() < 0.25:\n            edges.append((i, j))\n\n# Cross-module interactions (sparse bridges — crosstalk between pathways)\nbridge_edges = [\n    (0, 15),\n    (5, 18),\n    (10, 22),  # Metabolism ↔ Signaling\n    (15, 28),\n    (20, 32),\n    (25, 35),  # Signaling ↔ Gene Regulation\n    (3, 30),  # Metabolism ↔ Gene Regulation\n]\nedges.extend(bridge_edges)\n\n# Force-directed layout (Fruchterman-Reingold)\nn = len(nodes)\npositions = np.random.rand(n, 2) * 2 - 1\n\nk = 1.1  # Increased from 0.95 — better node separation in dense clusters\niterations = 320\n\nfor iteration in range(iterations):\n    displacement = np.zeros((n, 2))\n\n    # Repulsive forces between all node pairs\n    for i in range(n):\n        for j in range(i + 1, n):\n            diff = positions[i] - positions[j]\n            dist = max(np.linalg.norm(diff), 0.01)\n            repulsive_force = (k * k / dist) * (diff / dist)\n            displacement[i] += repulsive_force\n            displacement[j] -= repulsive_force\n\n    # Attractive forces along edges\n    for src, tgt in edges:\n        diff = positions[src] - positions[tgt]\n        dist = max(np.linalg.norm(diff), 0.01)\n        attractive_force = (dist * dist / k) * (diff / dist)\n        displacement[src] -= attractive_force\n        displacement[tgt] += attractive_force\n\n    # Apply displacement with cooling schedule\n    temperature = 1 - iteration / iterations\n    for i in range(n):\n        disp_norm = np.linalg.norm(displacement[i])\n        if disp_norm > 0:\n            positions[i] += (displacement[i] / disp_norm) * min(disp_norm, 0.15 * temperature)\n\n# Normalize positions with ~15% margin on each side to keep clusters away from canvas edges\npos_min = positions.min(axis=0)\npos_max = positions.max(axis=0)\npositions = (positions - pos_min) / (pos_max - pos_min + 1e-6) * 9 + 1.5\npos = {node[\"id\"]: positions[i] for i, node in enumerate(nodes)}\n\n# Node degrees (for size encoding)\ndegrees = {node[\"id\"]: 0 for node in nodes}\nfor src, tgt in edges:\n    degrees[src] += 1\n    degrees[tgt] += 1\n\n# Style — module series use Imprint positions 1-3; intra edges use INK_MUTED; bridge edges use Imprint[3]\nmodule_colors = IMPRINT[: len(module_names)]\nBRIDGE_COLOR = IMPRINT[3]  # #BD8233 amber — visually distinct cross-module connector\nseries_colors = module_colors + (INK_MUTED, BRIDGE_COLOR)  # nodes first → Metabolism gets #009E73\n\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=series_colors,\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    stroke_width=2.5,\n    opacity=0.9,\n    opacity_hover=1.0,\n    tooltip_font_size=28,\n    font_family=\"DejaVu Sans, Helvetica, Arial, sans-serif\",\n)\n\nchart = pygal.XY(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=\"network-force-directed · python · pygal · anyplot.ai\",\n    show_legend=True,\n    show_x_guides=False,\n    show_y_guides=False,\n    show_x_labels=False,\n    show_y_labels=False,\n    stroke=True,\n    dots_size=18,\n    stroke_style={\"width\": 2.5, \"linecap\": \"round\"},\n    legend_at_bottom=True,\n    legend_at_bottom_columns=5,\n    legend_box_size=24,\n    margin=60,\n    range=(0, 12),\n    xrange=(0, 12),\n)\n\n# Node series added FIRST — Metabolism is series 0 and receives Imprint #009E73\nmin_radius, max_radius = 12, 35\nmax_degree = max(degrees.values())\nfor mod_idx, mod_name in enumerate(module_names):\n    mod_nodes = [node for node in nodes if node[\"module\"] == mod_idx]\n    node_points = []\n    for node in mod_nodes:\n        x, y = pos[node[\"id\"]]\n        degree = degrees[node[\"id\"]]\n        radius = min_radius + (max_radius - min_radius) * (degree / max_degree)\n        label = f\"Protein {node['id']} | {degree} interactions\"\n        if degree >= 8:\n            label += \" (Hub)\"\n        node_points.append({\"value\": (x, y), \"label\": label, \"node\": {\"r\": round(radius, 1)}})\n    chart.add(mod_name, node_points, stroke=False)\n\n# Build intra-module edge set for fast lookup\nbridge_edge_set = set(map(tuple, bridge_edges))\n\n# Intra-module edges (series 3) — uses INK_MUTED via colors position 3\nintra_edge_points = []\nfor src, tgt in edges:\n    if (src, tgt) not in bridge_edge_set and (tgt, src) not in bridge_edge_set:\n        x1, y1 = pos[src]\n        x2, y2 = pos[tgt]\n        intra_edge_points.append((x1, y1))\n        intra_edge_points.append((x2, y2))\n        intra_edge_points.append(None)\n\nchart.add(\"Interactions\", intra_edge_points, stroke=True, show_dots=False, fill=False)\n\n# Cross-module bridge edges (series 4) — amber #BD8233, dashed to signal inter-pathway crosstalk\nbridge_edge_points = []\nfor src, tgt in bridge_edges:\n    x1, y1 = pos[src]\n    x2, y2 = pos[tgt]\n    bridge_edge_points.append((x1, y1))\n    bridge_edge_points.append((x2, y2))\n    bridge_edge_points.append(None)\n\nchart.add(\n    \"Cross-module Bridges\",\n    bridge_edge_points,\n    stroke=True,\n    show_dots=False,\n    fill=False,\n    stroke_style={\"width\": 2.5, \"dasharray\": \"8 5\", \"linecap\": \"round\"},\n)\n\n# Save outputs (theme-aware filenames)\nchart.render_to_file(f\"plot-{THEME}.svg\")\nchart.render_to_png(f\"plot-{THEME}.png\")\n\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(chart.render())\n"}