{"spec_id":"hive-basic","library":"letsplot","language":"python","code":"# ruff: noqa: F403, F405\n\"\"\"anyplot.ai\nhive-basic: Basic Hive Plot\nLibrary: lets-plot | Python 3.13\nQuality: pending | Created: 2025-12-24\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\n\n\nLetsPlot.setup_html()\n\n# Theme tokens (see prompts/default-style-guide.md)\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 for categorical node types (positions 1-3)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\n\n# Data: Software module dependency network with 3 module types\nnp.random.seed(42)\n\n# Define nodes with axis assignments (core, utility, interface modules)\nnodes = [\n    # Core modules (axis 0)\n    {\"id\": \"core_main\", \"axis\": \"Core\", \"degree\": 8},\n    {\"id\": \"core_data\", \"axis\": \"Core\", \"degree\": 6},\n    {\"id\": \"core_config\", \"axis\": \"Core\", \"degree\": 5},\n    {\"id\": \"core_logger\", \"axis\": \"Core\", \"degree\": 4},\n    {\"id\": \"core_cache\", \"axis\": \"Core\", \"degree\": 3},\n    # Utility modules (axis 1)\n    {\"id\": \"util_parser\", \"axis\": \"Utility\", \"degree\": 5},\n    {\"id\": \"util_validator\", \"axis\": \"Utility\", \"degree\": 4},\n    {\"id\": \"util_formatter\", \"axis\": \"Utility\", \"degree\": 6},\n    {\"id\": \"util_crypto\", \"axis\": \"Utility\", \"degree\": 3},\n    {\"id\": \"util_compress\", \"axis\": \"Utility\", \"degree\": 2},\n    {\"id\": \"util_encode\", \"axis\": \"Utility\", \"degree\": 4},\n    # Interface modules (axis 2)\n    {\"id\": \"api_rest\", \"axis\": \"Interface\", \"degree\": 7},\n    {\"id\": \"api_graphql\", \"axis\": \"Interface\", \"degree\": 5},\n    {\"id\": \"api_websocket\", \"axis\": \"Interface\", \"degree\": 4},\n    {\"id\": \"api_grpc\", \"axis\": \"Interface\", \"degree\": 3},\n]\n\n# Define edges between modules\nedges = [\n    (\"core_main\", \"core_data\"),\n    (\"core_main\", \"core_config\"),\n    (\"core_main\", \"core_logger\"),\n    (\"core_data\", \"core_cache\"),\n    (\"core_config\", \"core_logger\"),\n    (\"util_parser\", \"core_data\"),\n    (\"util_validator\", \"core_config\"),\n    (\"util_formatter\", \"core_logger\"),\n    (\"util_crypto\", \"core_main\"),\n    (\"util_compress\", \"core_cache\"),\n    (\"util_encode\", \"util_parser\"),\n    (\"util_validator\", \"util_formatter\"),\n    (\"api_rest\", \"core_main\"),\n    (\"api_rest\", \"util_parser\"),\n    (\"api_rest\", \"util_validator\"),\n    (\"api_graphql\", \"core_data\"),\n    (\"api_graphql\", \"util_parser\"),\n    (\"api_websocket\", \"core_main\"),\n    (\"api_websocket\", \"util_formatter\"),\n    (\"api_grpc\", \"core_config\"),\n    (\"api_grpc\", \"util_crypto\"),\n    (\"api_rest\", \"api_graphql\"),\n    (\"util_formatter\", \"util_encode\"),\n]\n\n# Convert nodes to DataFrame and assign positions\nnodes_df = pd.DataFrame(nodes)\n\n# Define axis angles (radial positions for 3 axes, evenly spaced)\naxis_angles = {\"Core\": 0, \"Utility\": 2 * np.pi / 3, \"Interface\": 4 * np.pi / 3}\naxis_to_color_idx = {\"Core\": 0, \"Utility\": 1, \"Interface\": 2}\n\n# Sort nodes by degree within each axis and assign radial position\nnodes_df = nodes_df.sort_values([\"axis\", \"degree\"], ascending=[True, False])\nnodes_df[\"radial_pos\"] = 0.0\n\nfor axis in axis_angles.keys():\n    mask = nodes_df[\"axis\"] == axis\n    n_nodes = mask.sum()\n    # Position nodes along radius (0.3 to 1.0 to leave center space)\n    nodes_df.loc[mask, \"radial_pos\"] = np.linspace(0.3, 0.95, n_nodes)\n\n# Calculate x, y coordinates for each node\nnodes_df[\"angle\"] = nodes_df[\"axis\"].map(axis_angles)\nnodes_df[\"x\"] = nodes_df[\"radial_pos\"] * np.cos(nodes_df[\"angle\"])\nnodes_df[\"y\"] = nodes_df[\"radial_pos\"] * np.sin(nodes_df[\"angle\"])\n\n# Create node position lookup\nnode_positions = nodes_df.set_index(\"id\")[[\"x\", \"y\"]].to_dict(\"index\")\n\n# Create edge data with Bezier curve approximation (quadratic)\nedge_data = []\nfor source, target in edges:\n    if source not in node_positions or target not in node_positions:\n        continue\n    src_pos = node_positions[source]\n    tgt_pos = node_positions[target]\n\n    # Get axis info\n    src_axis = nodes_df.loc[nodes_df[\"id\"] == source, \"axis\"].values[0]\n    tgt_axis = nodes_df.loc[nodes_df[\"id\"] == target, \"axis\"].values[0]\n\n    # Create curved path using quadratic Bezier through center offset\n    # Control point closer to center for nice curves\n    ctrl_x = (src_pos[\"x\"] + tgt_pos[\"x\"]) * 0.15\n    ctrl_y = (src_pos[\"y\"] + tgt_pos[\"y\"]) * 0.15\n\n    # Generate points along the Bezier curve\n    t = np.linspace(0, 1, 20)\n    bx = (1 - t) ** 2 * src_pos[\"x\"] + 2 * (1 - t) * t * ctrl_x + t**2 * tgt_pos[\"x\"]\n    by = (1 - t) ** 2 * src_pos[\"y\"] + 2 * (1 - t) * t * ctrl_y + t**2 * tgt_pos[\"y\"]\n\n    # Determine edge type for coloring\n    if src_axis == tgt_axis:\n        edge_type = f\"Within {src_axis}\"\n    else:\n        edge_type = \"Between axes\"\n\n    for i in range(len(t)):\n        edge_data.append({\"x\": bx[i], \"y\": by[i], \"edge_id\": f\"{source}-{target}\", \"edge_type\": edge_type})\n\nedges_df = pd.DataFrame(edge_data)\n\n# Create axis lines data\naxis_lines = []\nfor axis, angle in axis_angles.items():\n    # Line from center to outer edge (start at 0.2 to extend past nodes)\n    r_vals = np.linspace(0.2, 1.0, 50)\n    x_vals = r_vals * np.cos(angle)\n    y_vals = r_vals * np.sin(angle)\n    for i in range(len(r_vals)):\n        axis_lines.append({\"x\": x_vals[i], \"y\": y_vals[i], \"axis\": axis})\naxis_lines_df = pd.DataFrame(axis_lines)\n\n# Create axis labels data - position labels further from center to avoid clipping\nlabel_positions = []\nfor axis, angle in axis_angles.items():\n    # Position labels at outer edge, with adjustments for visibility\n    r = 1.05\n    x = r * np.cos(angle)\n    y = r * np.sin(angle)\n    label_positions.append({\"x\": x, \"y\": y, \"label\": axis, \"axis\": axis})\nlabels_df = pd.DataFrame(label_positions)\n\n# Map edges to colors using Okabe-Ito palette\nedge_color_map = {}\nfor axis in axis_angles.keys():\n    edge_color_map[f\"Within {axis}\"] = IMPRINT[axis_to_color_idx[axis]]\nedge_color_map[\"Between axes\"] = INK_SOFT\n\n# Build the plot\nplot = (\n    ggplot()\n    # Axis lines (theme-adaptive gray)\n    + geom_path(aes(x=\"x\", y=\"y\", group=\"axis\"), data=axis_lines_df, color=INK_SOFT, size=1.5, alpha=0.4)\n    # Edges with curves (reduced alpha for better center visibility)\n    + geom_path(aes(x=\"x\", y=\"y\", group=\"edge_id\", color=\"edge_type\"), data=edges_df, size=1.0, alpha=0.5)\n    # Nodes\n    + geom_point(\n        aes(x=\"x\", y=\"y\", fill=\"axis\", size=\"degree\"), data=nodes_df, color=INK_SOFT, stroke=1.0, shape=21, alpha=0.9\n    )\n    # Axis labels (separate layer, not in legend)\n    + geom_text(\n        aes(x=\"x\", y=\"y\", label=\"label\"), data=labels_df, size=14, fontface=\"bold\", color=INK, show_legend=False\n    )\n    # Color scales using Okabe-Ito palette\n    + scale_fill_manual(values=IMPRINT)\n    + scale_color_manual(values=edge_color_map)\n    + scale_size(range=[5, 12])\n    # Styling - expand limits to show axis labels\n    + coord_fixed(ratio=1, xlim=(-1.3, 1.3), ylim=(-1.3, 1.3))\n    + labs(title=\"hive-basic · letsplot · anyplot.ai\", fill=\"Module Type\", size=\"Connections\", color=\"Edge Type\")\n    + theme_void()\n    + theme(\n        plot_title=element_text(size=24, hjust=0.5, face=\"bold\", color=INK),\n        legend_title=element_text(size=16, color=INK),\n        legend_text=element_text(size=14, color=INK_SOFT),\n        legend_position=\"right\",\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        plot_margin=[40, 40, 40, 40],\n    )\n    + ggsize(1600, 900)\n)\n\n# Save as PNG (4800 × 2700 px with scale factor)\nggsave(plot, f\"plot-{THEME}.png\", scale=3)\n\n# Save as HTML for interactive version\nggsave(plot, f\"plot-{THEME}.html\")\n"}