{"spec_id":"tree-decision","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\ntree-decision: Decision Tree Visualization with Probabilities\nLibrary: matplotlib 3.10.9 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-06-02\n\"\"\"\n\nimport sys\n\n\n# Prevent matplotlib.py from shadowing the matplotlib package on sys.path\nsys.path = [p for p in sys.path if not p.endswith(\"/python\")]\n\nimport os\n\nimport matplotlib.patches as mpatches\nimport matplotlib.patheffects as pe\nimport matplotlib.pyplot as plt\nfrom matplotlib.patches import FancyArrowPatch, FancyBboxPatch\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — node type colors (semantic mapping)\nDECISION_COLOR = \"#4467A3\"  # blue (position 3) — decision / control\nCHANCE_COLOR = \"#BD8233\"  # ochre (position 4) — uncertainty / risk\nTERMINAL_COLOR = \"#009E73\"  # brand green (position 1) — outcome / payoff\nPRUNED_COLOR = INK_MUTED\nPRUNED_MARK = \"#AE3030\"  # Imprint matte red — rejection / loss semantic\n\n# Data — two-stage investment decision tree\n# (node_id, node_type, parent_id, branch_label, probability, payoff, emv, pruned)\n# EMV rollback: D2=max(1.2M,800K)=1.2M, C1=0.6×1.2M+0.4×100K=760K, D1=max(760K,0)=760K\nnodes = [\n    (\"D1\", \"decision\", None, None, None, None, 760000, False),\n    (\"C1\", \"chance\", \"D1\", \"Invest\", None, None, 760000, False),\n    (\"T1\", \"terminal\", \"D1\", \"Don't Invest\", None, 0, None, True),\n    (\"D2\", \"decision\", \"C1\", \"High Demand (0.6)\", 0.6, None, 1200000, False),\n    (\"T2\", \"terminal\", \"C1\", \"Low Demand (0.4)\", 0.4, 100000, None, False),\n    (\"T3\", \"terminal\", \"D2\", \"Expand\", None, 1200000, None, False),\n    (\"T4\", \"terminal\", \"D2\", \"Maintain\", None, 800000, None, True),\n]\n\n# Layout — coordinate space tuned to 18×10 ≈ 16:9, so content fills the canvas\n# without needing set_aspect(\"equal\"). Each axis unit ≈ equal physical inches.\npositions = {\n    \"D1\": (2.0, 5.0),\n    \"C1\": (6.0, 7.0),\n    \"T1\": (6.0, 2.8),\n    \"D2\": (10.5, 8.4),\n    \"T2\": (10.5, 5.0),\n    \"T3\": (15.2, 9.4),\n    \"T4\": (15.2, 7.4),\n}\n\n# Canvas — 3200×1800 px (landscape 16:9). No bbox_inches=\"tight\".\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Draw branches first (under nodes)\nfor node in nodes:\n    nid, ntype, parent_id, label, prob, payoff, emv, pruned = node\n    if parent_id is None:\n        continue\n    px, py = positions[parent_id]\n    cx, cy = positions[nid]\n\n    line_color = PRUNED_COLOR if pruned else INK_SOFT\n    lw = 1.4 if pruned else 2.0\n    ls = \"dashed\" if pruned else \"solid\"\n\n    arrow = FancyArrowPatch(\n        (px, py),\n        (cx, cy),\n        arrowstyle=\"-\",\n        color=line_color,\n        linestyle=ls,\n        linewidth=lw,\n        zorder=1,\n        connectionstyle=\"arc3,rad=0.0\",\n    )\n    ax.add_patch(arrow)\n\n    # Pruned × marker at midpoint\n    if pruned:\n        mx, my = (px + cx) / 2, (py + cy) / 2\n        ax.plot(mx, my, marker=\"x\", markersize=12, color=PRUNED_MARK, markeredgewidth=2.5, zorder=5)\n\n    # Branch label — 30% along the branch, offset toward open space\n    if label:\n        t = 0.30\n        lx = px + (cx - px) * t\n        ly = py + (cy - py) * t\n        off = 0.55 if cy >= py else -0.55\n        ax.text(\n            lx,\n            ly + off,\n            label,\n            fontsize=7,\n            ha=\"center\",\n            va=\"center\",\n            color=PRUNED_COLOR if pruned else INK_SOFT,\n            bbox={\n                \"boxstyle\": \"round,pad=0.2\",\n                \"facecolor\": ELEVATED_BG,\n                \"edgecolor\": INK_MUTED,\n                \"alpha\": 0.92,\n                \"linewidth\": 0.6,\n            },\n            zorder=6,\n        )\n\n# Draw nodes\nNODE_R = 0.58  # radius / half-size — large enough for two-line EMV text\nSHADOW = [pe.SimplePatchShadow(offset=(3, -3), shadow_rgbFace=\"black\", alpha=0.13), pe.Normal()]\n\nfor node in nodes:\n    nid, ntype, parent_id, label, prob, payoff, emv, pruned = node\n    x, y = positions[nid]\n\n    if ntype == \"decision\":\n        rect = FancyBboxPatch(\n            (x - NODE_R, y - NODE_R),\n            NODE_R * 2,\n            NODE_R * 2,\n            boxstyle=\"round,pad=0.03\",\n            facecolor=DECISION_COLOR,\n            edgecolor=PAGE_BG,\n            linewidth=1.8,\n            zorder=3,\n        )\n        rect.set_path_effects(SHADOW)\n        ax.add_patch(rect)\n        if emv is not None:\n            emv_str = f\"${emv / 1e6:.1f}M\" if emv >= 1e6 else f\"${emv / 1e3:.0f}K\"\n            ax.text(\n                x,\n                y + 0.08,\n                \"EMV\",\n                fontsize=7,\n                ha=\"center\",\n                va=\"bottom\",\n                color=\"white\",\n                fontweight=\"bold\",\n                zorder=4,\n                alpha=0.85,\n            )\n            ax.text(\n                x, y - 0.08, emv_str, fontsize=8.5, ha=\"center\", va=\"top\", color=\"white\", fontweight=\"bold\", zorder=4\n            )\n\n    elif ntype == \"chance\":\n        circle = plt.Circle((x, y), NODE_R, facecolor=CHANCE_COLOR, edgecolor=PAGE_BG, linewidth=1.8, zorder=3)\n        circle.set_path_effects(SHADOW)\n        ax.add_patch(circle)\n        if emv is not None:\n            emv_str = f\"${emv / 1e6:.1f}M\" if emv >= 1e6 else f\"${emv / 1e3:.0f}K\"\n            ax.text(\n                x,\n                y + 0.08,\n                \"EMV\",\n                fontsize=7,\n                ha=\"center\",\n                va=\"bottom\",\n                color=\"white\",\n                fontweight=\"bold\",\n                zorder=4,\n                alpha=0.85,\n            )\n            ax.text(\n                x, y - 0.08, emv_str, fontsize=8.5, ha=\"center\", va=\"top\", color=\"white\", fontweight=\"bold\", zorder=4\n            )\n\n    elif ntype == \"terminal\":\n        tri_color = TERMINAL_COLOR if not pruned else PRUNED_COLOR\n        r = NODE_R * 0.88\n        triangle = plt.Polygon(\n            [(x - r * 0.55, y - r), (x - r * 0.55, y + r), (x + r, y)],\n            facecolor=tri_color,\n            edgecolor=PAGE_BG,\n            linewidth=1.8,\n            zorder=3,\n        )\n        triangle.set_path_effects(SHADOW)\n        ax.add_patch(triangle)\n        if payoff is not None:\n            pay_str = f\"${payoff / 1e6:.1f}M\" if payoff >= 1e6 else f\"${payoff / 1e3:.0f}K\"\n            ax.text(\n                x + r + 0.25,\n                y,\n                pay_str,\n                fontsize=9,\n                ha=\"left\",\n                va=\"center\",\n                fontweight=\"bold\",\n                color=tri_color,\n                zorder=4,\n            )\n\n# Legend\nlegend_handles = [\n    mpatches.Patch(facecolor=DECISION_COLOR, edgecolor=PAGE_BG, label=\"Decision Node\"),\n    mpatches.Patch(facecolor=CHANCE_COLOR, edgecolor=PAGE_BG, label=\"Chance Node\"),\n    mpatches.Patch(facecolor=TERMINAL_COLOR, edgecolor=PAGE_BG, label=\"Terminal Node\"),\n    plt.Line2D(\n        [0],\n        [0],\n        color=PRUNED_MARK,\n        marker=\"x\",\n        linestyle=\"--\",\n        markeredgewidth=2.0,\n        markersize=9,\n        label=\"Pruned Branch\",\n        linewidth=1.2,\n    ),\n]\nleg = ax.legend(\n    handles=legend_handles,\n    fontsize=7.5,\n    loc=\"lower right\",\n    framealpha=0.95,\n    edgecolor=INK_MUTED,\n    fancybox=True,\n    borderpad=0.8,\n)\nleg.get_frame().set_facecolor(ELEVATED_BG)\nplt.setp(leg.get_texts(), color=INK_SOFT)\n\n# Title — 48 chars; within 67-char baseline so default fontsize 12 is fine\ntitle = \"tree-decision · python · matplotlib · anyplot.ai\"\nax.set_title(title, fontsize=12, fontweight=\"medium\", pad=10, color=INK)\n\nax.set_xlim(0.5, 17.8)\nax.set_ylim(1.2, 10.5)\nax.set_aspect(\"equal\")\nax.axis(\"off\")\n\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}