{"spec_id":"network-force-directed","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nnetwork-force-directed: Force-Directed Graph\nLibrary: plotly 6.8.0 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-07-01\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\n\n\n# Theme-adaptive chrome tokens\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\"\nGRID = \"rgba(26,26,23,0.15)\" if THEME == \"light\" else \"rgba(240,239,232,0.15)\"\n\n# Imprint categorical palette (positions 1-3)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\n\nnp.random.seed(42)\n\n# Social network: 50 nodes in 3 teams, corporate collaboration graph\ncommunity_sizes = [18, 17, 15]\ncommunity_names = [\"Engineering\", \"Marketing\", \"Sales\"]\nnodes = []\nnode_id = 0\nfor comm_idx, size in enumerate(community_sizes):\n    for _ in range(size):\n        nodes.append({\"id\": node_id, \"community\": comm_idx})\n        node_id += 1\n\n# Intra-community edges (dense within-team connections)\nedges_intra = []\nfor i in range(18):\n    for j in range(i + 1, 18):\n        if np.random.random() < 0.3:\n            edges_intra.append((i, j))\nfor i in range(18, 35):\n    for j in range(i + 1, 35):\n        if np.random.random() < 0.3:\n            edges_intra.append((i, j))\nfor i in range(35, 50):\n    for j in range(i + 1, 50):\n        if np.random.random() < 0.3:\n            edges_intra.append((i, j))\n\n# Inter-community bridge edges (sparse cross-team links)\nedges_bridge = [(0, 18), (5, 20), (10, 25), (18, 35), (22, 40), (30, 45), (8, 38), (15, 48)]\n\nall_edges = edges_intra + edges_bridge\n\n# Force-directed layout: Fruchterman-Reingold algorithm\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.86 + 0.07\npos = {node[\"id\"]: positions[i] for i, node in enumerate(nodes)}\n\n# Node degrees\ndegrees = {node[\"id\"]: 0 for node in nodes}\nfor src, tgt in all_edges:\n    degrees[src] += 1\n    degrees[tgt] += 1\n\nfig = go.Figure()\n\n# Intra-community edges: thin and subtle — team cohesion background\nintra_x, intra_y = [], []\nfor src, tgt in edges_intra:\n    x0, y0 = pos[src]\n    x1, y1 = pos[tgt]\n    intra_x.extend([x0, x1, None])\n    intra_y.extend([y0, y1, None])\n\nfig.add_trace(\n    go.Scatter(\n        x=intra_x,\n        y=intra_y,\n        mode=\"lines\",\n        line={\"width\": 1.2, \"color\": INK_SOFT},\n        opacity=0.2,\n        hoverinfo=\"none\",\n        showlegend=False,\n    )\n)\n\n# Bridge edges: thicker dotted lines — highlight cross-team connections\nbridge_x, bridge_y = [], []\nfor src, tgt in edges_bridge:\n    x0, y0 = pos[src]\n    x1, y1 = pos[tgt]\n    bridge_x.extend([x0, x1, None])\n    bridge_y.extend([y0, y1, None])\n\nfig.add_trace(\n    go.Scatter(\n        x=bridge_x,\n        y=bridge_y,\n        mode=\"lines\",\n        line={\"width\": 2.2, \"color\": INK_MUTED, \"dash\": \"dot\"},\n        opacity=0.65,\n        hoverinfo=\"none\",\n        name=\"Cross-team link\",\n        showlegend=True,\n    )\n)\n\n# Node traces: one per community for legend grouping\nfor comm_idx, comm_name in enumerate(community_names):\n    comm_nodes = [node for node in nodes if node[\"community\"] == comm_idx]\n    x_vals = [pos[node[\"id\"]][0] for node in comm_nodes]\n    y_vals = [pos[node[\"id\"]][1] for node in comm_nodes]\n    sizes = [10 + degrees[node[\"id\"]] * 2 for node in comm_nodes]\n    hover_text = [\n        f\"Node {node['id']}<br>Team: {comm_name}<br>Connections: {degrees[node['id']]}\" for node in comm_nodes\n    ]\n\n    fig.add_trace(\n        go.Scatter(\n            x=x_vals,\n            y=y_vals,\n            mode=\"markers\",\n            marker={\"size\": sizes, \"color\": IMPRINT[comm_idx], \"line\": {\"width\": 2, \"color\": PAGE_BG}, \"opacity\": 0.92},\n            name=comm_name,\n            text=hover_text,\n            hoverinfo=\"text\",\n        )\n    )\n\n# Hub annotations: label the top-degree node per community only\ntitle_str = \"network-force-directed · python · plotly · anyplot.ai\"\nn_chars = len(title_str)\nratio = 67 / n_chars if n_chars > 67 else 1.0\ntitle_fontsize = max(round(16 * ratio), 11)\n\nhub_annotations = []\nfor comm_idx, comm_name in enumerate(community_names):\n    comm_nodes = [node for node in nodes if node[\"community\"] == comm_idx]\n    top_node = max(comm_nodes, key=lambda node: degrees[node[\"id\"]])\n    x, y = pos[top_node[\"id\"]]\n    hub_annotations.append(\n        {\n            \"x\": x,\n            \"y\": y + 0.055,\n            \"text\": f\"{comm_name} hub\",\n            \"showarrow\": False,\n            \"font\": {\"size\": 16, \"color\": INK, \"family\": \"Arial Black\"},\n            \"bgcolor\": ELEVATED_BG,\n            \"bordercolor\": INK_SOFT,\n            \"borderwidth\": 1,\n            \"borderpad\": 4,\n            \"xanchor\": \"center\",\n            \"yanchor\": \"bottom\",\n        }\n    )\n\nfig.update_layout(\n    autosize=False,\n    title={\"text\": title_str, \"font\": {\"size\": title_fontsize, \"color\": INK}, \"x\": 0.5, \"xanchor\": \"center\"},\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font={\"color\": INK},\n    xaxis={\"showgrid\": False, \"zeroline\": False, \"showticklabels\": False, \"range\": [-0.05, 1.05]},\n    yaxis={\"showgrid\": False, \"zeroline\": False, \"showticklabels\": False, \"range\": [-0.05, 1.05]},\n    legend={\n        \"title\": {\"text\": \"Teams\", \"font\": {\"size\": 12, \"color\": INK}},\n        \"font\": {\"size\": 10, \"color\": INK_SOFT},\n        \"x\": 0.02,\n        \"y\": 0.98,\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n    },\n    annotations=hub_annotations,\n    margin={\"l\": 40, \"r\": 40, \"t\": 80, \"b\": 40},\n)\n\nfig.write_image(f\"plot-{THEME}.png\", width=600, height=600, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}