{"spec_id":"network-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nnetwork-basic: Basic Network Graph\nLibrary: letsplot 4.11.0 | Python 3.13.14\nQuality: 87/100 | Updated: 2026-07-24\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_curve,\n    geom_point,\n    geom_text,\n    ggplot,\n    ggsize,\n    labs,\n    layer_tooltips,\n    scale_color_manual,\n    scale_size_identity,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n)\nfrom lets_plot.export import ggsave\n\n\nLetsPlot.setup_html()\n\n# Theme 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\"\nEDGE_COLOR = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Imprint categorical palette (first series always #009E73)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n\n# Data: A small social network with 20 people in 4 departments\nnp.random.seed(42)\n\nnodes = [\n    {\"id\": 0, \"label\": \"Alice\", \"group\": 0},\n    {\"id\": 1, \"label\": \"Bob\", \"group\": 0},\n    {\"id\": 2, \"label\": \"Carol\", \"group\": 0},\n    {\"id\": 3, \"label\": \"David\", \"group\": 0},\n    {\"id\": 4, \"label\": \"Eve\", \"group\": 0},\n    {\"id\": 5, \"label\": \"Frank\", \"group\": 1},\n    {\"id\": 6, \"label\": \"Grace\", \"group\": 1},\n    {\"id\": 7, \"label\": \"Henry\", \"group\": 1},\n    {\"id\": 8, \"label\": \"Ivy\", \"group\": 1},\n    {\"id\": 9, \"label\": \"Jack\", \"group\": 1},\n    {\"id\": 10, \"label\": \"Kate\", \"group\": 2},\n    {\"id\": 11, \"label\": \"Leo\", \"group\": 2},\n    {\"id\": 12, \"label\": \"Mia\", \"group\": 2},\n    {\"id\": 13, \"label\": \"Noah\", \"group\": 2},\n    {\"id\": 14, \"label\": \"Olivia\", \"group\": 2},\n    {\"id\": 15, \"label\": \"Paul\", \"group\": 3},\n    {\"id\": 16, \"label\": \"Quinn\", \"group\": 3},\n    {\"id\": 17, \"label\": \"Ryan\", \"group\": 3},\n    {\"id\": 18, \"label\": \"Sara\", \"group\": 3},\n    {\"id\": 19, \"label\": \"Tom\", \"group\": 3},\n]\n\nedges = [\n    # Group 0 internal connections\n    (0, 1),\n    (0, 2),\n    (1, 2),\n    (1, 3),\n    (2, 4),\n    (3, 4),\n    # Group 1 internal connections\n    (5, 6),\n    (5, 7),\n    (6, 8),\n    (7, 8),\n    (7, 9),\n    (8, 9),\n    # Group 2 internal connections\n    (10, 11),\n    (10, 12),\n    (11, 13),\n    (12, 13),\n    (12, 14),\n    (13, 14),\n    # Group 3 internal connections\n    (15, 16),\n    (15, 17),\n    (16, 18),\n    (17, 18),\n    (17, 19),\n    (18, 19),\n    # Cross-group connections (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]\n\n# Layout: each group anchored to a canvas quadrant, force-directed within each group\nn = len(nodes)\ngroup_corners = {\n    0: np.array([0.18, 0.77]),  # Research: top-left\n    1: np.array([0.84, 0.77]),  # Marketing: top-right\n    2: np.array([0.18, 0.23]),  # Engineering: bottom-left\n    3: np.array([0.84, 0.23]),  # Design: bottom-right\n}\n\n# Place each group's nodes in a circle around their quadrant center\ngroup_node_map = {g: [i for i, nd in enumerate(nodes) if nd[\"group\"] == g] for g in range(4)}\npositions = np.zeros((n, 2))\nfor group_id, node_indices in group_node_map.items():\n    m = len(node_indices)\n    center = group_corners[group_id]\n    for idx, ni in enumerate(node_indices):\n        angle = (idx / m) * 2 * np.pi\n        positions[ni] = center + 0.14 * np.array([np.cos(angle), np.sin(angle)])\n\n# Intra-group spring layout with centroid anchor (200 iterations)\nk = 0.13\nfor iteration in range(200):\n    displacement = np.zeros((n, 2))\n\n    # Repulsion between same-group nodes only\n    for i in range(n):\n        for j in range(i + 1, n):\n            if nodes[i][\"group\"] != nodes[j][\"group\"]:\n                continue\n            diff = positions[i] - positions[j]\n            dist = max(np.linalg.norm(diff), 0.001)\n            force = (k * k / dist) * (diff / dist)\n            displacement[i] += force\n            displacement[j] -= force\n\n    # Attraction along intra-group edges only\n    for src, tgt in edges:\n        if nodes[src][\"group\"] != nodes[tgt][\"group\"]:\n            continue\n        diff = positions[src] - positions[tgt]\n        dist = max(np.linalg.norm(diff), 0.001)\n        force = (dist * dist / k) * (diff / dist)\n        displacement[src] -= force\n        displacement[tgt] += force\n\n    # Strong centroid anchor keeps each group in its quadrant\n    for i, node in enumerate(nodes):\n        center = group_corners[node[\"group\"]]\n        displacement[i] += 0.35 * (center - positions[i])\n\n    cooling = 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.025 * cooling)\n\n# The panel maps more pixels per data-unit horizontally than vertically (16:9\n# canvas minus the right-hand legend column), so an isotropic spring layout\n# renders each quadrant's circular cluster as a squashed ellipse. Stretch the\n# vertical spread around each group's centroid to compensate, without shrinking\n# the horizontal footprint that already fills the canvas width.\nY_ASPECT_COMPENSATION = 1.45\nfor i, node in enumerate(nodes):\n    center = group_corners[node[\"group\"]]\n    positions[i][1] = center[1] + (positions[i][1] - center[1]) * Y_ASPECT_COMPENSATION\n\npos = {node[\"id\"]: positions[i] for i, node in enumerate(nodes)}\n\n# Calculate node degrees for sizing and tooltips\ndegrees = {node[\"id\"]: 0 for node in nodes}\nfor src, tgt in edges:\n    degrees[src] += 1\n    degrees[tgt] += 1\n\ngroup_names = [\"Research\", \"Marketing\", \"Engineering\", \"Design\"]\n\n# Build dataframes\nedge_data = []\nfor src, tgt in edges:\n    x0, y0 = pos[src]\n    x1, y1 = pos[tgt]\n    edge_data.append({\"x\": x0, \"y\": y0, \"xend\": x1, \"yend\": y1})\ndf_edges = pd.DataFrame(edge_data)\n\nnode_data = []\nfor node in nodes:\n    x, y = pos[node[\"id\"]]\n    degree = degrees[node[\"id\"]]\n    node_data.append(\n        {\n            \"x\": x,\n            \"y\": y,\n            \"label\": node[\"label\"],\n            \"group\": group_names[node[\"group\"]],\n            # Wider spread than a flat linear term so hub nodes (higher degree)\n            # stand out as a clear focal point rather than a subtle size nudge.\n            \"size\": 6 + degree * 1.8,\n            \"degree\": degree,\n            \"label_y\": y + 0.095,\n        }\n    )\ndf_nodes = pd.DataFrame(node_data)\n\n# Plot — no coord_fixed so the network fills the full 16:9 landscape canvas\nplot = (\n    ggplot()\n    + geom_curve(\n        aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"),\n        data=df_edges,\n        color=EDGE_COLOR,\n        size=1.5,\n        alpha=0.6,\n        curvature=0.15,\n    )\n    + geom_point(\n        aes(x=\"x\", y=\"y\", color=\"group\", size=\"size\"),\n        data=df_nodes,\n        tooltips=layer_tooltips().line(\"@label\").line(\"Department|@group\").line(\"Connections|@degree\"),\n        stroke=1.5,\n        alpha=0.95,\n    )\n    + geom_text(aes(x=\"x\", y=\"label_y\", label=\"label\"), data=df_nodes, size=6, color=INK_SOFT, fontface=\"bold\")\n    + scale_color_manual(values=IMPRINT, name=\"Department\")\n    + scale_size_identity()\n    + scale_x_continuous(limits=(-0.05, 1.05))\n    + scale_y_continuous(limits=(-0.05, 1.05))\n    + labs(title=\"Office Social Network · network-basic · letsplot · anyplot.ai\")\n    + ggsize(800, 450)\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        plot_title=element_text(size=16, face=\"bold\", color=INK),\n        axis_title=element_blank(),\n        axis_text=element_blank(),\n        axis_ticks=element_blank(),\n        axis_line=element_blank(),\n        panel_grid=element_blank(),\n        panel_border=element_blank(),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_text=element_text(size=12, color=INK_SOFT),\n        legend_title=element_text(size=14, face=\"bold\", color=INK),\n        legend_position=\"right\",\n        legend_key_size=14,\n        legend_spacing=4,\n        legend_box_spacing=6,\n        legend_margin=6,\n        plot_margin=[15, 8, 10, 10],\n    )\n)\n\n# Save\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}