{"spec_id":"network-basic","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nnetwork-basic: Basic Network Graph\nLibrary: plotnine 0.15.7 | Python 3.13.14\nQuality: 89/100 | Updated: 2026-07-24\n\"\"\"\n\nimport os\nimport sys\n\nimport numpy as np\nimport pandas as pd\n\n\ntry:\n    from plotnine import (\n        aes,\n        annotate,\n        coord_cartesian,\n        element_blank,\n        element_rect,\n        element_text,\n        geom_point,\n        geom_segment,\n        geom_text,\n        ggplot,\n        guide_legend,\n        labs,\n        scale_color_manual,\n        scale_size_area,\n        theme,\n    )\nexcept ImportError:\n    # This file is named plotnine.py; remove current dir so the library is found instead\n    sys.path = [p for p in sys.path if os.path.abspath(p) != os.path.dirname(os.path.abspath(__file__))]\n    from plotnine import (\n        aes,\n        annotate,\n        coord_cartesian,\n        element_blank,\n        element_rect,\n        element_text,\n        geom_point,\n        geom_segment,\n        geom_text,\n        ggplot,\n        guide_legend,\n        labs,\n        scale_color_manual,\n        scale_size_area,\n        theme,\n    )\n\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\"\n\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n\n# Data: social network with 20 people in 4 communities\nnp.random.seed(42)\n\nnodes = [\n    {\"id\": 0, \"label\": \"Alice\", \"group\": \"Engineering\"},\n    {\"id\": 1, \"label\": \"Bob\", \"group\": \"Engineering\"},\n    {\"id\": 2, \"label\": \"Carol\", \"group\": \"Engineering\"},\n    {\"id\": 3, \"label\": \"David\", \"group\": \"Engineering\"},\n    {\"id\": 4, \"label\": \"Eve\", \"group\": \"Engineering\"},\n    {\"id\": 5, \"label\": \"Frank\", \"group\": \"Design\"},\n    {\"id\": 6, \"label\": \"Grace\", \"group\": \"Design\"},\n    {\"id\": 7, \"label\": \"Henry\", \"group\": \"Design\"},\n    {\"id\": 8, \"label\": \"Ivy\", \"group\": \"Design\"},\n    {\"id\": 9, \"label\": \"Jack\", \"group\": \"Design\"},\n    {\"id\": 10, \"label\": \"Kate\", \"group\": \"Sales\"},\n    {\"id\": 11, \"label\": \"Leo\", \"group\": \"Sales\"},\n    {\"id\": 12, \"label\": \"Mia\", \"group\": \"Sales\"},\n    {\"id\": 13, \"label\": \"Noah\", \"group\": \"Sales\"},\n    {\"id\": 14, \"label\": \"Olivia\", \"group\": \"Sales\"},\n    {\"id\": 15, \"label\": \"Paul\", \"group\": \"Support\"},\n    {\"id\": 16, \"label\": \"Quinn\", \"group\": \"Support\"},\n    {\"id\": 17, \"label\": \"Ryan\", \"group\": \"Support\"},\n    {\"id\": 18, \"label\": \"Sara\", \"group\": \"Support\"},\n    {\"id\": 19, \"label\": \"Tom\", \"group\": \"Support\"},\n]\n\nedges = [\n    (0, 1),\n    (0, 2),\n    (1, 2),\n    (1, 3),\n    (2, 4),\n    (3, 4),\n    (5, 6),\n    (5, 7),\n    (6, 8),\n    (7, 8),\n    (7, 9),\n    (8, 9),\n    (10, 11),\n    (10, 12),\n    (11, 13),\n    (12, 13),\n    (12, 14),\n    (13, 14),\n    (15, 16),\n    (15, 17),\n    (16, 18),\n    (17, 18),\n    (17, 19),\n    (18, 19),\n    (0, 5),\n    (4, 10),\n    (9, 15),\n    (14, 19),\n    (2, 6),\n    (8, 11),\n    (13, 16),\n]\n\n# Node degrees\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# Quadrant-based initialization for balanced 4-cluster arrangement\nquadrant_centers = {\n    \"Engineering\": np.array([-0.6, 0.6]),\n    \"Design\": np.array([0.6, 0.6]),\n    \"Sales\": np.array([-0.6, -0.6]),\n    \"Support\": np.array([0.6, -0.6]),\n}\npositions = np.zeros((n, 2))\nfor i, node in enumerate(nodes):\n    positions[i] = quadrant_centers[node[\"group\"]] + np.random.randn(2) * 0.12\n\n# Vectorized Fruchterman-Reingold spring layout\nk = 0.52\nfor iteration in range(200):\n    diff = positions[:, None, :] - positions[None, :, :]  # (n, n, 2)\n    dist = np.linalg.norm(diff, axis=2, keepdims=True).clip(0.01)\n    repulsion = (k * k / dist**2) * diff\n    np.fill_diagonal(repulsion[:, :, 0], 0)\n    np.fill_diagonal(repulsion[:, :, 1], 0)\n    disp = repulsion.sum(axis=1)\n\n    for src, tgt in edges:\n        d = positions[src] - positions[tgt]\n        dn = max(np.linalg.norm(d), 0.01)\n        f = d * dn / k\n        disp[src] -= f\n        disp[tgt] += f\n\n    norms = np.linalg.norm(disp, axis=1, keepdims=True).clip(1e-10)\n    step = np.minimum(norms, 0.1 * (1 - iteration / 200))\n    positions += (disp / norms) * step\n\npos_min = positions.min(axis=0)\npos_max = positions.max(axis=0)\npositions = (positions - pos_min) / (pos_max - pos_min + 1e-6) * 0.9 + 0.05\n\n# Build DataFrames\ngroup_order = [\"Engineering\", \"Design\", \"Sales\", \"Support\"]\nnode_df = pd.DataFrame(\n    {\n        \"x\": [positions[i, 0] for i in range(n)],\n        \"y\": [positions[i, 1] for i in range(n)],\n        \"label\": [node[\"label\"] for node in nodes],\n        \"group\": pd.Categorical([node[\"group\"] for node in nodes], categories=group_order, ordered=True),\n        \"degree\": [float(degrees[node[\"id\"]]) for node in nodes],\n    }\n)\n\nedge_df = pd.DataFrame(\n    [\n        {\"x\": positions[src, 0], \"y\": positions[src, 1], \"xend\": positions[tgt, 0], \"yend\": positions[tgt, 1]}\n        for src, tgt in edges\n    ]\n)\n\ngroup_colors = {\"Engineering\": IMPRINT[0], \"Design\": IMPRINT[1], \"Sales\": IMPRINT[2], \"Support\": IMPRINT[3]}\n\n# Hub nodes for emphasis — top 20% by degree\nhub_threshold = node_df[\"degree\"].quantile(0.8)\nhub_df = node_df[node_df[\"degree\"] >= hub_threshold].copy()\ntop_hub = node_df.loc[node_df[\"degree\"].idxmax()]\n\n# Plot\nplot = (\n    ggplot()\n    + geom_segment(\n        data=edge_df, mapping=aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), color=INK_SOFT, size=0.4, alpha=0.45\n    )\n    # Halo layer highlights hub nodes — uses scale_size_area's proportional area encoding\n    + geom_point(data=hub_df, mapping=aes(x=\"x\", y=\"y\"), size=13, color=INK_SOFT, alpha=0.18, show_legend=False)\n    + geom_point(data=node_df, mapping=aes(x=\"x\", y=\"y\", color=\"group\", size=\"degree\"), alpha=0.92)\n    + geom_text(data=node_df, mapping=aes(x=\"x\", y=\"y\", label=\"label\"), color=INK, size=9, nudge_y=0.045, va=\"bottom\")\n    # Annotate top hub node to draw reader's eye\n    + annotate(\n        \"text\",\n        x=float(top_hub[\"x\"]),\n        y=float(top_hub[\"y\"]) - 0.10,\n        label=\"hub\",\n        color=INK_SOFT,\n        size=7,\n        ha=\"center\",\n        fontstyle=\"italic\",\n    )\n    + scale_color_manual(values=group_colors, name=\"Community\")\n    # scale_size_area ensures area (not radius) is proportional to degree value.\n    # limits=(0, max) anchors the domain at a literal zero degree — without it, plotnine\n    # maps the *observed minimum* degree to size 0, making the lowest-degree node invisible.\n    + scale_size_area(\n        max_size=10,\n        limits=(0, node_df[\"degree\"].max()),\n        guide=guide_legend(title=\"Degree\", override_aes={\"color\": INK_SOFT, \"alpha\": 0.85}),\n    )\n    + coord_cartesian(xlim=(-0.03, 1.03), ylim=(-0.10, 1.03))\n    + labs(title=\"network-basic · plotnine · anyplot.ai\")\n    + theme(\n        figure_size=(8, 4.5),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        panel_grid_major=element_blank(),\n        panel_grid_minor=element_blank(),\n        panel_border=element_blank(),\n        axis_title=element_blank(),\n        axis_text=element_blank(),\n        axis_ticks=element_blank(),\n        axis_line=element_blank(),\n        plot_title=element_text(color=INK, size=12, ha=\"center\"),\n        legend_background=element_rect(fill=ELEVATED_BG, color=None),\n        legend_text=element_text(color=INK_SOFT, size=8),\n        legend_title=element_text(color=INK, size=9),\n        legend_key=element_rect(fill=ELEVATED_BG),\n        legend_box_spacing=0.01,\n        plot_margin=0.005,\n    )\n)\n\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\")\n"}