{"spec_id":"tree-decision","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\ntree-decision: Decision Tree Visualization with Probabilities\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 87/100 | Updated: 2026-06-02\n\"\"\"\n\nimport os\n\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_rect,\n    element_text,\n    geom_label,\n    geom_point,\n    geom_polygon,\n    geom_rect,\n    geom_segment,\n    geom_text,\n    ggplot,\n    ggsize,\n    labs,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_void,\n)\nfrom lets_plot.export import ggsave\n\n\nLetsPlot.setup_html()\n\n# Theme-adaptive chrome\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 types in canonical order (positions 1, 2, 3)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nCOL_DECISION = IMPRINT[0]  # #009E73 brand green — always first series\nCOL_CHANCE = IMPRINT[3]  # #BD8233 ochre — warm tone fits probability/uncertainty\nCOL_TERMINAL = IMPRINT[2]  # #4467A3 blue — neutral outcome nodes\nPRUNE_RED = IMPRINT[4]  # #AE3030 semantic red for pruning (rejected branches)\n\n# Two-stage product launch decision tree\n# EMV rollback:\n#   C2: 0.6×$600K + 0.4×$200K = $440K\n#   D2: max($440K, $350K) = $440K → Maintain pruned\n#   C1: 0.3×$900K + 0.5×$440K + 0.2×(−$200K) = $450K\n#   D1: max($450K, $250K) = $450K → License Tech pruned\n\nnode_records = [\n    {\"id\": \"D1\", \"type\": \"decision\", \"x\": 0, \"y\": 6.0, \"value\": \"EMV $450K\"},\n    {\"id\": \"C1\", \"type\": \"chance\", \"x\": 5, \"y\": 9.0, \"value\": \"EMV $450K\"},\n    {\"id\": \"T6\", \"type\": \"terminal\", \"x\": 5, \"y\": 2.0, \"value\": \"$250K\"},\n    {\"id\": \"T1\", \"type\": \"terminal\", \"x\": 10, \"y\": 12.5, \"value\": \"$900K\"},\n    {\"id\": \"D2\", \"type\": \"decision\", \"x\": 10, \"y\": 7.5, \"value\": \"EMV $440K\"},\n    {\"id\": \"T2\", \"type\": \"terminal\", \"x\": 10, \"y\": 4.0, \"value\": \"-$200K\"},\n    {\"id\": \"C2\", \"type\": \"chance\", \"x\": 15, \"y\": 10.5, \"value\": \"EMV $440K\"},\n    {\"id\": \"T3\", \"type\": \"terminal\", \"x\": 15, \"y\": 3.5, \"value\": \"$350K\"},\n    {\"id\": \"T4\", \"type\": \"terminal\", \"x\": 20, \"y\": 12.5, \"value\": \"$600K\"},\n    {\"id\": \"T5\", \"type\": \"terminal\", \"x\": 20, \"y\": 8.0, \"value\": \"$200K\"},\n]\n\nbranch_records = [\n    {\"from_id\": \"D1\", \"to_id\": \"C1\", \"label\": \"Launch Product\", \"pruned\": False, \"is_prob\": False},\n    {\"from_id\": \"D1\", \"to_id\": \"T6\", \"label\": \"License Tech\", \"pruned\": True, \"is_prob\": False},\n    {\"from_id\": \"C1\", \"to_id\": \"T1\", \"label\": \"Strong (0.3)\", \"pruned\": False, \"is_prob\": True},\n    {\"from_id\": \"C1\", \"to_id\": \"D2\", \"label\": \"Moderate (0.5)\", \"pruned\": False, \"is_prob\": True},\n    {\"from_id\": \"C1\", \"to_id\": \"T2\", \"label\": \"Weak (0.2)\", \"pruned\": False, \"is_prob\": True},\n    {\"from_id\": \"D2\", \"to_id\": \"C2\", \"label\": \"Scale Up\", \"pruned\": False, \"is_prob\": False},\n    {\"from_id\": \"D2\", \"to_id\": \"T3\", \"label\": \"Maintain\", \"pruned\": True, \"is_prob\": False},\n    {\"from_id\": \"C2\", \"to_id\": \"T4\", \"label\": \"Success (0.6)\", \"pruned\": False, \"is_prob\": True},\n    {\"from_id\": \"C2\", \"to_id\": \"T5\", \"label\": \"Setback (0.4)\", \"pruned\": False, \"is_prob\": True},\n]\n\nnode_lookup = {r[\"id\"]: r for r in node_records}\n\n# Elbow connectors (horizontal → vertical → horizontal)\nactive_segs, pruned_segs = [], []\nfor b in branch_records:\n    f, t = node_lookup[b[\"from_id\"]], node_lookup[b[\"to_id\"]]\n    mx = (f[\"x\"] + t[\"x\"]) / 2\n    segs = [\n        {\"x\": f[\"x\"], \"y\": f[\"y\"], \"xend\": mx, \"yend\": f[\"y\"]},\n        {\"x\": mx, \"y\": f[\"y\"], \"xend\": mx, \"yend\": t[\"y\"]},\n        {\"x\": mx, \"y\": t[\"y\"], \"xend\": t[\"x\"], \"yend\": t[\"y\"]},\n    ]\n    (pruned_segs if b[\"pruned\"] else active_segs).extend(segs)\n\ndf_active = pd.DataFrame(active_segs)\ndf_pruned = pd.DataFrame(pruned_segs)\n\n# Branch labels — placed on vertical segment\n# Use 0.55 fraction near D2 to spread labels away from the congested node\nprob_labels, dec_labels = [], []\nfor b in branch_records:\n    f, t = node_lookup[b[\"from_id\"]], node_lookup[b[\"to_id\"]]\n    mx = (f[\"x\"] + t[\"x\"]) / 2\n    frac = 0.55 if (b[\"from_id\"] == \"D2\" or b[\"to_id\"] == \"D2\") else 0.35\n    ly = f[\"y\"] + (t[\"y\"] - f[\"y\"]) * frac\n    rec = {\"x\": mx + 0.5, \"y\": ly, \"label\": b[\"label\"]}\n    (prob_labels if b[\"is_prob\"] else dec_labels).append(rec)\n\ndf_prob = pd.DataFrame(prob_labels)\ndf_dec = pd.DataFrame(dec_labels)\n\n# Decision node rectangles (coordinate-space sizing)\nrect_half = 0.55\ndf_dec_rects = pd.DataFrame(\n    [\n        {\n            \"xmin\": r[\"x\"] - rect_half,\n            \"xmax\": r[\"x\"] + rect_half,\n            \"ymin\": r[\"y\"] - rect_half * 0.72,\n            \"ymax\": r[\"y\"] + rect_half * 0.72,\n        }\n        for r in node_records\n        if r[\"type\"] == \"decision\"\n    ]\n)\n\n# Chance nodes (circles via geom_point)\ndf_chance = pd.DataFrame([r for r in node_records if r[\"type\"] == \"chance\"])\n\n# Terminal node triangles (right-pointing)\ntri_w, tri_h = 0.55, 0.38\ntri_polys = []\nfor r in node_records:\n    if r[\"type\"] == \"terminal\":\n        gid = f\"tri_{r['id']}\"\n        tri_polys.extend(\n            [\n                {\"x\": r[\"x\"] - tri_w, \"y\": r[\"y\"] + tri_h, \"group\": gid},\n                {\"x\": r[\"x\"] - tri_w, \"y\": r[\"y\"] - tri_h, \"group\": gid},\n                {\"x\": r[\"x\"] + tri_w * 0.6, \"y\": r[\"y\"], \"group\": gid},\n            ]\n        )\ndf_triangles = pd.DataFrame(tri_polys)\n\n# Value labels: EMV below non-terminal nodes, payoffs right of terminals\nemv_recs, pay_recs = [], []\nfor r in node_records:\n    if r[\"type\"] == \"terminal\":\n        pay_recs.append({\"x\": r[\"x\"] + 1.0, \"y\": r[\"y\"], \"label\": r[\"value\"]})\n    else:\n        emv_recs.append({\"x\": r[\"x\"], \"y\": r[\"y\"] - 0.9, \"label\": r[\"value\"]})\ndf_emv = pd.DataFrame(emv_recs)\ndf_pay = pd.DataFrame(pay_recs)\n\n# Pruning X crosses (Imprint semantic red)\nprune_marks = []\nfor b in branch_records:\n    if b[\"pruned\"]:\n        f, t = node_lookup[b[\"from_id\"]], node_lookup[b[\"to_id\"]]\n        cx = f[\"x\"] + (t[\"x\"] - f[\"x\"]) * 0.4\n        cy = f[\"y\"] + (t[\"y\"] - f[\"y\"]) * 0.4\n        d = 0.3\n        prune_marks += [\n            {\"x\": cx - d, \"y\": cy - d, \"xend\": cx + d, \"yend\": cy + d},\n            {\"x\": cx - d, \"y\": cy + d, \"xend\": cx + d, \"yend\": cy - d},\n        ]\ndf_prune = pd.DataFrame(prune_marks)\n\n# Manual legend (bottom-right area)\nlx, lby = 16.5, 1.8\ndf_leg_labels = pd.DataFrame(\n    [\n        {\"x\": lx + 0.8, \"y\": lby + 1.3, \"label\": \"Decision Node\"},\n        {\"x\": lx + 0.8, \"y\": lby + 0.65, \"label\": \"Chance Node\"},\n        {\"x\": lx + 0.8, \"y\": lby, \"label\": \"Terminal Node\"},\n    ]\n)\ndf_leg_rect = pd.DataFrame([{\"xmin\": lx - 0.3, \"xmax\": lx + 0.3, \"ymin\": lby + 1.1, \"ymax\": lby + 1.5}])\ndf_leg_tri = pd.DataFrame(\n    [\n        {\"x\": lx - 0.28, \"y\": lby + 0.18, \"group\": \"lt\"},\n        {\"x\": lx - 0.28, \"y\": lby - 0.18, \"group\": \"lt\"},\n        {\"x\": lx + 0.22, \"y\": lby, \"group\": \"lt\"},\n    ]\n)\n\n# Subtle depth shading bands to distinguish tree stages\ndepth_bands = pd.DataFrame(\n    [{\"xmin\": -1.5, \"xmax\": 2.5, \"ymin\": 0.5, \"ymax\": 14.0}, {\"xmin\": 7.5, \"xmax\": 12.5, \"ymin\": 0.5, \"ymax\": 14.0}]\n)\n\n# ── Build plot ────────────────────────────────────────────────────────────────\np = (\n    ggplot()\n    # Depth shading bands (more visible than before)\n    + geom_rect(\n        aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\"),\n        data=depth_bands,\n        fill=INK,\n        color=\"transparent\",\n        size=0,\n        alpha=0.055,\n    )\n    # Active branches\n    + geom_segment(aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), data=df_active, size=0.7, color=INK)\n    # Pruned branches (dashed, muted)\n    + geom_segment(\n        aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), data=df_pruned, size=0.4, color=INK_MUTED, linetype=\"dashed\"\n    )\n    # Pruning X marks\n    + geom_segment(aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), data=df_prune, size=1.0, color=PRUNE_RED)\n    # Decision nodes as filled rectangles (Imprint brand green)\n    + geom_rect(\n        aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\"),\n        data=df_dec_rects,\n        fill=COL_DECISION,\n        color=INK_SOFT,\n        size=0.6,\n        alpha=0.92,\n    )\n    # Chance nodes as circles (Imprint ochre)\n    + geom_point(aes(x=\"x\", y=\"y\"), data=df_chance, shape=21, size=7, fill=COL_CHANCE, color=INK_SOFT, stroke=1.2)\n    # Terminal nodes as right-pointing triangles (Imprint blue)\n    + geom_polygon(aes(x=\"x\", y=\"y\", group=\"group\"), data=df_triangles, fill=COL_TERMINAL, color=INK_SOFT, size=0.6)\n    # Probability branch labels (italic, elevated background)\n    + geom_label(\n        aes(x=\"x\", y=\"y\", label=\"label\"),\n        data=df_prob,\n        size=4,\n        color=INK_SOFT,\n        fill=ELEVATED_BG,\n        alpha=0.92,\n        label_padding=0.28,\n        label_r=0.15,\n        label_size=0,\n        fontface=\"italic\",\n    )\n    # Decision branch labels (bold)\n    + geom_label(\n        aes(x=\"x\", y=\"y\", label=\"label\"),\n        data=df_dec,\n        size=4.5,\n        color=INK,\n        fill=ELEVATED_BG,\n        alpha=0.92,\n        label_padding=0.28,\n        label_r=0.15,\n        label_size=0.2,\n        fontface=\"bold\",\n    )\n    # EMV labels below non-terminal nodes\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=df_emv, size=4, color=INK, fontface=\"bold\")\n    # Payoff labels right of terminal nodes\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=df_pay, size=4, color=INK, fontface=\"bold\")\n    # Legend: decision rectangle\n    + geom_rect(\n        aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\"),\n        data=df_leg_rect,\n        fill=COL_DECISION,\n        color=INK_SOFT,\n        size=0.5,\n    )\n    # Legend: chance circle\n    + geom_point(\n        aes(x=\"x\", y=\"y\"),\n        data=pd.DataFrame([{\"x\": lx, \"y\": lby + 0.65}]),\n        shape=21,\n        size=4,\n        fill=COL_CHANCE,\n        color=INK_SOFT,\n        stroke=0.8,\n    )\n    # Legend: terminal triangle\n    + geom_polygon(aes(x=\"x\", y=\"y\", group=\"group\"), data=df_leg_tri, fill=COL_TERMINAL, color=INK_SOFT, size=0.4)\n    # Legend text labels\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=df_leg_labels, size=3.5, color=INK_SOFT, hjust=0)\n    + scale_x_continuous(limits=[-2, 23])\n    + scale_y_continuous(limits=[0.0, 14.5])\n    + labs(\n        title=\"tree-decision · python · letsplot · anyplot.ai\",\n        subtitle=\"Product Launch Strategy — Two-stage EMV rollback analysis\",\n    )\n    + theme_void()\n    + theme(\n        plot_title=element_text(size=16, hjust=0.5, face=\"bold\", color=INK),\n        plot_subtitle=element_text(size=11, hjust=0.5, color=INK_SOFT, face=\"italic\"),\n        plot_margin=[30, 20, 15, 15],\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    )\n    + ggsize(800, 450)\n)\n\nggsave(p, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(p, f\"plot-{THEME}.html\", path=\".\")\n"}