{"spec_id":"network-directed","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nnetwork-directed: Directed Network Graph\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-05-14\n\"\"\"\n\nimport os\nimport sys\nfrom pathlib import Path\n\n\n# Avoid name collision with script file named altair.py\nscript_dir = str(Path(__file__).parent)\nif script_dir in sys.path:\n    sys.path.remove(script_dir)\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\"\n\nnp.random.seed(42)\n\n# Data: Software package dependency graph\nnodes = [\n    {\"id\": \"app\", \"label\": \"App\", \"group\": \"main\"},\n    {\"id\": \"api\", \"label\": \"API\", \"group\": \"core\"},\n    {\"id\": \"auth\", \"label\": \"Auth\", \"group\": \"core\"},\n    {\"id\": \"database\", \"label\": \"Database\", \"group\": \"core\"},\n    {\"id\": \"cache\", \"label\": \"Cache\", \"group\": \"service\"},\n    {\"id\": \"logger\", \"label\": \"Logger\", \"group\": \"util\"},\n    {\"id\": \"config\", \"label\": \"Config\", \"group\": \"util\"},\n    {\"id\": \"utils\", \"label\": \"Utils\", \"group\": \"util\"},\n    {\"id\": \"models\", \"label\": \"Models\", \"group\": \"data\"},\n    {\"id\": \"schemas\", \"label\": \"Schemas\", \"group\": \"data\"},\n    {\"id\": \"router\", \"label\": \"Router\", \"group\": \"core\"},\n    {\"id\": \"middleware\", \"label\": \"Middleware\", \"group\": \"core\"},\n]\n\n# Directed edges: (source, target) - arrows point from source to target\nedges = [\n    (\"app\", \"api\"),\n    (\"app\", \"auth\"),\n    (\"app\", \"router\"),\n    (\"api\", \"database\"),\n    (\"api\", \"cache\"),\n    (\"api\", \"models\"),\n    (\"auth\", \"database\"),\n    (\"auth\", \"cache\"),\n    (\"auth\", \"logger\"),\n    (\"database\", \"config\"),\n    (\"database\", \"logger\"),\n    (\"cache\", \"config\"),\n    (\"cache\", \"logger\"),\n    (\"router\", \"middleware\"),\n    (\"router\", \"api\"),\n    (\"middleware\", \"auth\"),\n    (\"middleware\", \"logger\"),\n    (\"models\", \"schemas\"),\n    (\"models\", \"utils\"),\n    (\"schemas\", \"utils\"),\n    (\"logger\", \"config\"),\n    (\"utils\", \"config\"),\n    (\"api\", \"auth\"),\n    (\"cache\", \"database\"),\n]\n\n# Okabe-Ito palette for groups\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\"]\ngroup_colors = {\n    \"main\": IMPRINT[0],  # Brand green\n    \"core\": IMPRINT[1],  # Vermillion\n    \"service\": IMPRINT[2],  # Blue\n    \"util\": IMPRINT[3],  # Reddish purple\n    \"data\": IMPRINT[4],  # Orange\n}\n\n# Node positions using hierarchical layout based on dependency depth\ndepths = {\"app\": 0}\nfor _ in range(len(nodes)):\n    for source, target in edges:\n        if source in depths:\n            current_depth = depths.get(target, -1)\n            depths[target] = max(current_depth, depths[source] + 1)\n\nfor node in nodes:\n    if node[\"id\"] not in depths:\n        depths[node[\"id\"]] = 0\n\ndepth_groups = {}\nfor node_id, depth in depths.items():\n    if depth not in depth_groups:\n        depth_groups[depth] = []\n    depth_groups[depth].append(node_id)\n\npositions = {}\nmax_depth = max(depths.values()) if depths else 0\nfor depth, node_ids in depth_groups.items():\n    n_nodes = len(node_ids)\n    for i, node_id in enumerate(node_ids):\n        x = depth / max(max_depth, 1)\n        y = (i + 0.5) / n_nodes\n        positions[node_id] = (x, y)\n\n# Create node DataFrame\nnode_df = pd.DataFrame(\n    [\n        {\n            \"id\": n[\"id\"],\n            \"label\": n[\"label\"],\n            \"group\": n[\"group\"],\n            \"x\": positions[n[\"id\"]][0],\n            \"y\": positions[n[\"id\"]][1],\n        }\n        for n in nodes\n    ]\n)\n\n# Identify bidirectional edge pairs\nedge_set = set(edges)\nbidirectional_pairs = set()\nfor source, target in edges:\n    if (target, source) in edge_set:\n        bidirectional_pairs.add(tuple(sorted([source, target])))\n\n# Create edge DataFrame with arrow coordinates\nedge_data = []\ncurved_edge_data = []\nfor source, target in edges:\n    sx, sy = positions[source]\n    tx, ty = positions[target]\n\n    is_bidirectional = tuple(sorted([source, target])) in bidirectional_pairs\n\n    dx, dy = tx - sx, ty - sy\n    length = np.sqrt(dx**2 + dy**2)\n    if length > 0:\n        offset = 0.03\n        sx_adj = sx + dx / length * offset\n        sy_adj = sy + dy / length * offset\n        tx_adj = tx - dx / length * offset\n        ty_adj = ty - dy / length * offset\n    else:\n        sx_adj, sy_adj = sx, sy\n        tx_adj, ty_adj = tx, ty\n\n    if is_bidirectional:\n        perp_x, perp_y = -dy / length * 0.05, dx / length * 0.05\n        mid_x, mid_y = (sx + tx) / 2 + perp_x, (sy + ty) / 2 + perp_y\n\n        for t in np.linspace(0, 1, 10):\n            t_next = min(t + 0.1, 1)\n            bx1 = (1 - t) ** 2 * sx_adj + 2 * (1 - t) * t * mid_x + t**2 * tx_adj\n            by1 = (1 - t) ** 2 * sy_adj + 2 * (1 - t) * t * mid_y + t**2 * ty_adj\n            bx2 = (1 - t_next) ** 2 * sx_adj + 2 * (1 - t_next) * t_next * mid_x + t_next**2 * tx_adj\n            by2 = (1 - t_next) ** 2 * sy_adj + 2 * (1 - t_next) * t_next * mid_y + t_next**2 * ty_adj\n            curved_edge_data.append({\"x\": bx1, \"y\": by1, \"x2\": bx2, \"y2\": by2, \"edge_id\": f\"{source}-{target}\"})\n    else:\n        edge_data.append({\"source\": source, \"target\": target, \"x\": sx_adj, \"y\": sy_adj, \"x2\": tx_adj, \"y2\": ty_adj})\n\nedge_df = pd.DataFrame(edge_data)\ncurved_edge_df = pd.DataFrame(curved_edge_data) if curved_edge_data else pd.DataFrame(columns=[\"x\", \"y\", \"x2\", \"y2\"])\n\n# Create arrow head data\narrow_data = []\nfor source, target in edges:\n    sx, sy = positions[source]\n    tx, ty = positions[target]\n\n    dx, dy = tx - sx, ty - sy\n    length = np.sqrt(dx**2 + dy**2)\n    if length > 0:\n        is_bidirectional = tuple(sorted([source, target])) in bidirectional_pairs\n\n        if is_bidirectional:\n            perp_x, perp_y = -dy / length * 0.05, dx / length * 0.05\n            mid_x, mid_y = (sx + tx) / 2 + perp_x, (sy + ty) / 2 + perp_y\n\n            t = 0.95\n            offset = 0.03\n            sx_adj = sx + dx / length * offset\n            sy_adj = sy + dy / length * offset\n            tx_adj = tx - dx / length * offset\n            ty_adj = ty - dy / length * offset\n\n            ax = (1 - t) ** 2 * sx_adj + 2 * (1 - t) * t * mid_x + t**2 * tx_adj\n            ay = (1 - t) ** 2 * sy_adj + 2 * (1 - t) * t * mid_y + t**2 * ty_adj\n\n            t_prev = 0.9\n            ax_prev = (1 - t_prev) ** 2 * sx_adj + 2 * (1 - t_prev) * t_prev * mid_x + t_prev**2 * tx_adj\n            ay_prev = (1 - t_prev) ** 2 * sy_adj + 2 * (1 - t_prev) * t_prev * mid_y + t_prev**2 * ty_adj\n            angle = np.degrees(np.arctan2(ay - ay_prev, ax - ax_prev))\n        else:\n            offset = 0.04\n            ax = tx - dx / length * offset\n            ay = ty - dy / length * offset\n            angle = np.degrees(np.arctan2(dy, dx))\n\n        arrow_data.append({\"x\": ax, \"y\": ay, \"angle\": angle})\n\narrow_df = pd.DataFrame(arrow_data)\n\n# Add colors to node dataframe\nnode_df[\"color\"] = node_df[\"group\"].map(group_colors)\n\n# Create the visualization\nedges_chart = (\n    alt.Chart(edge_df)\n    .mark_rule(strokeWidth=2, opacity=0.6, color=INK_SOFT)\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=[-0.1, 1.1]), axis=None),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=[-0.05, 1.05]), axis=None),\n        x2=\"x2:Q\",\n        y2=\"y2:Q\",\n    )\n)\n\ncurved_edges_chart = (\n    alt.Chart(curved_edge_df)\n    .mark_rule(strokeWidth=2, opacity=0.6, color=INK_SOFT)\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=[-0.1, 1.1]), axis=None),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=[-0.05, 1.05]), axis=None),\n        x2=\"x2:Q\",\n        y2=\"y2:Q\",\n    )\n)\n\n# Arrow heads as triangular points\narrows_chart = (\n    alt.Chart(arrow_df)\n    .mark_point(shape=\"triangle\", size=150, filled=True, color=INK_SOFT, opacity=0.8)\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=[-0.1, 1.1])),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=[-0.05, 1.05])),\n        angle=alt.Angle(\"angle:Q\"),\n    )\n)\n\n# Nodes as circles\nnodes_chart = (\n    alt.Chart(node_df)\n    .mark_circle(size=800, stroke=PAGE_BG, strokeWidth=2)\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=[-0.1, 1.1])),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=[-0.05, 1.05])),\n        color=alt.Color(\n            \"group:N\",\n            scale=alt.Scale(domain=list(group_colors.keys()), range=list(group_colors.values())),\n            legend=alt.Legend(title=\"Module Type\", titleFontSize=18, labelFontSize=16, orient=\"right\"),\n        ),\n        tooltip=[\"label:N\", \"group:N\"],\n    )\n)\n\n# Node labels\nlabels_chart = (\n    alt.Chart(node_df)\n    .mark_text(fontSize=18, fontWeight=\"bold\", dy=-28, color=INK)\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=[-0.1, 1.1])),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=[-0.05, 1.05])),\n        text=\"label:N\",\n    )\n)\n\n# Combine all layers\nchart = (\n    (edges_chart + curved_edges_chart + arrows_chart + nodes_chart + labels_chart)\n    .properties(\n        width=1600,\n        height=900,\n        background=PAGE_BG,\n        title=alt.Title(text=\"network-directed · altair · anyplot.ai\", fontSize=28, anchor=\"middle\", color=INK),\n    )\n    .configure_view(fill=PAGE_BG, stroke=INK_SOFT)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n)\n\n# Save as PNG and HTML\nscript_dir = Path(__file__).parent\noutput_png = script_dir / f\"plot-{THEME}.png\"\noutput_html = script_dir / f\"plot-{THEME}.html\"\nchart.save(str(output_png), scale_factor=3.0)\nchart.save(str(output_html))\n"}