Decision Tree Visualization with Probabilities — Makie.jl

A tree-structured diagram for sequential decision analysis, displaying decision nodes (squares), chance nodes (circles), and terminal outcome nodes (triangles) connected by branching paths. Each chance branch is labeled with probabilities, terminal nodes show payoff values, and Expected Monetary Values (EMV) are calculated via rollback at each node. Rejected (pruned) branches are visually marked, making it easy to trace the optimal decision path through a multi-stage problem.

Decision Tree Visualization with Probabilities rendered with Makie.jl

Renders

Julia source (Makie.jl)

# anyplot.ai
# tree-decision: Decision Tree Visualization with Probabilities
# Library: makie 0.22.10 | Julia 1.11.9
# Quality: 85/100 | Created: 2026-06-02

using CairoMakie
using Colors
using Random

Random.seed!(42)

# Theme tokens
const THEME       = get(ENV, "ANYPLOT_THEME", "light")
const PAGE_BG     = THEME == "light" ? colorant"#FAF8F1" : colorant"#1A1A17"
const ELEVATED_BG = THEME == "light" ? colorant"#FFFDF6" : colorant"#242420"
const INK         = THEME == "light" ? colorant"#1A1A17" : colorant"#F0EFE8"
const INK_SOFT    = THEME == "light" ? colorant"#4A4A44" : colorant"#B8B7B0"
const INK_MUTED   = THEME == "light" ? colorant"#6B6A63" : colorant"#A8A79F"

const IMPRINT_PALETTE = [
    colorant"#009E73",
    colorant"#C475FD",
    colorant"#4467A3",
    colorant"#BD8233",
    colorant"#AE3030",
    colorant"#2ABCCD",
    colorant"#954477",
    colorant"#99B314",
]

const COL_DECISION = IMPRINT_PALETTE[1]  # brand green — decision nodes
const COL_CHANCE   = IMPRINT_PALETTE[3]  # blue         — chance nodes
const COL_TERMINAL = IMPRINT_PALETTE[4]  # ochre        — terminal nodes

# Data: software company product-launch decision tree
# Stage 1: Launch (D1) vs Abandon (T1)
# Stage 2 (after strong market): Expand (D2→C2) vs Maintain (T4)
# Rollback:
#   EMV(C2)  = 0.65×1800 + 0.35×400  = 1310
#   EMV(D2)  = max(1310, 750) = 1310  → Expand wins, Maintain pruned
#   EMV(C1)  = 0.55×1310 + 0.45×(−200) = 631
#   EMV(D1)  = max(631, 0) = 631      → Launch wins, Abandon pruned

const node_ids    = ["D1",       "T1",       "C1",     "T5",      "D2",       "T4",       "C2",          "T2",      "T3"]
const node_types  = [:decision,  :terminal,  :chance,  :terminal, :decision,  :terminal,  :chance,       :terminal, :terminal]
const node_xs     = [0.0,        3.0,        3.0,      6.5,       6.5,        10.0,       10.0,          13.5,      13.5]
const node_ys     = [1.5,       -1.5,        4.5,      2.5,       6.0,         4.0,        8.0,           9.0,       7.0]
const node_emvs   = [631.0,      0.0,      631.0,    -200.0,   1310.0,       750.0,     1310.0,        1800.0,    400.0]
const node_pruned = [false,      true,     false,     false,    false,        true,      false,         false,     false]
const node_names  = ["Launch?",  "Abandon", "Market", "Weak",   "Scale Up?", "Maintain", "Competition", "Low Comp","High Comp"]

const edge_froms  = ["D1",     "D1",              "C1",             "C1",               "D2",       "D2",    "C2",                "C2"]
const edge_tos    = ["T1",     "C1",              "T5",             "D2",               "T4",       "C2",    "T2",                "T3"]
const 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)"]
const edge_pruned = [true,     false,             false,            false,              true,       false,   false,               false]

const pos_lookup = Dict{String,Tuple{Float64,Float64}}(
    node_ids[i] => (node_xs[i], node_ys[i]) for i in eachindex(node_ids)
)

# Title — scale fontsize linearly when title exceeds 67-char baseline
title_str = "Product Launch Decision · tree-decision · julia · makie · anyplot.ai"
n_chars   = length(title_str)
title_sz  = max(13, round(Int, 20 * min(1.0, 67.0 / n_chars)))

# Figure — landscape 3200×1800 px (size 1600×900 × px_per_unit 2)
fig = Figure(
    size            = (1600, 900),
    fontsize        = 11,
    backgroundcolor = PAGE_BG,
)

ax = Axis(
    fig[1, 1];
    backgroundcolor    = PAGE_BG,
    title              = title_str,
    titlesize          = title_sz,
    titlecolor         = INK,
    topspinevisible    = false,
    rightspinevisible  = false,
    leftspinevisible   = false,
    bottomspinevisible = false,
    xgridvisible       = false,
    ygridvisible       = false,
    xticksvisible      = false,
    yticksvisible      = false,
    xticklabelsvisible = false,
    yticklabelsvisible = false,
)

xlims!(ax, -1.5, 15.5)
ylims!(ax, -3.0, 10.5)

