{"spec_id":"network-force-directed","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nnetwork-force-directed: Force-Directed Graph\nLibrary: matplotlib 3.11.0 | Python 3.13.14\nQuality: 86/100 | Updated: 2026-07-01\n\"\"\"\n\nimport os\nimport pathlib\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom matplotlib.collections import LineCollection\n\n\nOUTPUT_DIR = pathlib.Path(__file__).parent\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\nCOMMUNITY_COLORS = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\nCOMMUNITY_NAMES = [\"Engineering\", \"Marketing\", \"Sales\"]\n\n# Data: 50-person company social network with 3 departments\nnp.random.seed(42)\ncommunity_sizes = [18, 17, 15]\n\nnodes = []\nnid_counter = 0\nfor comm_idx, size in enumerate(community_sizes):\n    for _ in range(size):\n        nodes.append({\"id\": nid_counter, \"community\": comm_idx})\n        nid_counter += 1\n\nintra_edges = []\nranges = [(0, 18), (18, 35), (35, 50)]\nfor start, stop in ranges:\n    for i in range(start, stop):\n        for j in range(i + 1, stop):\n            if np.random.random() < 0.3:\n                intra_edges.append((i, j))\n\n# Sparse cross-department bridge edges\nbridge_edges = [(0, 18), (5, 20), (10, 25), (18, 35), (22, 40), (30, 45), (8, 38), (15, 48)]\nall_edges = intra_edges + bridge_edges\n\n# Force-directed layout (Fruchterman-Reingold)\nn = len(nodes)\npositions = np.random.rand(n, 2) * 2 - 1\nk = 0.5\n\nfor iteration in range(200):\n    displacement = np.zeros((n, 2))\n\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    for src, tgt in all_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    temperature = 1 - iteration / 200\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\npos_min = positions.min(axis=0)\npos_max = positions.max(axis=0)\npositions = (positions - pos_min) / (pos_max - pos_min + 1e-6) * 0.84 + 0.08\npos = {node[\"id\"]: positions[i] for i, node in enumerate(nodes)}\n\ndegrees = {node[\"id\"]: 0 for node in nodes}\nfor src, tgt in all_edges:\n    degrees[src] += 1\n    degrees[tgt] += 1\n\n# Canvas: 3200×1800 px (landscape 16:9)\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Intra-community edges (solid, subtle — dense within-team connections)\nintra_lines = [(pos[src], pos[tgt]) for src, tgt in intra_edges]\nlc_intra = LineCollection(intra_lines, colors=INK_SOFT, linewidths=0.7, alpha=0.22, zorder=1)\nax.add_collection(lc_intra)\n\n# Bridge edges (dashed, more visible — sparse cross-team connections reveal structure)\nbridge_lines = [(pos[src], pos[tgt]) for src, tgt in bridge_edges]\nlc_bridge = LineCollection(bridge_lines, colors=INK_MUTED, linewidths=1.2, alpha=0.65, linestyle=\"dashed\", zorder=1)\nax.add_collection(lc_bridge)\n\n# Nodes sized by degree\nnode_sizes = {}\nfor node in nodes:\n    x, y = pos[node[\"id\"]]\n    degree = degrees[node[\"id\"]]\n    size = 80 + degree * 12\n    node_sizes[node[\"id\"]] = size\n    color = COMMUNITY_COLORS[node[\"community\"]]\n    ax.scatter(x, y, s=size, c=color, edgecolors=PAGE_BG, linewidths=1.2, alpha=0.92, zorder=2)\n\n# Label top 2 hubs per community\ntop_hubs = []\nfor comm_idx in range(3):\n    comm_degrees = [(node[\"id\"], degrees[node[\"id\"]]) for node in nodes if node[\"community\"] == comm_idx]\n    comm_degrees.sort(key=lambda x: x[1], reverse=True)\n    top_hubs.extend([nid for nid, _ in comm_degrees[:2]])\n\nfor node in nodes:\n    nid = node[\"id\"]\n    if nid in top_hubs:\n        x, y = pos[nid]\n        offset = 0.008 + 0.0007 * np.sqrt(node_sizes[nid])\n        team_initial = COMMUNITY_NAMES[node[\"community\"]][0]\n        ax.text(\n            x,\n            y + offset,\n            f\"Hub ({team_initial})\",\n            fontsize=8,\n            fontweight=\"bold\",\n            ha=\"center\",\n            va=\"bottom\",\n            color=INK,\n            zorder=4,\n            bbox={\"facecolor\": ELEVATED_BG, \"edgecolor\": \"none\", \"boxstyle\": \"round,pad=0.2\", \"alpha\": 0.85},\n        )\n\ntitle = \"network-force-directed · python · matplotlib · anyplot.ai\"\ntitle_fontsize = max(8, round(12 * 67 / len(title))) if len(title) > 67 else 12\nax.set_title(title, fontsize=title_fontsize, fontweight=\"medium\", color=INK, pad=10)\nax.set_xlim(-0.02, 1.02)\nax.set_ylim(-0.02, 1.02)\nax.axis(\"off\")\n\nlegend_handles = [\n    ax.scatter([], [], c=color, s=80, edgecolors=PAGE_BG, linewidths=1.2, label=name)\n    for color, name in zip(COMMUNITY_COLORS, COMMUNITY_NAMES, strict=True)\n]\nleg = ax.legend(\n    handles=legend_handles,\n    loc=\"upper left\",\n    fontsize=8,\n    title=\"Teams\",\n    title_fontsize=10,\n    framealpha=0.95,\n    fancybox=True,\n)\nleg.get_frame().set_facecolor(ELEVATED_BG)\nleg.get_frame().set_edgecolor(INK_SOFT)\nleg.get_title().set_color(INK)\nplt.setp(leg.get_texts(), color=INK_SOFT)\n\nfig.text(\n    0.5,\n    0.01,\n    f\"50 nodes · {len(all_edges)} edges · node size ∝ degree · dashed = cross-team bridges\",\n    ha=\"center\",\n    va=\"bottom\",\n    fontsize=8,\n    color=INK_MUTED,\n)\n\nfig.subplots_adjust(left=0.03, right=0.97, top=0.93, bottom=0.07)\nplt.savefig(OUTPUT_DIR / f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}