{"spec_id":"network-directed","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nnetwork-directed: Directed Network Graph\nLibrary: letsplot 4.9.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-14\n\"\"\"\n\nimport os\nimport shutil\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    arrow,\n    element_rect,\n    element_text,\n    geom_point,\n    geom_segment,\n    geom_text,\n    ggplot,\n    ggsize,\n    labs,\n    scale_fill_manual,\n    theme,\n    theme_void,\n    xlim,\n    ylim,\n)\nfrom lets_plot.export import ggsave\n\n\nLetsPlot.setup_html()\n\nnp.random.seed(42)\n\n# Theme-adaptive colors\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# Okabe-Ito palette (positions 1-4 for node groups)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n\n# Define software package dependency network\nnodes = [\n    {\"id\": \"app\", \"label\": \"App\", \"group\": \"Application\"},\n    {\"id\": \"api\", \"label\": \"API\", \"group\": \"Core\"},\n    {\"id\": \"auth\", \"label\": \"Auth\", \"group\": \"Core\"},\n    {\"id\": \"db\", \"label\": \"Database\", \"group\": \"Core\"},\n    {\"id\": \"cache\", \"label\": \"Cache\", \"group\": \"Infrastructure\"},\n    {\"id\": \"config\", \"label\": \"Config\", \"group\": \"Utility\"},\n    {\"id\": \"logger\", \"label\": \"Logger\", \"group\": \"Utility\"},\n    {\"id\": \"utils\", \"label\": \"Utils\", \"group\": \"Utility\"},\n    {\"id\": \"http\", \"label\": \"HTTP Client\", \"group\": \"Infrastructure\"},\n    {\"id\": \"queue\", \"label\": \"Queue\", \"group\": \"Infrastructure\"},\n]\n\n# Directed edges (source depends on target, arrow from source to target)\nedges = [\n    (\"app\", \"api\"),\n    (\"app\", \"config\"),\n    (\"api\", \"auth\"),\n    (\"api\", \"db\"),\n    (\"api\", \"cache\"),\n    (\"api\", \"logger\"),\n    (\"auth\", \"db\"),\n    (\"auth\", \"config\"),\n    (\"auth\", \"logger\"),\n    (\"db\", \"config\"),\n    (\"db\", \"logger\"),\n    (\"cache\", \"config\"),\n    (\"cache\", \"logger\"),\n    (\"http\", \"config\"),\n    (\"http\", \"logger\"),\n    (\"queue\", \"config\"),\n    (\"queue\", \"logger\"),\n    (\"api\", \"http\"),\n    (\"api\", \"queue\"),\n    (\"utils\", \"logger\"),\n]\n\n# Create node positions using hierarchical layout\nnode_positions = {\n    \"app\": (0.5, 1.0),\n    \"api\": (0.5, 0.75),\n    \"auth\": (0.15, 0.5),\n    \"db\": (0.5, 0.5),\n    \"cache\": (0.85, 0.5),\n    \"http\": (0.05, 0.25),\n    \"queue\": (0.3, 0.25),\n    \"config\": (0.55, 0.25),\n    \"logger\": (0.8, 0.25),\n    \"utils\": (1.0, 0.75),\n}\n\n# Build node dataframe\nnode_df = pd.DataFrame(nodes)\nnode_df[\"x\"] = node_df[\"id\"].map(lambda n: node_positions[n][0])\nnode_df[\"y\"] = node_df[\"id\"].map(lambda n: node_positions[n][1])\n\n# Map groups to Okabe-Ito colors\ngroup_order = [\"Application\", \"Core\", \"Infrastructure\", \"Utility\"]\ngroup_colors = {group: IMPRINT[i] for i, group in enumerate(group_order)}\nnode_df[\"color\"] = node_df[\"group\"].map(group_colors)\n\n# Build edge dataframe with arrow endpoints\nedge_data = []\nfor source, target in edges:\n    x0, y0 = node_positions[source]\n    x1, y1 = node_positions[target]\n\n    # Shorten edges to not overlap with nodes\n    dx, dy = x1 - x0, y1 - y0\n    length = np.sqrt(dx**2 + dy**2)\n    if length > 0:\n        # Shrink by node radius on each end\n        shrink = 0.05\n        x0_adj = x0 + (dx / length) * shrink\n        y0_adj = y0 + (dy / length) * shrink\n        x1_adj = x1 - (dx / length) * shrink * 1.8\n        y1_adj = y1 - (dy / length) * shrink * 1.8\n    else:\n        x0_adj, y0_adj, x1_adj, y1_adj = x0, y0, x1, y1\n\n    edge_data.append({\"x\": x0_adj, \"y\": y0_adj, \"xend\": x1_adj, \"yend\": y1_adj})\n\nedge_df = pd.DataFrame(edge_data)\n\n# Create the plot\nplot = (\n    ggplot()\n    # Draw edges with arrows\n    + geom_segment(\n        aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"),\n        data=edge_df,\n        color=INK_SOFT,\n        size=1.2,\n        arrow=arrow(length=12, type=\"closed\"),\n        alpha=0.5,\n    )\n    # Draw nodes\n    + geom_point(aes(x=\"x\", y=\"y\", fill=\"group\"), data=node_df, size=22, shape=21, stroke=2.5, color=\"white\")\n    # Add node labels\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=node_df, size=12, color=INK, fontface=\"bold\", nudge_y=-0.06)\n    # Color scale\n    + scale_fill_manual(values=IMPRINT, name=\"Module Type\")\n    # Theme and styling\n    + theme_void()\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        plot_title=element_text(size=28, face=\"bold\", hjust=0.5, color=INK),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_title=element_text(size=18, color=INK),\n        legend_text=element_text(size=16, color=INK_SOFT),\n        legend_position=\"right\",\n        plot_margin=[60, 80, 80, 60],\n    )\n    + labs(title=\"network-directed · letsplot · anyplot.ai\")\n    + ggsize(1600, 900)\n    + xlim(-0.05, 1.15)\n    + ylim(0.1, 1.1)\n)\n\n# Save as PNG and HTML\nggsave(plot, f\"plot-{THEME}.png\", scale=3)\nggsave(plot, f\"plot-{THEME}.html\")\n\n# Move files from lets-plot-images subdirectory to current directory\nif os.path.exists(f\"lets-plot-images/plot-{THEME}.png\"):\n    shutil.move(f\"lets-plot-images/plot-{THEME}.png\", f\"plot-{THEME}.png\")\nif os.path.exists(f\"lets-plot-images/plot-{THEME}.html\"):\n    shutil.move(f\"lets-plot-images/plot-{THEME}.html\", f\"plot-{THEME}.html\")\nif os.path.exists(\"lets-plot-images\") and not os.listdir(\"lets-plot-images\"):\n    os.rmdir(\"lets-plot-images\")\n"}