{"spec_id":"tree-decision","library":"makie","language":"julia","code":"# anyplot.ai\n# tree-decision: Decision Tree Visualization with Probabilities\n# Library: makie 0.22.10 | Julia 1.11.9\n# Quality: 85/100 | Created: 2026-06-02\n\nusing CairoMakie\nusing Colors\nusing Random\n\nRandom.seed!(42)\n\n# Theme tokens\nconst THEME       = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG     = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst ELEVATED_BG = THEME == \"light\" ? colorant\"#FFFDF6\" : colorant\"#242420\"\nconst INK         = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_SOFT    = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\nconst INK_MUTED   = THEME == \"light\" ? colorant\"#6B6A63\" : colorant\"#A8A79F\"\n\nconst IMPRINT_PALETTE = [\n    colorant\"#009E73\",\n    colorant\"#C475FD\",\n    colorant\"#4467A3\",\n    colorant\"#BD8233\",\n    colorant\"#AE3030\",\n    colorant\"#2ABCCD\",\n    colorant\"#954477\",\n    colorant\"#99B314\",\n]\n\nconst COL_DECISION = IMPRINT_PALETTE[1]  # brand green — decision nodes\nconst COL_CHANCE   = IMPRINT_PALETTE[3]  # blue         — chance nodes\nconst COL_TERMINAL = IMPRINT_PALETTE[4]  # ochre        — terminal nodes\n\n# Data: software company product-launch decision tree\n# Stage 1: Launch (D1) vs Abandon (T1)\n# Stage 2 (after strong market): Expand (D2→C2) vs Maintain (T4)\n# Rollback:\n#   EMV(C2)  = 0.65×1800 + 0.35×400  = 1310\n#   EMV(D2)  = max(1310, 750) = 1310  → Expand wins, Maintain pruned\n#   EMV(C1)  = 0.55×1310 + 0.45×(−200) = 631\n#   EMV(D1)  = max(631, 0) = 631      → Launch wins, Abandon pruned\n\nconst node_ids    = [\"D1\",       \"T1\",       \"C1\",     \"T5\",      \"D2\",       \"T4\",       \"C2\",          \"T2\",      \"T3\"]\nconst node_types  = [:decision,  :terminal,  :chance,  :terminal, :decision,  :terminal,  :chance,       :terminal, :terminal]\nconst node_xs     = [0.0,        3.0,        3.0,      6.5,       6.5,        10.0,       10.0,          13.5,      13.5]\nconst node_ys     = [1.5,       -1.5,        4.5,      2.5,       6.0,         4.0,        8.0,           9.0,       7.0]\nconst node_emvs   = [631.0,      0.0,      631.0,    -200.0,   1310.0,       750.0,     1310.0,        1800.0,    400.0]\nconst node_pruned = [false,      true,     false,     false,    false,        true,      false,         false,     false]\nconst node_names  = [\"Launch?\",  \"Abandon\", \"Market\", \"Weak\",   \"Scale Up?\", \"Maintain\", \"Competition\", \"Low Comp\",\"High Comp\"]\n\nconst edge_froms  = [\"D1\",     \"D1\",              \"C1\",             \"C1\",               \"D2\",       \"D2\",    \"C2\",                \"C2\"]\nconst edge_tos    = [\"T1\",     \"C1\",              \"T5\",             \"D2\",               \"T4\",       \"C2\",    \"T2\",                \"T3\"]\nconst edge_labels = [\"Abandon\",\"Launch Product\",  \"Weak (p=0.45)\",  \"Strong (p=0.55)\", \"Maintain\", \"Expand\",\"Low Comp (p=0.65)\", \"High Comp (p=0.35)\"]\nconst edge_pruned = [true,     false,             false,            false,              true,       false,   false,               false]\n\nconst pos_lookup = Dict{String,Tuple{Float64,Float64}}(\n    node_ids[i] => (node_xs[i], node_ys[i]) for i in eachindex(node_ids)\n)\n\n# Title — scale fontsize linearly when title exceeds 67-char baseline\ntitle_str = \"Product Launch Decision · tree-decision · julia · makie · anyplot.ai\"\nn_chars   = length(title_str)\ntitle_sz  = max(13, round(Int, 20 * min(1.0, 67.0 / n_chars)))\n\n# Figure — landscape 3200×1800 px (size 1600×900 × px_per_unit 2)\nfig = Figure(\n    size            = (1600, 900),\n    fontsize        = 11,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    backgroundcolor    = PAGE_BG,\n    title              = title_str,\n    titlesize          = title_sz,\n    titlecolor         = INK,\n    topspinevisible    = false,\n    rightspinevisible  = false,\n    leftspinevisible   = false,\n    bottomspinevisible = false,\n    xgridvisible       = false,\n    ygridvisible       = false,\n    xticksvisible      = false,\n    yticksvisible      = false,\n    xticklabelsvisible = false,\n    yticklabelsvisible = false,\n)\n\nxlims!(ax, -1.5, 15.5)\nylims!(ax, -3.0, 10.5)\n\n# Draw edges (rendered before nodes so markers sit on top)\nfor i in eachindex(edge_froms)\n    x1, y1 = pos_lookup[edge_froms[i]]\n    x2, y2 = pos_lookup[edge_tos[i]]\n    is_pr  = edge_pruned[i]\n    dx     = x2 - x1\n    dy     = y2 - y1\n    elen   = sqrt(dx^2 + dy^2)\n\n    ec  = is_pr ? RGBAf(INK_MUTED.r, INK_MUTED.g, INK_MUTED.b, 0.4f0) :\n                  RGBAf(INK_SOFT.r,  INK_SOFT.g,  INK_SOFT.b,  0.9f0)\n    lw  = is_pr ? 1.5 : 2.5\n    lst = is_pr ? :dash : :solid\n\n    lines!(ax, [x1, x2], [y1, y2]; color = ec, linewidth = lw, linestyle = lst)\n\n    # Double-slash pruning marks (two perpendicular strokes across the edge)\n    if is_pr\n        nx_ = (-dy / elen) * 0.32\n        ny_ = (dx  / elen) * 0.32\n        pc  = RGBAf(INK_MUTED.r, INK_MUTED.g, INK_MUTED.b, 0.8f0)\n        for frac in (0.41, 0.51)\n            ox = x1 + dx * frac\n            oy = y1 + dy * frac\n            lines!(ax, [ox - nx_, ox + nx_], [oy - ny_, oy + ny_];\n                   color = pc, linewidth = 2.2)\n        end\n    end\n\n    # Branch label offset perpendicular to edge (counter-clockwise normal)\n    # Pruned-edge labels use a larger offset to avoid collision with double-slash marks\n    mx     = (x1 + x2) / 2\n    my     = (y1 + y2) / 2\n    off    = is_pr ? 0.8 : 0.5\n    nx_off = (-dy / elen) * off\n    ny_off = (dx  / elen) * off\n    lc     = is_pr ? INK_MUTED : INK_SOFT\n    text!(ax, mx + nx_off, my + ny_off;\n          text = edge_labels[i], color = lc, fontsize = 10, align = (:center, :center))\nend\n\n# Draw nodes\nfor i in eachindex(node_ids)\n    x      = node_xs[i]\n    y      = node_ys[i]\n    ntype  = node_types[i]\n    emv    = node_emvs[i]\n    pruned = node_pruned[i]\n    name   = node_names[i]\n\n    nc = if pruned\n        RGBAf(INK_MUTED.r, INK_MUTED.g, INK_MUTED.b, 0.45f0)\n    elseif ntype == :decision\n        COL_DECISION\n    elseif ntype == :chance\n        COL_CHANCE\n    else\n        COL_TERMINAL\n    end\n\n    mk = ntype == :decision  ? :rect :\n         ntype == :chance    ? :circle :\n                               :rtriangle\n\n    scatter!(ax, [x], [y];\n             marker = mk, color = nc, markersize = 30,\n             strokewidth = 1.5, strokecolor = PAGE_BG)\n\n    lc = pruned ? INK_MUTED : INK\n\n    # Short node name above the marker\n    text!(ax, x, y + 0.62;\n          text = name, color = lc, fontsize = 10, align = (:center, :bottom))\n\n    # EMV (decision/chance) or payoff (terminal) below the marker\n    emv_abs = abs(Int(round(emv)))\n    emv_str = if ntype == :terminal\n        emv >= 0 ? \"\\$$(emv_abs)K\" : \"-\\$$(emv_abs)K\"\n    else\n        emv >= 0 ? \"EMV=\\$$(emv_abs)K\" : \"EMV=-\\$$(emv_abs)K\"\n    end\n    text!(ax, x, y - 0.62;\n          text = emv_str, color = lc, fontsize = 10, align = (:center, :top))\nend\n\n# Legend in bottom strip\nleg_elems = [\n    MarkerElement(marker = :rect,      color = COL_DECISION, strokewidth = 0, markersize = 14),\n    MarkerElement(marker = :circle,    color = COL_CHANCE,   strokewidth = 0, markersize = 14),\n    MarkerElement(marker = :rtriangle, color = COL_TERMINAL, strokewidth = 0, markersize = 14),\n    LineElement(color = RGBAf(INK_MUTED.r, INK_MUTED.g, INK_MUTED.b, 0.7f0), linewidth = 1.5, linestyle = :dash),\n]\nleg_labels = [\"Decision node\", \"Chance node\", \"Terminal node\", \"Pruned branch\"]\n\nLegend(\n    fig[2, 1],\n    leg_elems,\n    leg_labels;\n    orientation  = :horizontal,\n    framevisible = false,\n    labelcolor   = INK_SOFT,\n    labelsize    = 10,\n    padding      = (10f0, 10f0, 4f0, 4f0),\n)\nrowsize!(fig.layout, 2, Fixed(28))\n\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}