# Draw edges (rendered before nodes so markers sit on top)
for i in eachindex(edge_froms)
    x1, y1 = pos_lookup[edge_froms[i]]
    x2, y2 = pos_lookup[edge_tos[i]]
    is_pr  = edge_pruned[i]
    dx     = x2 - x1
    dy     = y2 - y1
    elen   = sqrt(dx^2 + dy^2)

    ec  = is_pr ? RGBAf(INK_MUTED.r, INK_MUTED.g, INK_MUTED.b, 0.4f0) :
                  RGBAf(INK_SOFT.r,  INK_SOFT.g,  INK_SOFT.b,  0.9f0)
    lw  = is_pr ? 1.5 : 2.5
    lst = is_pr ? :dash : :solid

    lines!(ax, [x1, x2], [y1, y2]; color = ec, linewidth = lw, linestyle = lst)

    # Double-slash pruning marks (two perpendicular strokes across the edge)
    if is_pr
        nx_ = (-dy / elen) * 0.32
        ny_ = (dx  / elen) * 0.32
        pc  = RGBAf(INK_MUTED.r, INK_MUTED.g, INK_MUTED.b, 0.8f0)
        for frac in (0.41, 0.51)
            ox = x1 + dx * frac
            oy = y1 + dy * frac
            lines!(ax, [ox - nx_, ox + nx_], [oy - ny_, oy + ny_];
                   color = pc, linewidth = 2.2)
        end
    end

    # Branch label offset perpendicular to edge (counter-clockwise normal)
    # Pruned-edge labels use a larger offset to avoid collision with double-slash marks
    mx     = (x1 + x2) / 2
    my     = (y1 + y2) / 2
    off    = is_pr ? 0.8 : 0.5
    nx_off = (-dy / elen) * off
    ny_off = (dx  / elen) * off
    lc     = is_pr ? INK_MUTED : INK_SOFT
    text!(ax, mx + nx_off, my + ny_off;
          text = edge_labels[i], color = lc, fontsize = 10, align = (:center, :center))
end

# Draw nodes
for i in eachindex(node_ids)
    x      = node_xs[i]
    y      = node_ys[i]
    ntype  = node_types[i]
    emv    = node_emvs[i]
    pruned = node_pruned[i]
    name   = node_names[i]

    nc = if pruned
        RGBAf(INK_MUTED.r, INK_MUTED.g, INK_MUTED.b, 0.45f0)
    elseif ntype == :decision
        COL_DECISION
    elseif ntype == :chance
        COL_CHANCE
    else
        COL_TERMINAL
    end

    mk = ntype == :decision  ? :rect :
         ntype == :chance    ? :circle :
                               :rtriangle

    scatter!(ax, [x], [y];
             marker = mk, color = nc, markersize = 30,
             strokewidth = 1.5, strokecolor = PAGE_BG)

    lc = pruned ? INK_MUTED : INK

    # Short node name above the marker
    text!(ax, x, y + 0.62;
          text = name, color = lc, fontsize = 10, align = (:center, :bottom))

    # EMV (decision/chance) or payoff (terminal) below the marker
    emv_abs = abs(Int(round(emv)))
    emv_str = if ntype == :terminal
        emv >= 0 ? "\$$(emv_abs)K" : "-\$$(emv_abs)K"
    else
        emv >= 0 ? "EMV=\$$(emv_abs)K" : "EMV=-\$$(emv_abs)K"
    end
    text!(ax, x, y - 0.62;
          text = emv_str, color = lc, fontsize = 10, align = (:center, :top))
end

# Legend in bottom strip
leg_elems = [
    MarkerElement(marker = :rect,      color = COL_DECISION, strokewidth = 0, markersize = 14),
    MarkerElement(marker = :circle,    color = COL_CHANCE,   strokewidth = 0, markersize = 14),
    MarkerElement(marker = :rtriangle, color = COL_TERMINAL, strokewidth = 0, markersize = 14),
    LineElement(color = RGBAf(INK_MUTED.r, INK_MUTED.g, INK_MUTED.b, 0.7f0), linewidth = 1.5, linestyle = :dash),
]
leg_labels = ["Decision node", "Chance node", "Terminal node", "Pruned branch"]

Legend(
    fig[2, 1],
    leg_elems,
    leg_labels;
    orientation  = :horizontal,
    framevisible = false,
    labelcolor   = INK_SOFT,
    labelsize    = 10,
    padding      = (10f0, 10f0, 4f0, 4f0),
)
rowsize!(fig.layout, 2, Fixed(28))

save("plot-$(THEME).png", fig; px_per_unit = 2)

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/tree-decision/makie/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "tree-decision",
  "language": "julia",
  "library": "makie",
  "page": "https://anyplot.ai/tree-decision/julia/makie",
  "hub": "https://anyplot.ai/tree-decision",
  "code_json": "https://api.anyplot.ai/specs/tree-decision/makie/code",
  "spec_json": "https://api.anyplot.ai/specs/tree-decision",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/tree-decision/julia/makie/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/tree-decision/julia/makie/plot-dark.png",
  "quality_score": 85.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Decision Tree Visualization with Probabilities on anyplot.ai.

Other implementations