{"spec_id":"hive-basic","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nhive-basic: Basic Hive Plot\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 81/100 | Updated: 2026-05-09\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\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\n# Okabe-Ito for the 3 axes (positions 1, 2, 3)\nAXIS_COLORS = {\"core\": \"#009E73\", \"utility\": \"#C475FD\", \"interface\": \"#4467A3\"}\n\n# Data: Software module dependency network\nnp.random.seed(42)\n\nnodes = [\n    # Core modules (axis 0)\n    {\"id\": \"engine\", \"category\": \"core\", \"degree\": 8},\n    {\"id\": \"runtime\", \"category\": \"core\", \"degree\": 6},\n    {\"id\": \"compiler\", \"category\": \"core\", \"degree\": 5},\n    {\"id\": \"memory\", \"category\": \"core\", \"degree\": 4},\n    {\"id\": \"scheduler\", \"category\": \"core\", \"degree\": 3},\n    # Utility modules (axis 1)\n    {\"id\": \"logger\", \"category\": \"utility\", \"degree\": 7},\n    {\"id\": \"parser\", \"category\": \"utility\", \"degree\": 6},\n    {\"id\": \"config\", \"category\": \"utility\", \"degree\": 5},\n    {\"id\": \"cache\", \"category\": \"utility\", \"degree\": 4},\n    {\"id\": \"validator\", \"category\": \"utility\", \"degree\": 3},\n    {\"id\": \"formatter\", \"category\": \"utility\", \"degree\": 2},\n    # Interface modules (axis 2)\n    {\"id\": \"api\", \"category\": \"interface\", \"degree\": 9},\n    {\"id\": \"web\", \"category\": \"interface\", \"degree\": 6},\n    {\"id\": \"cli\", \"category\": \"interface\", \"degree\": 5},\n    {\"id\": \"rest\", \"category\": \"interface\", \"degree\": 4},\n    {\"id\": \"grpc\", \"category\": \"interface\", \"degree\": 3},\n]\n\nedges = [\n    (\"api\", \"engine\"),\n    (\"api\", \"logger\"),\n    (\"api\", \"config\"),\n    (\"api\", \"cache\"),\n    (\"cli\", \"engine\"),\n    (\"cli\", \"parser\"),\n    (\"cli\", \"formatter\"),\n    (\"web\", \"runtime\"),\n    (\"web\", \"logger\"),\n    (\"web\", \"cache\"),\n    (\"rest\", \"engine\"),\n    (\"rest\", \"validator\"),\n    (\"grpc\", \"runtime\"),\n    (\"grpc\", \"config\"),\n    (\"engine\", \"memory\"),\n    (\"engine\", \"scheduler\"),\n    (\"engine\", \"logger\"),\n    (\"runtime\", \"memory\"),\n    (\"runtime\", \"cache\"),\n    (\"compiler\", \"parser\"),\n    (\"compiler\", \"memory\"),\n    (\"logger\", \"config\"),\n    (\"cache\", \"memory\"),\n    (\"parser\", \"validator\"),\n]\n\n# Axis layout: 3 axes evenly spaced\ncategories = [\"core\", \"utility\", \"interface\"]\naxis_angles = [90, 210, 330]  # degrees\ninner_radius = 0.25\nouter_radius = 0.90\n\nnode_lookup = {n[\"id\"]: n for n in nodes}\n\n# Group and sort by degree (descending → innermost = highest degree)\nnodes_by_category = {cat: [] for cat in categories}\nfor node in nodes:\n    nodes_by_category[node[\"category\"]].append(node)\nfor cat in categories:\n    nodes_by_category[cat].sort(key=lambda x: x[\"degree\"], reverse=True)\n\n# Compute polar positions (inline, no functions)\nnode_positions = {}\nfor node in nodes:\n    cat_idx = categories.index(node[\"category\"])\n    angle_rad = np.radians(axis_angles[cat_idx])\n    cat_nodes = nodes_by_category[node[\"category\"]]\n    rank = cat_nodes.index(node)\n    n_nodes = len(cat_nodes)\n    t = rank / (n_nodes - 1) if n_nodes > 1 else 0.5\n    radius = inner_radius + t * (outer_radius - inner_radius)\n    node_positions[node[\"id\"]] = (radius * np.cos(angle_rad), radius * np.sin(angle_rad))\n\n# Plot\nfig = go.Figure()\n\n# Axes (radial spokes)\nfor i, cat in enumerate(categories):\n    angle_rad = np.radians(axis_angles[i])\n    x0 = inner_radius * 0.7 * np.cos(angle_rad)\n    y0 = inner_radius * 0.7 * np.sin(angle_rad)\n    x1 = outer_radius * 1.05 * np.cos(angle_rad)\n    y1 = outer_radius * 1.05 * np.sin(angle_rad)\n    fig.add_trace(\n        go.Scatter(\n            x=[x0, x1],\n            y=[y0, y1],\n            mode=\"lines\",\n            line={\"color\": AXIS_COLORS[cat], \"width\": 5},\n            showlegend=False,\n            hoverinfo=\"skip\",\n        )\n    )\n\n# Edges (quadratic bezier curves, pulled toward center)\nfor source, target in edges:\n    if source not in node_positions or target not in node_positions:\n        continue\n    x0, y0 = node_positions[source]\n    x1, y1 = node_positions[target]\n    cx, cy = (x0 + x1) / 2 * 0.25, (y0 + y1) / 2 * 0.25\n    t_vals = np.linspace(0, 1, 40)\n    curve_x = (1 - t_vals) ** 2 * x0 + 2 * (1 - t_vals) * t_vals * cx + t_vals**2 * x1\n    curve_y = (1 - t_vals) ** 2 * y0 + 2 * (1 - t_vals) * t_vals * cy + t_vals**2 * y1\n    edge_color = AXIS_COLORS[node_lookup[source][\"category\"]]\n    fig.add_trace(\n        go.Scatter(\n            x=curve_x,\n            y=curve_y,\n            mode=\"lines\",\n            line={\"color\": edge_color, \"width\": 2.5},\n            opacity=0.35,\n            showlegend=False,\n            hoverinfo=\"skip\",\n        )\n    )\n\n# Nodes (per category for legend grouping)\nlabel_positions = {\"core\": \"top center\", \"utility\": \"bottom left\", \"interface\": \"bottom right\"}\nfor cat in categories:\n    cat_nodes = nodes_by_category[cat]\n    xs = [node_positions[n[\"id\"]][0] for n in cat_nodes]\n    ys = [node_positions[n[\"id\"]][1] for n in cat_nodes]\n    labels = [n[\"id\"] for n in cat_nodes]\n    degrees = [n[\"degree\"] for n in cat_nodes]\n    sizes = [d * 5 + 18 for d in degrees]\n    fig.add_trace(\n        go.Scatter(\n            x=xs,\n            y=ys,\n            mode=\"markers+text\",\n            marker={\"size\": sizes, \"color\": AXIS_COLORS[cat], \"line\": {\"color\": PAGE_BG, \"width\": 2.5}},\n            text=labels,\n            textposition=label_positions[cat],\n            textfont={\"size\": 16, \"color\": INK_SOFT},\n            name=cat.capitalize(),\n            hovertemplate=\"<b>%{text}</b><br>Degree: %{customdata}<extra></extra>\",\n            customdata=degrees,\n        )\n    )\n\n# Axis category labels — placed well beyond outermost nodes to avoid overlap\nfor i, cat in enumerate(categories):\n    angle_rad = np.radians(axis_angles[i])\n    label_r = outer_radius * 1.6\n    fig.add_annotation(\n        x=label_r * np.cos(angle_rad),\n        y=label_r * np.sin(angle_rad),\n        text=f\"<b>{cat.upper()}</b><br><span style='font-size:13px'>sorted by degree</span>\",\n        showarrow=False,\n        font={\"size\": 20, \"color\": AXIS_COLORS[cat]},\n    )\n\n# Style\nfig.update_layout(\n    title={\n        \"text\": (\n            \"hive-basic · plotly · anyplot.ai\"\n            \"<br><sup>Software Dependency Network — nodes by module type, radius by degree</sup>\"\n        ),\n        \"font\": {\"size\": 28, \"color\": INK},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n    },\n    showlegend=True,\n    legend={\n        \"title\": {\"text\": \"Module Type\", \"font\": {\"size\": 18, \"color\": INK}},\n        \"font\": {\"size\": 16, \"color\": INK_SOFT},\n        \"x\": 0.01,\n        \"y\": 0.99,\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n    },\n    xaxis={\"showgrid\": False, \"zeroline\": False, \"showticklabels\": False, \"range\": [-2.0, 2.0]},\n    yaxis={\n        \"showgrid\": False,\n        \"zeroline\": False,\n        \"showticklabels\": False,\n        \"range\": [-1.8, 1.8],\n        \"scaleanchor\": \"x\",\n        \"scaleratio\": 1,\n    },\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font={\"color\": INK},\n    margin={\"l\": 60, \"r\": 60, \"t\": 110, \"b\": 60},\n)\n\n# Save\nfig.write_image(f\"plot-{THEME}.png\", width=1600, height=900, scale=3)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}