{"spec_id":"treemap-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' treemap-basic: Basic Treemap\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 91/100 | Created: 2026-08-04\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(ragg)\n\n# --- Theme tokens ------------------------------------------------------\nTHEME       <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG     <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\nINK         <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nIMPRINT_PALETTE <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n                     \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# WCAG relative luminance, used to pick readable ink for tile labels\nrelative_luminance <- function(hex) {\n  channel <- grDevices::col2rgb(hex) / 255\n  linear <- ifelse(channel <= 0.03928, channel / 12.92, ((channel + 0.055) / 1.055)^2.4)\n  sum(c(0.2126, 0.7152, 0.0722) * linear)\n}\n\n# Alpha-composite `hex` over `bg_hex` so label ink reflects the tile's\n# actual rendered shade (tiles vary alpha to encode hierarchy depth)\nblend_hex <- function(hex, bg_hex, alpha) {\n  fg <- as.vector(grDevices::col2rgb(hex))\n  bg <- as.vector(grDevices::col2rgb(bg_hex))\n  blended <- alpha * fg + (1 - alpha) * bg\n  grDevices::rgb(blended[1], blended[2], blended[3], maxColorValue = 255)\n}\n\n# Squarified treemap layout (Bruls, Huizing & van Wijk, 1999): greedily\n# grows rows/columns of tiles that keep aspect ratios close to square,\n# recursing into the leftover rectangle after each row is placed.\nsquarify <- function(values, x, y, w, h) {\n  n <- length(values)\n  out <- data.frame(xmin = numeric(n), xmax = numeric(n), ymin = numeric(n), ymax = numeric(n))\n  if (n == 0) return(out)\n  scaled <- values / sum(values) * (w * h)\n\n  worst_ratio <- function(row, side) {\n    s <- sum(row)\n    max((side^2 * max(row)) / (s^2), (s^2) / (side^2 * min(row)))\n  }\n\n  place_row <- function(row_vals, x, y, w, h) {\n    side <- min(w, h)\n    thickness <- sum(row_vals) / side\n    rects <- vector(\"list\", length(row_vals))\n    if (w >= h) {\n      cy <- y\n      for (k in seq_along(row_vals)) {\n        seg <- row_vals[k] / thickness\n        rects[[k]] <- list(xmin = x, xmax = x + thickness, ymin = cy, ymax = cy + seg)\n        cy <- cy + seg\n      }\n      remainder <- list(x = x + thickness, y = y, w = w - thickness, h = h)\n    } else {\n      cx <- x\n      for (k in seq_along(row_vals)) {\n        seg <- row_vals[k] / thickness\n        rects[[k]] <- list(xmin = cx, xmax = cx + seg, ymin = y, ymax = y + thickness)\n        cx <- cx + seg\n      }\n      remainder <- list(x = x, y = y + thickness, w = w, h = h - thickness)\n    }\n    list(rects = rects, remainder = remainder)\n  }\n\n  cur_x <- x; cur_y <- y; cur_w <- w; cur_h <- h\n  row_vals <- numeric(0); row_idx <- integer(0)\n  i <- 1\n  while (i <= n || length(row_vals) > 0) {\n    if (i <= n) {\n      side <- min(cur_w, cur_h)\n      trial <- c(row_vals, scaled[i])\n      if (length(row_vals) == 0 || worst_ratio(trial, side) <= worst_ratio(row_vals, side)) {\n        row_vals <- trial\n        row_idx <- c(row_idx, i)\n        i <- i + 1\n        next\n      }\n    }\n    placed <- place_row(row_vals, cur_x, cur_y, cur_w, cur_h)\n    for (k in seq_along(row_idx)) {\n      pos <- row_idx[k]\n      r <- placed$rects[[k]]\n      out$xmin[pos] <- r$xmin; out$xmax[pos] <- r$xmax\n      out$ymin[pos] <- r$ymin; out$ymax[pos] <- r$ymax\n    }\n    rem <- placed$remainder\n    cur_x <- rem$x; cur_y <- rem$y; cur_w <- rem$w; cur_h <- rem$h\n    row_vals <- numeric(0); row_idx <- integer(0)\n  }\n  out\n}\n\n# --- Data: monthly cloud infrastructure spend ($K) by service category --\nleaves <- tibble::tribble(\n  ~category,    ~resource,               ~spend,\n  \"Compute\",    \"On-Demand Instances\",      420,\n  \"Compute\",    \"Reserved Instances\",       310,\n  \"Compute\",    \"Serverless Functions\",     140,\n  \"Compute\",    \"Spot Instances\",            95,\n  \"Database\",   \"Managed SQL\",              220,\n  \"Database\",   \"Data Warehousing\",         175,\n  \"Database\",   \"NoSQL\",                    130,\n  \"Storage\",    \"Object Storage\",           260,\n  \"Storage\",    \"Block Storage\",            150,\n  \"Storage\",    \"Archival Storage\",          60,\n  \"Networking\", \"Data Transfer\",            180,\n  \"Networking\", \"CDN\",                       90,\n  \"Networking\", \"Load Balancers\",            70,\n  \"Security\",   \"Identity & Access\",         55,\n  \"Security\",   \"Threat Detection\",          40,\n  \"Security\",   \"Key Management\",            25\n)\n\n# --- Layout: squarify category totals, then subcategories within each ---\nDOMAIN_W <- 1600\nDOMAIN_H <- 900\n\ncat_totals <- leaves %>%\n  group_by(category) %>%\n  summarise(total = sum(spend), .groups = \"drop\") %>%\n  arrange(desc(total)) %>%\n  mutate(fill_hex = IMPRINT_PALETTE[seq_len(n())])\n\n# A slim header band per category (reserved above its children) holds the\n# category name + total, so it never competes for space with leaf labels.\ncat_layout <- bind_cols(cat_totals, squarify(cat_totals$total, 0, 0, DOMAIN_W, DOMAIN_H)) %>%\n  mutate(\n    header_h    = pmin(60, pmax(34, (ymax - ymin) * 0.14)),\n    header_label = paste0(category, \" · $\", total, \"K\"),\n    header_size = pmin(4.2, pmax(2.0, header_h / 16), (xmax - xmin - 20) / (nchar(header_label) * 5.2)),\n    header_ink  = ifelse(sapply(fill_hex, relative_luminance) > 0.4, \"#1A1A17\", \"#FFFDF6\")\n  )\n\nleaf_layout <- bind_rows(lapply(seq_len(nrow(cat_layout)), function(i) {\n  cat_row <- cat_layout[i, ]\n  sub <- leaves %>% filter(category == cat_row$category) %>% arrange(desc(spend))\n  rects <- squarify(sub$spend, cat_row$xmin, cat_row$ymin,\n                     cat_row$xmax - cat_row$xmin,\n                     (cat_row$ymax - cat_row$header_h) - cat_row$ymin)\n  bind_cols(sub, rects) %>% mutate(fill_hex = cat_row$fill_hex)\n})) %>%\n  group_by(category) %>%\n  mutate(\n    rank_in_cat = row_number(),\n    n_in_cat    = n(),\n    tile_alpha  = if (n_in_cat[1] == 1) 0.85 else seq(0.92, 0.55, length.out = n_in_cat[1])[rank_in_cat]\n  ) %>%\n  ungroup() %>%\n  mutate(\n    tile_fill  = mapply(function(hex, a) blend_hex(hex, PAGE_BG, a), fill_hex, tile_alpha),\n    label_ink  = ifelse(sapply(tile_fill, relative_luminance) > 0.4, \"#1A1A17\", \"#FFFDF6\"),\n    tile_w     = xmax - xmin,\n    tile_h     = ymax - ymin,\n    value_label = paste0(\"$\", spend, \"K\"),\n    # Text-width-aware thresholds (~12.5 domain units per char at size 2.7) —\n    # narrow tiles omit their label instead of overflowing past their bounds.\n    show_name  = tile_w > nchar(resource) * 12.5 & tile_h > 48,\n    show_value = show_name & tile_h > 78 & tile_w > nchar(value_label) * 12.5,\n    name_y     = ifelse(show_value, (ymin + ymax) / 2 + tile_h * 0.14, (ymin + ymax) / 2)\n  )\n\n# --- Title (shrinks to fit when the mandated string runs long) ----------\nplot_title  <- \"Cloud Infrastructure Spend · treemap-basic · r · ggplot2 · anyplot.ai\"\ntitle_ratio <- ifelse(nchar(plot_title) > 67, 67 / nchar(plot_title), 1.0)\ntitle_size  <- max(8, round(12 * title_ratio))\n\n# --- Plot -----------------------------------------------------------------\np <- ggplot() +\n  geom_rect(\n    data = leaf_layout,\n    aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax, fill = tile_fill),\n    color = PAGE_BG, linewidth = 1.3\n  ) +\n  scale_fill_identity() +\n  geom_text(\n    data = filter(leaf_layout, show_name),\n    aes(x = (xmin + xmax) / 2, y = name_y, label = resource, color = label_ink),\n    size = 2.7, lineheight = 0.9\n  ) +\n  geom_text(\n    data = filter(leaf_layout, show_value),\n    aes(x = (xmin + xmax) / 2, y = (ymin + ymax) / 2 - tile_h * 0.17,\n        label = value_label, color = label_ink),\n    size = 2.3, fontface = \"italic\"\n  ) +\n  geom_rect(\n    data = cat_layout,\n    aes(xmin = xmin, xmax = xmax, ymin = ymax - header_h, ymax = ymax, fill = fill_hex),\n    color = PAGE_BG, linewidth = 1.3\n  ) +\n  geom_text(\n    data = cat_layout,\n    aes(x = xmin + 14, y = ymax - header_h / 2, label = header_label,\n        color = header_ink, size = header_size),\n    hjust = 0, fontface = \"bold\"\n  ) +\n  geom_rect(\n    data = cat_layout,\n    aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax),\n    fill = NA, color = PAGE_BG, linewidth = 2.4\n  ) +\n  scale_color_identity() +\n  scale_size_identity() +\n  scale_x_continuous(limits = c(0, DOMAIN_W), expand = c(0, 0)) +\n  scale_y_continuous(limits = c(0, DOMAIN_H), expand = c(0, 0)) +\n  labs(title = plot_title) +\n  theme_void(base_size = 8) +\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(color = INK, size = title_size, face = \"bold\",\n                                     hjust = 0.5, margin = margin(t = 6, b = 10)),\n    plot.margin      = margin(10, 14, 10, 14)\n  )\n\n# --- Save -------------------------------------------------------------------\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"}