{"spec_id":"network-force-directed","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nnetwork-force-directed: Force-Directed Graph\nLibrary: seaborn 0.13.2 | Python 3.13.14\nQuality: 88/100 | Updated: 2026-07-01\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\n\n\n# Theme-adaptive chrome\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\"\nEDGE_COLOR = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.10,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\nnp.random.seed(42)\n\n# Imprint categorical palette — canonical order, first series always #009E73\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\n\n# Data — organizational social network: 3 departments\nnum_nodes = 37\ncommunity_sizes = [15, 12, 10]\ncommunity_names = [\"Engineering\", \"Marketing\", \"Sales\"]\ncommunities = []\nfor comm_idx, size in enumerate(community_sizes):\n    communities.extend([comm_idx] * size)\n\n# Generate edges with community structure\nedges = []\nfor i in range(num_nodes):\n    for j in range(i + 1, num_nodes):\n        if communities[i] == communities[j]:\n            if np.random.random() < 0.35:\n                weight = np.random.uniform(0.5, 1.0)\n                edges.append((i, j, weight))\n        else:\n            if np.random.random() < 0.05:\n                weight = np.random.uniform(0.3, 0.7)\n                edges.append((i, j, weight))\n\n# Calculate node degrees\ndegrees = [0] * num_nodes\nfor src, tgt, _ in edges:\n    degrees[src] += 1\n    degrees[tgt] += 1\n\n# Identify bridge nodes (cross-community edges)\nbridge_nodes = set()\nfor src, tgt, _ in edges:\n    if communities[src] != communities[tgt]:\n        bridge_nodes.add(src)\n        bridge_nodes.add(tgt)\n\n# Force-directed layout (Fruchterman-Reingold inline)\nn = num_nodes\nk = 0.5\niterations = 150\npos = np.random.rand(n, 2) * 2 - 1\nt = 1.0\ndt = t / (iterations + 1)\n\nfor _ in range(iterations):\n    disp = np.zeros((n, 2))\n    for i in range(n):\n        for j in range(i + 1, n):\n            delta = pos[i] - pos[j]\n            dist = max(np.linalg.norm(delta), 0.01)\n            force = (k * k) / dist\n            force_vec = (delta / dist) * force\n            disp[i] += force_vec\n            disp[j] -= force_vec\n    for src, tgt, _ in edges:\n        delta = pos[src] - pos[tgt]\n        dist = max(np.linalg.norm(delta), 0.01)\n        force = (dist * dist) / k\n        force_vec = (delta / dist) * force\n        disp[src] -= force_vec\n        disp[tgt] += force_vec\n    for i in range(n):\n        disp_norm = max(np.linalg.norm(disp[i]), 0.01)\n        pos[i] += (disp[i] / disp_norm) * min(disp_norm, t)\n    t -= dt\n\n# Normalize positions to [-1, 1]\npos -= pos.mean(axis=0)\nmax_coord = np.abs(pos).max()\nif max_coord > 0:\n    pos /= max_coord\n\nx_coords = pos[:, 0]\ny_coords = pos[:, 1]\n\n# Node DataFrame for seaborn three-way encoding (hue + size + style)\nnode_df = pd.DataFrame(\n    {\n        \"x\": x_coords,\n        \"y\": y_coords,\n        \"department\": [community_names[c] for c in communities],\n        \"degree\": degrees,\n        \"node_type\": [\"Bridge\" if i in bridge_nodes else \"Member\" for i in range(num_nodes)],\n    }\n)\n\n# Plot — 3200×1800 px landscape (figsize=(8,4.5) × dpi=400)\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Edges\nfor src, tgt, weight in edges:\n    x0, y0 = pos[src]\n    x1, y1 = pos[tgt]\n    ax.plot([x0, x1], [y0, y1], color=EDGE_COLOR, linewidth=0.5 + weight * 1.0, alpha=0.22, zorder=1)\n\n# Nodes via seaborn three-way encoding: hue=department, size=degree, style=node_type\n# Diamond markers distinguish bridge nodes (cross-community connectors) from regular members\npalette = {name: IMPRINT[i] for i, name in enumerate(community_names)}\nsns.scatterplot(\n    data=node_df,\n    x=\"x\",\n    y=\"y\",\n    hue=\"department\",\n    hue_order=community_names,\n    palette=palette,\n    size=\"degree\",\n    sizes=(80, 400),\n    style=\"node_type\",\n    style_order=[\"Member\", \"Bridge\"],\n    markers={\"Member\": \"o\", \"Bridge\": \"D\"},\n    alpha=0.90,\n    edgecolor=PAGE_BG,\n    linewidth=0.8,\n    ax=ax,\n    legend=False,\n    zorder=2,\n)\n\n# Label top 4 hubs by degree only — prevents center-cluster label overlap\ntop_hubs = sorted(range(num_nodes), key=lambda i: degrees[i], reverse=True)[:4]\nfor node in top_hubs:\n    ax.annotate(\n        f\"Node {node}\",\n        (pos[node, 0], pos[node, 1]),\n        fontsize=8,\n        ha=\"center\",\n        va=\"bottom\",\n        xytext=(0, 7),\n        textcoords=\"offset points\",\n        fontweight=\"bold\",\n        color=INK_SOFT,\n    )\n\n# Legend — departments (circle) + bridge node indicator (diamond)\nlegend_elements = []\nfor idx, name in enumerate(community_names):\n    count = community_sizes[idx]\n    legend_elements.append(\n        plt.scatter(\n            [], [], c=IMPRINT[idx], s=80, marker=\"o\", label=f\"{name} ({count})\", edgecolor=PAGE_BG, linewidth=0.8\n        )\n    )\nlegend_elements.append(\n    plt.scatter([], [], c=INK_SOFT, s=80, marker=\"D\", edgecolor=PAGE_BG, linewidth=0.8, label=\"Bridge node\")\n)\n\nax.legend(\n    handles=legend_elements,\n    loc=\"upper left\",\n    fontsize=8,\n    title=\"Department\",\n    title_fontsize=9,\n    frameon=True,\n    labelcolor=INK,\n)\n\n# Network summary inside the axes (bottom centre)\ntotal_edges = len(edges)\navg_degree = sum(degrees) / num_nodes\nstats_text = (\n    f\"Nodes: {num_nodes}  ·  Edges: {total_edges}  ·  Avg degree: {avg_degree:.1f}  ·  Bridges: {len(bridge_nodes)}\"\n)\nax.text(0.5, 0.02, stats_text, transform=ax.transAxes, fontsize=7, ha=\"center\", va=\"bottom\", color=INK_MUTED)\n\n# Style\ntitle = \"network-force-directed · python · seaborn · anyplot.ai\"\nax.set_title(title, fontsize=12, fontweight=\"medium\", pad=10, color=INK)\nax.set_xlabel(\"Force-directed X\", fontsize=10, color=INK)\nax.set_ylabel(\"Force-directed Y\", fontsize=10, color=INK)\nax.set_xticks([])\nax.set_yticks([])\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nax.spines[\"left\"].set_color(INK_SOFT)\nax.spines[\"bottom\"].set_color(INK_SOFT)\n\nplt.subplots_adjust(left=0.07, right=0.97, top=0.91, bottom=0.10)\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}