{"spec_id":"tree-decision","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' tree-decision: Decision Tree Visualization with Probabilities\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 90/100 | Created: 2026-06-02\n\nlibrary(ggplot2)\nlibrary(ragg)\n\nset.seed(42)\n\n# Theme tokens (Imprint palette)\nTHEME       <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG     <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\nELEVATED_BG <- if (THEME == \"light\") \"#FFFDF6\" else \"#242420\"\nINK         <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\nINK_MUTED   <- if (THEME == \"light\") \"#6B6A63\" else \"#A8A79F\"\n\n# Imprint palette positions used\nCOL_DEC  <- \"#009E73\"  # position 1 — decision nodes (brand green)\nCOL_CHC  <- \"#4467A3\"  # position 3 — chance nodes (blue)\nCOL_TERM <- \"#BD8233\"  # position 4 — terminal nodes (ochre)\n\n# Tech startup product launch decision tree\n# Stage 1: Launch vs. Skip\n# Stage 2 (if Launch): Market Strong (p=0.6) vs. Weak (p=0.4)\n# Stage 3 (if Strong): Expand vs. Maintain\n#\n# EMV rollback:\n#   EMV(D1) = max($300K Expand, $150K Maintain) = $300K  -> Expand wins\n#   EMV(C0) = 0.6 * $300K + 0.4 * $20K = $188K\n#   EMV(D0) = max($188K Launch, $0 Skip) = $188K          -> Launch wins\n\nnodes <- data.frame(\n  id      = c(\"D0\",    \"C0\",    \"D1\",    \"T1\",  \"T2\",    \"T3\",  \"T4\"),\n  type    = c(\"dec\",   \"chc\",   \"dec\",   \"term\",\"term\",  \"term\",\"term\"),\n  x       = c(1.0,     3.0,     5.0,     7.0,   7.0,     5.0,   3.0),\n  y       = c(0.0,     0.5,     1.0,     1.5,   0.5,     0.0,  -0.5),\n  emv_lbl = c(\"$188K\", \"$188K\", \"$300K\", NA,    NA,      NA,    NA),\n  payoff  = c(NA,      NA,      NA,      300,   150,     20,    0),\n  pruned  = c(FALSE,   FALSE,   FALSE,   FALSE, TRUE,    FALSE, TRUE),\n  stringsAsFactors = FALSE\n)\n\nedges <- data.frame(\n  from   = c(\"D0\",    \"D0\",   \"C0\",           \"C0\",          \"D1\",     \"D1\"),\n  to     = c(\"C0\",    \"T4\",   \"D1\",           \"T3\",          \"T1\",     \"T2\"),\n  label  = c(\"Launch\",\"Skip\", \"p=0.6\\nStrong\",\"p=0.4\\nWeak\", \"Expand\", \"Maintain\"),\n  pruned = c(FALSE,   TRUE,   FALSE,          FALSE,         FALSE,    TRUE),\n  stringsAsFactors = FALSE\n)\n\n# Attach node coordinates to edges\nfi       <- match(edges$from, nodes$id)\nti       <- match(edges$to,   nodes$id)\nedges$x0 <- nodes$x[fi];  edges$y0 <- nodes$y[fi]\nedges$x1 <- nodes$x[ti];  edges$y1 <- nodes$y[ti]\n\n# Branch label at 30% from source; offset above/below line depending on slope\nedges$lx <- edges$x0 + 0.30 * (edges$x1 - edges$x0)\nedges$ly <- edges$y0 + 0.30 * (edges$y1 - edges$y0)\nedges$dy <- ifelse(edges$y1 >= edges$y0, 0.12, -0.12)\n\n# Pruned-mark position at 58% from source\nedges$px <- edges$x0 + 0.58 * (edges$x1 - edges$x0)\nedges$py <- edges$y0 + 0.58 * (edges$y1 - edges$y0)\n\n# Right-pointing triangle polygons for terminal nodes\nhw <- 0.14\nterm_rows <- which(nodes$type == \"term\")\ntri_frames <- lapply(seq_along(term_rows), function(i) {\n  ni <- term_rows[i]\n  data.frame(\n    x      = c(nodes$x[ni] - hw, nodes$x[ni] - hw, nodes$x[ni] + hw),\n    y      = c(nodes$y[ni] - hw * 0.85, nodes$y[ni] + hw * 0.85, nodes$y[ni]),\n    pruned = nodes$pruned[ni],\n    grp    = i\n  )\n})\ntri_data <- do.call(rbind, tri_frames)\n\n# Subsets for conditional styling\ne_live  <- edges[!edges$pruned, ]\ne_dead  <- edges[edges$pruned, ]\nt_live  <- tri_data[!tri_data$pruned, ]\nt_dead  <- tri_data[tri_data$pruned, ]\nn_dec   <- nodes[nodes$type == \"dec\", ]\nn_chc   <- nodes[nodes$type == \"chc\", ]\nn_emv   <- nodes[!is.na(nodes$emv_lbl), ]\nn_paylv <- nodes[nodes$type == \"term\" & !nodes$pruned, ]\nn_paydt <- nodes[nodes$type == \"term\" &  nodes$pruned, ]\n\np <- ggplot() +\n  # Edges: active\n  geom_segment(\n    data = e_live,\n    aes(x = x0, y = y0, xend = x1, yend = y1),\n    color = INK_SOFT, linewidth = 0.75\n  ) +\n  # Edges: pruned (dashed, muted)\n  geom_segment(\n    data = e_dead,\n    aes(x = x0, y = y0, xend = x1, yend = y1),\n    color = INK_MUTED, linewidth = 0.5, linetype = \"dashed\", alpha = 0.55\n  ) +\n  # Terminal triangles: active\n  geom_polygon(\n    data = t_live,\n    aes(x = x, y = y, group = grp),\n    fill = COL_TERM, color = INK_SOFT\n  ) +\n  # Terminal triangles: pruned (faded)\n  geom_polygon(\n    data = t_dead,\n    aes(x = x, y = y, group = grp),\n    fill = COL_TERM, color = INK_MUTED, alpha = 0.30\n  ) +\n  # Chance nodes (circles)\n  geom_point(\n    data = n_chc,\n    aes(x = x, y = y),\n    shape = 21, size = 9.5, fill = COL_CHC, color = INK_SOFT, stroke = 0.5\n  ) +\n  # Decision nodes (squares)\n  geom_point(\n    data = n_dec,\n    aes(x = x, y = y),\n    shape = 22, size = 9.5, fill = COL_DEC, color = INK_SOFT, stroke = 0.5\n  ) +\n  # EMV values inside decision/chance nodes\n  geom_text(\n    data = n_emv,\n    aes(x = x, y = y, label = emv_lbl),\n    color = \"white\", size = 2.4, fontface = \"bold\"\n  ) +\n  # Branch labels: active\n  geom_label(\n    data = e_live,\n    aes(x = lx, y = ly + dy, label = label),\n    color = INK_SOFT, fill = ELEVATED_BG, size = 2.3, lineheight = 0.85,\n    label.padding = unit(0.13, \"lines\"), label.size = 0\n  ) +\n  # Branch labels: pruned\n  geom_label(\n    data = e_dead,\n    aes(x = lx, y = ly + dy, label = label),\n    color = INK_MUTED, fill = ELEVATED_BG, size = 2.3, lineheight = 0.85,\n    label.padding = unit(0.13, \"lines\"), label.size = 0\n  ) +\n  # Double-strike pruned mark\n  geom_text(\n    data = e_dead,\n    aes(x = px, y = py),\n    label = \"//\", color = INK_MUTED, size = 3.8\n  ) +\n  # Payoff labels: active terminals (right of triangle)\n  geom_text(\n    data = n_paylv,\n    aes(x = x + hw + 0.09, y = y, label = paste0(\"$\", payoff, \"K\")),\n    color = INK, size = 3.0, fontface = \"bold\", hjust = 0\n  ) +\n  # Payoff labels: pruned terminals\n  geom_text(\n    data = n_paydt,\n    aes(x = x + hw + 0.09, y = y, label = paste0(\"$\", payoff, \"K\")),\n    color = INK_MUTED, size = 2.8, hjust = 0, alpha = 0.6\n  ) +\n  # Legend: Decision node\n  annotate(\"point\", x = 1.0, y = -0.90, shape = 22, size = 5,\n           fill = COL_DEC, color = INK_SOFT) +\n  annotate(\"text\", x = 1.18, y = -0.90, label = \"Decision\",\n           hjust = 0, size = 2.4, color = INK_SOFT) +\n  # Legend: Chance node\n  annotate(\"point\", x = 2.75, y = -0.90, shape = 21, size = 5,\n           fill = COL_CHC, color = INK_SOFT) +\n  annotate(\"text\", x = 2.93, y = -0.90, label = \"Chance\",\n           hjust = 0, size = 2.4, color = INK_SOFT) +\n  # Legend: Terminal node (small right-pointing triangle)\n  annotate(\"polygon\",\n           x = c(4.35 - 0.07, 4.35 - 0.07, 4.35 + 0.07),\n           y = c(-0.90 - 0.06, -0.90 + 0.06, -0.90),\n           fill = COL_TERM, color = INK_SOFT) +\n  annotate(\"text\", x = 4.50, y = -0.90, label = \"Terminal\",\n           hjust = 0, size = 2.4, color = INK_SOFT) +\n  coord_cartesian(xlim = c(0.35, 8.15), ylim = c(-1.15, 1.95), expand = FALSE) +\n  labs(title = \"tree-decision · r · ggplot2 · anyplot.ai\") +\n  theme_void() +\n  theme(\n    plot.background  = element_rect(fill = PAGE_BG, color = PAGE_BG),\n    panel.background = element_rect(fill = PAGE_BG, color = NA),\n    plot.title       = element_text(\n      color = INK, size = 12, hjust = 0.5,\n      margin = margin(t = 14, b = 8)\n    ),\n    plot.margin = margin(t = 5, r = 40, b = 20, l = 10)\n  )\n\nggsave(\n  filename = sprintf(\"plot-%s.png\", THEME),\n  plot     = p,\n  device   = ragg::agg_png,\n  width    = 8,\n  height   = 4.5,\n  units    = \"in\",\n  dpi      = 400\n)\n"}