{"spec_id":"network-basic","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nnetwork-basic: Basic Network Graph\nLibrary: seaborn 0.13.2 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-07-24\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 tokens (see prompts/default-style-guide.md \"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\"\n\n# Imprint categorical palette — first 4 positions for the 4 communities\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n\nsns.set_theme(\n    style=\"white\",\n    context=\"talk\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"text.color\": INK,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\n# Seed for reproducibility\nnp.random.seed(42)\n\n# Data: a small social network with 20 people in 4 communities\nnodes = [\n    {\"id\": 0, \"label\": \"Alice\", \"group\": \"Team A\"},\n    {\"id\": 1, \"label\": \"Bob\", \"group\": \"Team A\"},\n    {\"id\": 2, \"label\": \"Carol\", \"group\": \"Team A\"},\n    {\"id\": 3, \"label\": \"David\", \"group\": \"Team A\"},\n    {\"id\": 4, \"label\": \"Eve\", \"group\": \"Team A\"},\n    {\"id\": 5, \"label\": \"Frank\", \"group\": \"Team B\"},\n    {\"id\": 6, \"label\": \"Grace\", \"group\": \"Team B\"},\n    {\"id\": 7, \"label\": \"Henry\", \"group\": \"Team B\"},\n    {\"id\": 8, \"label\": \"Ivy\", \"group\": \"Team B\"},\n    {\"id\": 9, \"label\": \"Jack\", \"group\": \"Team B\"},\n    {\"id\": 10, \"label\": \"Kate\", \"group\": \"Team C\"},\n    {\"id\": 11, \"label\": \"Leo\", \"group\": \"Team C\"},\n    {\"id\": 12, \"label\": \"Mia\", \"group\": \"Team C\"},\n    {\"id\": 13, \"label\": \"Noah\", \"group\": \"Team C\"},\n    {\"id\": 14, \"label\": \"Olivia\", \"group\": \"Team C\"},\n    {\"id\": 15, \"label\": \"Paul\", \"group\": \"Team D\"},\n    {\"id\": 16, \"label\": \"Quinn\", \"group\": \"Team D\"},\n    {\"id\": 17, \"label\": \"Ryan\", \"group\": \"Team D\"},\n    {\"id\": 18, \"label\": \"Sara\", \"group\": \"Team D\"},\n    {\"id\": 19, \"label\": \"Tom\", \"group\": \"Team D\"},\n]\n\n# Edges: friendship connections (within and between groups)\nedges = [\n    # Team A internal connections\n    (0, 1),\n    (0, 2),\n    (1, 2),\n    (1, 3),\n    (2, 4),\n    (3, 4),\n    # Team B internal connections\n    (5, 6),\n    (5, 7),\n    (6, 8),\n    (7, 8),\n    (7, 9),\n    (8, 9),\n    # Team C internal connections\n    (10, 11),\n    (10, 12),\n    (11, 13),\n    (12, 13),\n    (12, 14),\n    (13, 14),\n    # Team D internal connections\n    (15, 16),\n    (15, 17),\n    (16, 18),\n    (17, 18),\n    (17, 19),\n    (18, 19),\n    # Cross-group bridges between communities\n    (0, 5),\n    (4, 10),\n    (9, 15),\n    (14, 19),\n    (2, 6),\n    (8, 11),\n    (13, 16),\n    # Direct Team A <-> Team D bridges close the A-B-C-D chain into a loop,\n    # which pulls the force-directed layout into a rounder shape instead of\n    # stretching diagonally and leaving the opposite canvas corners empty\n    (3, 17),\n    (4, 15),\n]\n\n# Node degree (connection count)\nn = len(nodes)\ndegrees = {node[\"id\"]: 0 for node in nodes}\nfor src, tgt in edges:\n    degrees[src] += 1\n    degrees[tgt] += 1\n\n# Force-directed (Fruchterman-Reingold) spring layout, vectorized with numpy\nk = 0.4  # optimal inter-node distance\nedge_src = np.array([e[0] for e in edges])\nedge_tgt = np.array([e[1] for e in edges])\npositions = np.random.rand(n, 2) * 2 - 1\n\niterations = 600\nfor iteration in range(iterations):\n    delta = positions[:, np.newaxis, :] - positions[np.newaxis, :, :]\n    dist = np.linalg.norm(delta, axis=-1)\n    np.fill_diagonal(dist, np.inf)  # ignore self-repulsion\n    dist = np.maximum(dist, 0.01)\n    displacement = ((k * k / dist**2)[..., np.newaxis] * delta).sum(axis=1)\n\n    edge_delta = positions[edge_src] - positions[edge_tgt]\n    edge_dist = np.maximum(np.linalg.norm(edge_delta, axis=-1), 0.01)\n    attraction = (edge_dist / k)[:, np.newaxis] * edge_delta\n    np.add.at(displacement, edge_src, -attraction)\n    np.add.at(displacement, edge_tgt, attraction)\n\n    cooling = 1 - iteration / iterations\n    disp_norm = np.linalg.norm(displacement, axis=1, keepdims=True)\n    unit = np.divide(displacement, disp_norm, out=np.zeros_like(displacement), where=disp_norm > 0)\n    positions += unit * np.minimum(disp_norm, 0.1 * cooling)\n\n# Center on the bounding box (not the mean) and scale uniformly (not per-axis)\n# so inter-node distances stay undistorted once drawn on the square canvas below\nbbox_min, bbox_max = positions.min(axis=0), positions.max(axis=0)\npositions -= (bbox_min + bbox_max) / 2\npositions /= (bbox_max - bbox_min).max() / 1.7\n\ndf_nodes = pd.DataFrame(\n    {\n        \"x\": positions[:, 0],\n        \"y\": positions[:, 1],\n        \"label\": [node[\"label\"] for node in nodes],\n        \"group\": [node[\"group\"] for node in nodes],\n        \"degree\": [degrees[node[\"id\"]] for node in nodes],\n    }\n)\n\n# Square canvas: a force-directed layout has no preferred horizontal axis\nfig, ax = plt.subplots(figsize=(6, 6), dpi=400)\nax.set_aspect(\"equal\", adjustable=\"box\")\n\n# Draw edges beneath the nodes\nfor src, tgt in edges:\n    ax.plot(\n        [positions[src, 0], positions[tgt, 0]],\n        [positions[src, 1], positions[tgt, 1]],\n        color=INK_SOFT,\n        linewidth=1.5,\n        alpha=0.35,\n        zorder=1,\n    )\n\n# Draw nodes with seaborn — hue for community, size for degree\nsns.scatterplot(\n    data=df_nodes,\n    x=\"x\",\n    y=\"y\",\n    hue=\"group\",\n    hue_order=[\"Team A\", \"Team B\", \"Team C\", \"Team D\"],\n    size=\"degree\",\n    sizes=(190, 400),\n    palette=IMPRINT_PALETTE,\n    edgecolor=PAGE_BG,\n    linewidth=2,\n    alpha=0.95,\n    legend=\"brief\",\n    ax=ax,\n    zorder=2,\n)\n\n# Labels sit just below each node, so text color never clashes with the node fill\ntext_artists = [\n    ax.text(\n        row[\"x\"],\n        row[\"y\"] - 0.09,\n        row[\"label\"],\n        fontsize=10,\n        fontweight=\"bold\",\n        ha=\"center\",\n        va=\"top\",\n        color=INK,\n        zorder=3,\n    )\n    for _, row in df_nodes.iterrows()\n]\n\ntitle = \"network-basic · python · seaborn · anyplot.ai\"\nax.set_title(title, fontsize=12, fontweight=\"medium\", color=INK)\n# Fit the view tightly around the network (plus margin for nodes/labels) so the\n# square canvas isn't mostly empty around an off-center force-directed layout\nmargin = 0.2\nax.set_xlim(positions[:, 0].min() - margin, positions[:, 0].max() + margin)\n# Extra headroom on top keeps the community legend clear of the topmost node\nax.set_ylim(positions[:, 1].min() - margin - 0.09, positions[:, 1].max() + margin + 0.35)\nax.axis(\"off\")\n\n# Keep only the community legend entries — drop the automatic size legend\nhandles, labels = ax.get_legend_handles_labels()\ncommunity_names = set(df_nodes[\"group\"])\ncommunity_handles = [h for h, lbl in zip(handles, labels, strict=False) if lbl in community_names]\ncommunity_labels = [lbl for lbl in labels if lbl in community_names]\nlegend = ax.legend(\n    community_handles,\n    community_labels,\n    loc=\"upper left\",\n    fontsize=8,\n    framealpha=0.95,\n    title=\"Community\",\n    title_fontsize=10,\n)\nlegend.get_frame().set_facecolor(ELEVATED_BG)\nlegend.get_frame().set_edgecolor(INK_SOFT)\n\nplt.tight_layout()\n\n# Measure the actual rendered label boxes (after the view/layout is final) and\n# nudge any that collide apart horizontally — cheaper than hand-tuning offsets\n# per node, and it adapts automatically if the layout places different nodes\n# close together on a re-run\nfig.canvas.draw()\nrenderer = fig.canvas.get_renderer()\ninv = ax.transData.inverted()\nlabel_boxes = [inv.transform_bbox(t.get_window_extent(renderer)) for t in text_artists]\nfor i in range(len(text_artists)):\n    for j in range(i + 1, len(text_artists)):\n        if not label_boxes[i].overlaps(label_boxes[j]):\n            continue\n        left, right = (i, j) if label_boxes[i].x0 < label_boxes[j].x0 else (j, i)\n        overlap_x = min(label_boxes[left].x1, label_boxes[right].x1) - label_boxes[right].x0\n        push = overlap_x / 2 + 0.01\n        xl, yl = text_artists[left].get_position()\n        xr, yr = text_artists[right].get_position()\n        text_artists[left].set_position((xl - push, yl))\n        text_artists[left].set_ha(\"right\")\n        text_artists[right].set_position((xr + push, yr))\n        text_artists[right].set_ha(\"left\")\n\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}