{"spec_id":"network-force-directed","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nnetwork-force-directed: Force-Directed Graph\nLibrary: plotnine 0.15.7 | Python 3.13.14\nQuality: 85/100 | Updated: 2026-07-01\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    annotate,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_point,\n    geom_segment,\n    ggplot,\n    guide_legend,\n    guides,\n    labs,\n    scale_color_manual,\n    scale_size_identity,\n    theme,\n    xlim,\n    ylim,\n)\n\n\n# Theme tokens (see prompts/default-style-guide.md)\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — first series is always #009E73\nDEPARTMENT_NAMES = [\"Engineering\", \"Design\", \"Marketing\", \"Sales\"]\nDEPARTMENT_COLORS = {\"Engineering\": \"#009E73\", \"Design\": \"#C475FD\", \"Marketing\": \"#4467A3\", \"Sales\": \"#BD8233\"}\n\nnp.random.seed(42)\n\n# Data: 40-person organization across 4 departments (10 each)\nnodes = [{\"id\": i, \"group\": i // 10} for i in range(40)]\n\n# Intra-department edges (dense clique-like pattern, repeated per department)\nedges: list[tuple[int, int, int]] = []\nfor s in (0, 10, 20, 30):\n    edges.extend(\n        [\n            (s + 0, s + 1, 3),\n            (s + 0, s + 2, 2),\n            (s + 0, s + 3, 2),\n            (s + 1, s + 2, 3),\n            (s + 1, s + 4, 2),\n            (s + 2, s + 3, 2),\n            (s + 2, s + 5, 1),\n            (s + 3, s + 4, 3),\n            (s + 3, s + 6, 2),\n            (s + 4, s + 5, 2),\n            (s + 4, s + 7, 1),\n            (s + 5, s + 6, 3),\n            (s + 5, s + 8, 2),\n            (s + 6, s + 7, 2),\n            (s + 6, s + 9, 1),\n            (s + 7, s + 8, 3),\n            (s + 7, s + 9, 2),\n            (s + 8, s + 9, 2),\n            (s + 0, s + 9, 1),\n            (s + 1, s + 8, 1),\n        ]\n    )\n\n# Cross-department bridges (weaker connections)\nedges.extend(\n    [\n        (0, 10, 1),\n        (2, 12, 1),\n        (5, 15, 1),  # Engineering ↔ Design\n        (10, 20, 1),\n        (14, 24, 1),\n        (18, 28, 1),  # Design ↔ Marketing\n        (20, 30, 1),\n        (23, 33, 1),\n        (27, 37, 1),  # Marketing ↔ Sales\n        (9, 39, 1),\n        (4, 34, 1),  # Engineering ↔ Sales\n        (3, 23, 1),\n        (7, 27, 1),  # Engineering ↔ Marketing\n        (13, 33, 1),\n        (16, 36, 1),  # Design ↔ Sales\n    ]\n)\n\n# Fruchterman-Reingold layout with weak centering gravity to avoid empty core\nn = len(nodes)\npositions = np.random.rand(n, 2) * 2 - 1\nk = 0.28\ngravity = 0.06\niterations = 250\ntemperature = 1.0\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            force = (k * k / dist) * (diff / dist)\n            displacement[i] += force\n            displacement[j] -= force\n\n    # Attractive forces along edges, scaled by collaboration weight\n    for src, tgt, weight in edges:\n        diff = positions[src] - positions[tgt]\n        dist = max(np.linalg.norm(diff), 0.01)\n        force = (dist * dist / k) * (weight / 3) * (diff / dist)\n        displacement[src] -= force\n        displacement[tgt] += force\n\n    # Weak gravity toward origin pulls clusters inward\n    for i in range(n):\n        displacement[i] += gravity * (-positions[i])\n\n    # Simulated annealing cooling\n    cooling = 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, cooling * 0.1)\n\n# Normalize to [0.05, 0.95]\npos_min = positions.min(axis=0)\npos_max = positions.max(axis=0)\npositions = (positions - pos_min) / (pos_max - pos_min + 1e-6) * 0.9 + 0.05\npos = {node[\"id\"]: positions[i] for i, node in enumerate(nodes)}\n\n# Node degrees for size scaling\ndegrees = {node[\"id\"]: 0 for node in nodes}\nfor src, tgt, _ in edges:\n    degrees[src] += 1\n    degrees[tgt] += 1\n\n# Department centroids — order legend spatially (left → right)\ncentroids = {}\nfor dept_idx, dept_name in enumerate(DEPARTMENT_NAMES):\n    member_ids = [node[\"id\"] for node in nodes if node[\"group\"] == dept_idx]\n    centroids[dept_name] = np.mean([pos[i] for i in member_ids], axis=0)\nlegend_order = sorted(DEPARTMENT_NAMES, key=lambda name: centroids[name][0])\n\nnode_df = pd.DataFrame(\n    {\n        \"x\": [pos[node[\"id\"]][0] for node in nodes],\n        \"y\": [pos[node[\"id\"]][1] for node in nodes],\n        \"group\": pd.Categorical(\n            [DEPARTMENT_NAMES[node[\"group\"]] for node in nodes], categories=legend_order, ordered=True\n        ),\n        \"size\": [3.0 + degrees[node[\"id\"]] * 0.4 for node in nodes],\n    }\n)\n\n# Split edges into internal (solid) vs. cross-department bridges (dashed)\nedge_records = []\nfor src, tgt, weight in edges:\n    is_internal = nodes[src][\"group\"] == nodes[tgt][\"group\"]\n    edge_records.append(\n        {\n            \"x\": pos[src][0],\n            \"y\": pos[src][1],\n            \"xend\": pos[tgt][0],\n            \"yend\": pos[tgt][1],\n            \"thickness\": 0.40 + weight * 0.35 if is_internal else 0.55 + weight * 0.35,\n            \"edge_type\": \"internal\" if is_internal else \"bridge\",\n        }\n    )\nedge_df = pd.DataFrame(edge_records)\ninternal_edges = edge_df[edge_df[\"edge_type\"] == \"internal\"]\nbridge_edges = edge_df[edge_df[\"edge_type\"] == \"bridge\"]\n\nEDGE_COLOR = INK_SOFT\nBRIDGE_COLOR = INK_MUTED\n\nplot = (\n    ggplot()\n    # Internal edges — solid, theme-adaptive\n    + geom_segment(\n        data=internal_edges,\n        mapping=aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\", size=\"thickness\"),\n        color=EDGE_COLOR,\n        alpha=0.45,\n    )\n    # Cross-department bridges — dashed, lighter\n    + geom_segment(\n        data=bridge_edges,\n        mapping=aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\", size=\"thickness\"),\n        color=BRIDGE_COLOR,\n        alpha=0.65,\n        linetype=\"dashed\",\n    )\n    # Nodes on top, sized by degree\n    + geom_point(data=node_df, mapping=aes(x=\"x\", y=\"y\", color=\"group\", size=\"size\"), alpha=0.95, stroke=0.5)\n    + scale_color_manual(values=DEPARTMENT_COLORS, breaks=legend_order)\n    + scale_size_identity()\n    + guides(color=guide_legend(override_aes={\"size\": 3}))\n    + labs(title=\"network-force-directed · python · plotnine · anyplot.ai\", color=\"Department\")\n    + xlim(-0.02, 1.02)\n    + ylim(-0.02, 1.02)\n    + annotate(\n        \"text\",\n        x=0.5,\n        y=-0.015,\n        label=f\"{len(nodes)} people · {len(edges)} collaborations · node size ∝ degree · dashed = cross-team\",\n        size=8,\n        color=INK_MUTED,\n        ha=\"center\",\n        va=\"top\",\n    )\n    + theme(\n        figure_size=(8, 4.5),\n        plot_title=element_text(size=12, color=INK, ha=\"center\", margin={\"b\": 6}),\n        legend_title=element_text(size=10, color=INK),\n        legend_text=element_text(size=8, color=INK_SOFT),\n        legend_position=(0.02, 0.98),\n        legend_direction=\"vertical\",\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT, size=0.5),\n        legend_key=element_rect(fill=ELEVATED_BG, color=ELEVATED_BG),\n        axis_title=element_blank(),\n        axis_text=element_blank(),\n        axis_ticks=element_blank(),\n        panel_grid=element_blank(),\n        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    )\n)\n\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\")\n"}