{"spec_id":"tree-decision","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\ntree-decision: Decision Tree Visualization with Probabilities\nLibrary: plotnine 0.15.4 | Python 3.13.13\nQuality: 86/100 | Updated: 2026-06-02\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove the script's own directory from sys.path so 'plotnine' resolves to the\n# installed library and not this file (which shares the library's name).\nsys.path = [p for p in sys.path if not p.endswith(\"implementations/python\")]\n\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    annotate,\n    coord_fixed,\n    element_rect,\n    element_text,\n    geom_point,\n    geom_segment,\n    geom_text,\n    ggplot,\n    guide_legend,\n    guides,\n    labs,\n    scale_color_manual,\n    scale_shape_manual,\n    theme,\n    theme_void,\n    xlim,\n    ylim,\n)\n\n\n# Theme tokens (Imprint palette — 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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — canonical order, first series always #009E73\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Node type colors: Decision→green(1), Chance→lavender(2), Terminal→blue(3)\nNODE_COLORS = {\n    \"Decision\": IMPRINT_PALETTE[0],  # #009E73 brand green\n    \"Chance\": IMPRINT_PALETTE[1],  # #C475FD lavender\n    \"Terminal\": IMPRINT_PALETTE[2],  # #4467A3 blue\n}\n# Matte red (position 5) — semantic anchor for rejected/pruned branches\nPRUNE_COLOR = IMPRINT_PALETTE[4]\n\n# Data — Two-stage product launch decision tree\n# EMV rollback:\n#   C2 = 0.5×200 + 0.5×(-100) = $50K\n#   D2 = max(Pivot=$50K, Cut=-$50K) = $50K  → prune Cut Losses\n#   C1 = 0.6×500 + 0.4×50 = $320K\n#   C3 = 0.7×250 + 0.3×30 = $184K\n#   D1 = max(Launch=$320K, License=$184K) = $320K  → prune License IP\n\nnodes = pd.DataFrame(\n    {\n        \"x\": [0, 3, 3, 6.5, 6.5, 6.5, 6.5, 9.5, 9.5, 12.5, 12.5],\n        \"y\": [5, 8.2, 1.8, 9.8, 6.0, 3.3, 0.3, 7.5, 4.3, 8.6, 6.2],\n        \"node_type\": [\n            \"Decision\",\n            \"Chance\",\n            \"Chance\",\n            \"Terminal\",\n            \"Decision\",\n            \"Terminal\",\n            \"Terminal\",\n            \"Chance\",\n            \"Terminal\",\n            \"Terminal\",\n            \"Terminal\",\n        ],\n        \"value\": [\n            \"EMV: $320K\",\n            \"EMV: $320K\",\n            \"EMV: $184K\",\n            \"$500K\",\n            \"EMV: $50K\",\n            \"$250K\",\n            \"$30K\",\n            \"EMV: $50K\",\n            \"-$50K\",\n            \"$200K\",\n            \"-$100K\",\n        ],\n    }\n)\n\nemv_nodes = nodes[nodes[\"node_type\"] != \"Terminal\"].copy()\nemv_nodes[\"lx\"] = emv_nodes[\"x\"]\nemv_nodes[\"ly\"] = emv_nodes[\"y\"] - 1.3\n\nterminal_nodes = nodes[nodes[\"node_type\"] == \"Terminal\"].copy()\nterminal_nodes[\"lx\"] = terminal_nodes[\"x\"] + 0.7\nterminal_nodes[\"ly\"] = terminal_nodes[\"y\"]\n\nedges = pd.DataFrame(\n    {\n        \"x\": [0, 0, 3, 3, 3, 3, 6.5, 6.5, 9.5, 9.5],\n        \"xend\": [3, 3, 6.5, 6.5, 6.5, 6.5, 9.5, 9.5, 12.5, 12.5],\n        \"y\": [5, 5, 8.2, 8.2, 1.8, 1.8, 6.0, 6.0, 7.5, 7.5],\n        \"yend\": [8.2, 1.8, 9.8, 6.0, 3.3, 0.3, 7.5, 4.3, 8.6, 6.2],\n        \"branch_label\": [\n            \"Launch Product\",\n            \"License IP\",\n            \"High Demand\\n(p=0.60)\",\n            \"Low Demand\\n(p=0.40)\",\n            \"Accepted\\n(p=0.70)\",\n            \"Rejected\\n(p=0.30)\",\n            \"Pivot Strategy\",\n            \"Cut Losses\",\n            \"Recovery\\n(p=0.50)\",\n            \"No Recovery\\n(p=0.50)\",\n        ],\n        \"pruned\": [False, True, False, False, True, True, False, True, False, False],\n    }\n)\n\n# Midpoint label positions with offsets above/below the branch\nedges[\"lx\"] = (edges[\"x\"] + edges[\"xend\"]) / 2\nedges[\"ly\"] = (edges[\"y\"] + edges[\"yend\"]) / 2\nfor i in edges.index:\n    dy = edges.loc[i, \"yend\"] - edges.loc[i, \"y\"]\n    edges.loc[i, \"ly\"] += 1.1 if dy > 0 else -1.1\n    if edges.loc[i, \"pruned\"]:\n        edges.loc[i, \"lx\"] -= 0.3\n\nactive = edges[~edges[\"pruned\"]].copy()\npruned_edges = edges[edges[\"pruned\"]].copy()\n\nprune_marks = pruned_edges.copy()\nprune_marks[\"mx\"] = (prune_marks[\"x\"] + prune_marks[\"xend\"]) / 2 - 0.3\nprune_marks[\"my\"] = (prune_marks[\"y\"] + prune_marks[\"yend\"]) / 2\nprune_marks[\"mark\"] = \"✕\"\n\n# Optimal decision path for glow highlighting\noptimal_path = pd.DataFrame(\n    {\n        \"x\": [0, 3, 3, 6.5, 9.5, 9.5],\n        \"xend\": [3, 6.5, 6.5, 9.5, 12.5, 12.5],\n        \"y\": [5, 8.2, 8.2, 6.0, 7.5, 7.5],\n        \"yend\": [8.2, 9.8, 6.0, 7.5, 8.6, 6.2],\n    }\n)\n\n# Title — 46 chars, under 67-char baseline → 12pt default is fine\ntitle = \"tree-decision · python · plotnine · anyplot.ai\"\n\nplot = (\n    ggplot()\n    # Optimal path glow — brand green tint behind the winning branches\n    + geom_segment(\n        aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"),\n        data=optimal_path,\n        size=6,\n        color=IMPRINT_PALETTE[0],\n        alpha=0.13,\n        lineend=\"round\",\n    )\n    # Active branches — solid, full opacity\n    + geom_segment(aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), data=active, size=1.6, color=INK, lineend=\"round\")\n    # Pruned branches — dashed, muted\n    + geom_segment(\n        aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"),\n        data=pruned_edges,\n        size=0.8,\n        color=INK_MUTED,\n        linetype=\"dashed\",\n        alpha=0.5,\n    )\n    # Prune marks — matte red (Imprint semantic anchor for bad/rejected)\n    + geom_text(aes(x=\"mx\", y=\"my\", label=\"mark\"), data=prune_marks, size=10, color=PRUNE_COLOR, fontweight=\"bold\")\n    # Active branch labels\n    + geom_text(aes(x=\"lx\", y=\"ly\", label=\"branch_label\"), data=active, size=7, color=INK_SOFT, ha=\"center\")\n    # Pruned branch labels — muted to de-emphasise\n    + geom_text(aes(x=\"lx\", y=\"ly\", label=\"branch_label\"), data=pruned_edges, size=7, color=INK_MUTED, ha=\"center\")\n    # Node outer ring — thin border for definition against both themes\n    + geom_point(aes(x=\"x\", y=\"y\", shape=\"node_type\"), data=nodes, size=13, color=INK, fill=INK, stroke=0.5)\n    # Node fill — Imprint palette by node type\n    + geom_point(aes(x=\"x\", y=\"y\", color=\"node_type\", shape=\"node_type\"), data=nodes, size=11)\n    # EMV labels at decision/chance nodes\n    + geom_text(aes(x=\"lx\", y=\"ly\", label=\"value\"), data=emv_nodes, size=8, color=INK, ha=\"center\", fontweight=\"bold\")\n    # Payoff labels at terminal nodes\n    + geom_text(\n        aes(x=\"lx\", y=\"ly\", label=\"value\"), data=terminal_nodes, size=8, color=INK_SOFT, ha=\"left\", fontweight=\"bold\"\n    )\n    # Optimal path annotation\n    + annotate(\n        \"text\", x=0.2, y=10.6, label=\"★ Optimal Path\", size=8, color=IMPRINT_PALETTE[0], fontweight=\"bold\", ha=\"left\"\n    )\n    + annotate(\"segment\", x=0.6, y=10.3, xend=1.0, yend=9.2, size=0.8, color=IMPRINT_PALETTE[0], alpha=0.7)\n    + scale_color_manual(values=NODE_COLORS, name=\"Node Type\")\n    + scale_shape_manual(values={\"Decision\": \"s\", \"Chance\": \"o\", \"Terminal\": \">\"}, name=\"Node Type\")\n    + guides(color=guide_legend(override_aes={\"size\": 8}))\n    + theme_void()\n    + theme(\n        figure_size=(8, 4.5),\n        plot_title=element_text(size=12, weight=\"bold\", ha=\"center\", color=INK),\n        plot_subtitle=element_text(size=9, ha=\"center\", color=INK_SOFT),\n        legend_position=(0.5, 0.03),\n        legend_direction=\"horizontal\",\n        legend_title=element_text(size=9, weight=\"bold\", color=INK),\n        legend_text=element_text(size=8, color=INK_SOFT),\n        legend_key_size=16,\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        plot_margin=0.02,\n    )\n    + labs(title=title, subtitle=\"Product Launch vs. License IP — EMV Rollback Analysis (Optimal: Launch → $320K)\")\n    + coord_fixed(ratio=0.65)\n    + xlim(-0.5, 14.5)\n    + ylim(-1.0, 11.5)\n)\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\")\n"}