{"spec_id":"hive-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nhive-basic: Basic Hive Plot\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 94/100 | Updated: 2026-05-07\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\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\"\nBRAND = \"#009E73\"  # Okabe-Ito position 1 — first categorical series\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\n\n# Data: Software module dependency network\nnodes_data = [\n    # Core modules (axis 0) — foundational components\n    {\"id\": \"core_main\", \"name\": \"Main\", \"axis\": \"Core\", \"importance\": 1.0},\n    {\"id\": \"core_db\", \"name\": \"Database\", \"axis\": \"Core\", \"importance\": 0.80},\n    {\"id\": \"core_config\", \"name\": \"Config\", \"axis\": \"Core\", \"importance\": 0.60},\n    {\"id\": \"core_logger\", \"name\": \"Logger\", \"axis\": \"Core\", \"importance\": 0.40},\n    {\"id\": \"core_cache\", \"name\": \"Cache\", \"axis\": \"Core\", \"importance\": 0.20},\n    # Utility modules (axis 1) — helper components\n    {\"id\": \"util_http\", \"name\": \"HTTP\", \"axis\": \"Utility\", \"importance\": 1.0},\n    {\"id\": \"util_file\", \"name\": \"FileIO\", \"axis\": \"Utility\", \"importance\": 0.75},\n    {\"id\": \"util_string\", \"name\": \"String\", \"axis\": \"Utility\", \"importance\": 0.50},\n    {\"id\": \"util_date\", \"name\": \"DateTime\", \"axis\": \"Utility\", \"importance\": 0.25},\n    # Interface modules (axis 2) — external-facing components\n    {\"id\": \"iface_api\", \"name\": \"REST API\", \"axis\": \"Interface\", \"importance\": 1.0},\n    {\"id\": \"iface_web\", \"name\": \"WebUI\", \"axis\": \"Interface\", \"importance\": 0.66},\n    {\"id\": \"iface_cli\", \"name\": \"CLI\", \"axis\": \"Interface\", \"importance\": 0.33},\n]\n\nedges_data = [\n    # Core to Utility connections\n    (\"core_main\", \"util_string\"),\n    (\"core_main\", \"util_file\"),\n    (\"core_config\", \"util_file\"),\n    (\"core_config\", \"util_string\"),\n    (\"core_logger\", \"util_date\"),\n    (\"core_logger\", \"util_file\"),\n    (\"core_cache\", \"util_date\"),\n    (\"core_db\", \"util_string\"),\n    # Core to Interface connections\n    (\"core_main\", \"iface_api\"),\n    (\"core_main\", \"iface_cli\"),\n    (\"core_config\", \"iface_api\"),\n    (\"core_db\", \"iface_api\"),\n    (\"core_logger\", \"iface_web\"),\n    # Utility to Interface connections\n    (\"util_http\", \"iface_api\"),\n    (\"util_http\", \"iface_web\"),\n    (\"util_string\", \"iface_cli\"),\n    (\"util_file\", \"iface_cli\"),\n    (\"util_date\", \"iface_web\"),\n]\n\n# Axis configuration: three axes at 120-degree separation using Okabe-Ito\naxis_config = {\n    \"Core\": {\"angle\": 30, \"color\": IMPRINT[0]},  # Bluish green\n    \"Utility\": {\"angle\": 150, \"color\": IMPRINT[1]},  # Vermillion\n    \"Interface\": {\"angle\": 270, \"color\": IMPRINT[2]},  # Blue\n}\n\n# Build node DataFrame with positions\nnodes_df = pd.DataFrame(nodes_data)\nmin_radius = 0.25\nmax_radius = 0.85\n\nnode_positions = {}\nnodes_df[\"x\"] = 0.0\nnodes_df[\"y\"] = 0.0\n\nfor idx, row in nodes_df.iterrows():\n    axis = row[\"axis\"]\n    angle_rad = np.radians(axis_config[axis][\"angle\"])\n    radius = min_radius + (max_radius - min_radius) * row[\"importance\"]\n    nodes_df.at[idx, \"x\"] = radius * np.cos(angle_rad)\n    nodes_df.at[idx, \"y\"] = radius * np.sin(angle_rad)\n    nodes_df.at[idx, \"color\"] = axis_config[axis][\"color\"]\n    node_positions[row[\"id\"]] = (nodes_df.at[idx, \"x\"], nodes_df.at[idx, \"y\"])\n\n# Build edges DataFrame with coordinates\nedges_list = []\nfor source, target in edges_data:\n    if source in node_positions and target in node_positions:\n        x1, y1 = node_positions[source]\n        x2, y2 = node_positions[target]\n        source_axis = nodes_df[nodes_df[\"id\"] == source][\"axis\"].values[0]\n        target_axis = nodes_df[nodes_df[\"id\"] == target][\"axis\"].values[0]\n        edges_list.append(\n            {\n                \"x\": x1,\n                \"y\": y1,\n                \"x2\": x2,\n                \"y2\": y2,\n                \"source\": source,\n                \"target\": target,\n                \"connection\": f\"{source_axis} → {target_axis}\",\n            }\n        )\n\nedges_df = pd.DataFrame(edges_list)\n\n# Axis lines (from center outward)\naxis_lines = []\nfor axis_name, config in axis_config.items():\n    angle_rad = np.radians(config[\"angle\"])\n    axis_lines.append(\n        {\"x\": 0, \"y\": 0, \"x2\": 0.95 * np.cos(angle_rad), \"y2\": 0.95 * np.sin(angle_rad), \"axis\": axis_name}\n    )\naxis_df = pd.DataFrame(axis_lines)\n\n# Axis labels positioned at the end of axes\naxis_labels = []\nfor axis_name, config in axis_config.items():\n    angle_rad = np.radians(config[\"angle\"])\n    label_dist = 1.12\n    axis_labels.append({\"x\": label_dist * np.cos(angle_rad), \"y\": label_dist * np.sin(angle_rad), \"label\": axis_name})\naxis_labels_df = pd.DataFrame(axis_labels)\n\n# Plot layers: edges, axes, nodes, labels\n\n# Edge lines with transparency\nedges_chart = (\n    alt.Chart(edges_df)\n    .mark_rule(strokeWidth=2, opacity=0.30)\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=[-1.2, 1.2]), axis=None),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=[-1.2, 1.2]), axis=None),\n        x2=\"x2:Q\",\n        y2=\"y2:Q\",\n        color=alt.value(INK_SOFT),\n        tooltip=[\"source:N\", \"target:N\", \"connection:N\"],\n    )\n)\n\n# Axis lines (radial axes)\naxis_chart = (\n    alt.Chart(axis_df)\n    .mark_rule(strokeWidth=4, opacity=0.7)\n    .encode(\n        x=\"x:Q\",\n        y=\"y:Q\",\n        x2=\"x2:Q\",\n        y2=\"y2:Q\",\n        color=alt.Color(\n            \"axis:N\", scale=alt.Scale(domain=[\"Core\", \"Utility\", \"Interface\"], range=IMPRINT), legend=None\n        ),\n    )\n)\n\n# Node circles\nnodes_chart = (\n    alt.Chart(nodes_df)\n    .mark_circle(size=600, opacity=0.95, stroke=PAGE_BG, strokeWidth=3)\n    .encode(\n        x=\"x:Q\",\n        y=\"y:Q\",\n        color=alt.Color(\n            \"axis:N\", scale=alt.Scale(domain=[\"Core\", \"Utility\", \"Interface\"], range=IMPRINT), legend=None\n        ),\n        tooltip=[\"name:N\", \"axis:N\", \"importance:Q\"],\n    )\n)\n\n# Node labels with proper positioning\nnode_labels_data = nodes_df.copy()\nlabel_offset = 0.12\n\nfor idx, row in node_labels_data.iterrows():\n    axis = row[\"axis\"]\n    if axis == \"Core\":\n        # Right side labels\n        node_labels_data.at[idx, \"label_x\"] = row[\"x\"] + label_offset\n        node_labels_data.at[idx, \"label_y\"] = row[\"y\"]\n    elif axis == \"Utility\":\n        # Left side labels\n        node_labels_data.at[idx, \"label_x\"] = row[\"x\"] - label_offset\n        node_labels_data.at[idx, \"label_y\"] = row[\"y\"]\n    else:  # Interface\n        # Bottom — alternate sides\n        node_labels_data.at[idx, \"label_x\"] = row[\"x\"] + (label_offset if row[\"importance\"] > 0.5 else -label_offset)\n        node_labels_data.at[idx, \"label_y\"] = row[\"y\"]\n\nnode_labels = (\n    alt.Chart(node_labels_data)\n    .mark_text(fontSize=18, fontWeight=\"bold\")\n    .encode(x=\"label_x:Q\", y=\"label_y:Q\", text=\"name:N\", color=alt.value(INK))\n)\n\n# Axis labels (Core, Utility, Interface)\naxis_label_chart = (\n    alt.Chart(axis_labels_df)\n    .mark_text(fontSize=36, fontWeight=\"bold\")\n    .encode(\n        x=\"x:Q\",\n        y=\"y:Q\",\n        text=\"label:N\",\n        color=alt.Color(\n            \"label:N\", scale=alt.Scale(domain=[\"Core\", \"Utility\", \"Interface\"], range=IMPRINT), legend=None\n        ),\n    )\n)\n\n# Combine all layers\nhive_chart = (\n    (edges_chart + axis_chart + nodes_chart + node_labels + axis_label_chart)\n    .properties(width=1600, height=1600, background=PAGE_BG)\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n    .configure_axis(grid=False)\n)\n\n# Add title\nchart = hive_chart.properties(\n    title=alt.Title(text=\"hive-basic · altair · anyplot.ai\", fontSize=28, anchor=\"middle\", color=INK)\n)\n\n# Save to both PNG and HTML with theme suffix\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}