{"spec_id":"icicle-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' icicle-basic: Basic Icicle Chart\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 91/100 | Created: 2026-09-05\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(ragg)\n\nset.seed(42)\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\"\nNEUTRAL <- INK\nIMPRINT_PALETTE <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n                     \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# --- Data: file system hierarchy (folder/file sizes in KB) ------------------\nnodes <- tibble::tribble(\n  ~id,                   ~parent,        ~value,\n  \"project\",             NA,             NA,\n  \"src\",                 \"project\",      NA,\n  \"docs\",                \"project\",      NA,\n  \"tests\",               \"project\",      NA,\n  \"assets\",              \"project\",      NA,\n  \"config\",              \"project\",      NA,\n  \"components\",          \"src\",          NA,\n  \"utils\",               \"src\",          NA,\n  \"hooks\",               \"src\",          NA,\n  \"api\",                 \"src\",          NA,\n  \"guides\",              \"docs\",         NA,\n  \"api-docs\",            \"docs\",         NA,\n  \"unit\",                \"tests\",        NA,\n  \"integration\",         \"tests\",        NA,\n  \"images\",              \"assets\",       NA,\n  \"fonts\",               \"assets\",       NA,\n  \"Button.tsx\",          \"components\",   18,\n  \"Modal.tsx\",           \"components\",   24,\n  \"Table.tsx\",           \"components\",   31,\n  \"format.ts\",           \"utils\",         9,\n  \"validate.ts\",         \"utils\",        12,\n  \"useAuth.ts\",          \"hooks\",         7,\n  \"useFetch.ts\",         \"hooks\",         6,\n  \"client.ts\",           \"api\",          15,\n  \"routes.ts\",           \"api\",          11,\n  \"getting-started.md\",  \"guides\",        8,\n  \"deployment.md\",       \"guides\",       14,\n  \"endpoints.md\",        \"api-docs\",     19,\n  \"Button.test.ts\",      \"unit\",         10,\n  \"Table.test.ts\",       \"unit\",         13,\n  \"auth.test.ts\",        \"integration\",  22,\n  \"logo.svg\",            \"images\",        5,\n  \"hero.png\",            \"images\",       48,\n  \"Inter.woff2\",         \"fonts\",        36,\n  \"ci.yml\",              \"config\",        4,\n  \"build.config.js\",     \"config\",        6,\n  \"eslint.config.js\",    \"config\",       20,\n  \"tsconfig.json\",       \"config\",       15\n)\n\n# Depth of each node from the root (iterative — resolves one generation per pass)\nnodes$depth <- ifelse(is.na(nodes$parent), 0L, NA_integer_)\nfor (pass in 1:3) {\n  parent_depth <- setNames(nodes$depth, nodes$id)\n  pending <- is.na(nodes$depth)\n  nodes$depth[pending] <- parent_depth[nodes$parent[pending]] + 1L\n}\nmax_depth <- max(nodes$depth)\n\n# Node size = own value for leaves, sum of children for folders (bottom-up)\nnodes$size <- nodes$value\nfor (d in rev(0:(max_depth - 1))) {\n  for (pid in nodes$id[nodes$depth == d]) {\n    child_sizes <- nodes$size[nodes$parent == pid & !is.na(nodes$parent)]\n    if (length(child_sizes) > 0) nodes$size[nodes$id == pid] <- sum(child_sizes)\n  }\n}\n\n# Horizontal extent: each node's width is proportional to its size within\n# the span its parent occupies (top-down pass fills children from parents)\nnodes$x0 <- NA_real_\nnodes$x1 <- NA_real_\nnodes$x0[nodes$depth == 0] <- 0\nnodes$x1[nodes$depth == 0] <- nodes$size[nodes$depth == 0]\nfor (d in 0:(max_depth - 1)) {\n  for (pid in nodes$id[nodes$depth == d]) {\n    child_idx <- which(nodes$parent == pid)\n    if (length(child_idx) == 0) next\n    parent_x0 <- nodes$x0[nodes$id == pid]\n    parent_x1 <- nodes$x1[nodes$id == pid]\n    child_sizes <- nodes$size[child_idx]\n    cum_end <- cumsum(child_sizes) / sum(child_sizes) * (parent_x1 - parent_x0)\n    nodes$x0[child_idx] <- parent_x0 + c(0, head(cum_end, -1))\n    nodes$x1[child_idx] <- parent_x0 + cum_end\n  }\n}\n\n# Vertical position: root row at top, deeper levels stack below it\nnodes$y1 <- max_depth + 1 - nodes$depth\nnodes$y0 <- nodes$y1 - 1\n\n# Branch = the top-level folder each node descends from (root is its own branch)\nnodes$branch <- NA_character_\nnodes$branch[nodes$depth == 0] <- \"root\"\nfor (d in 1:max_depth) {\n  for (nid in nodes$id[nodes$depth == d]) {\n    nodes$branch[nodes$id == nid] <- if (d == 1) {\n      nid\n    } else {\n      nodes$branch[nodes$id == nodes$parent[nodes$id == nid]]\n    }\n  }\n}\n\npx_per_unit <- 3200 / nodes$size[nodes$depth == 0]\n\nnodes <- nodes %>%\n  mutate(\n    fill_color = case_when(\n      branch == \"root\"   ~ NEUTRAL,\n      branch == \"src\"    ~ IMPRINT_PALETTE[1],\n      branch == \"docs\"   ~ IMPRINT_PALETTE[2],\n      branch == \"tests\"  ~ IMPRINT_PALETTE[3],\n      branch == \"assets\" ~ IMPRINT_PALETTE[4],\n      branch == \"config\" ~ IMPRINT_PALETTE[5]\n    ),\n    depth_alpha = case_when(\n      depth <= 1 ~ 1.00,\n      depth == 2 ~ 0.75,\n      depth == 3 ~ 0.55\n    ),\n    label_color = if_else(depth <= 1, PAGE_BG, INK),\n    width = x1 - x0,\n    # A label only fits if the rect is wider than its own text at render size —\n    # avoids the classic icicle/treemap failure mode of text spilling past narrow cells.\n    char_px = if_else(depth <= 1, 30, 24.3),\n    required_width = (nchar(id) * char_px * 1.15) / px_per_unit,\n    show_label = width >= required_width\n  )\n\nheaders <- filter(nodes, show_label, depth <= 1)\nleaves  <- filter(nodes, show_label, depth > 1)\n\n# Focal point: the single largest terminal file gets an ink outline + bolder\n# label so the viewer has one clear \"biggest thing\" to anchor on.\nlargest_id <- nodes %>%\n  filter(!(id %in% nodes$parent)) %>%\n  slice_max(value, n = 1) %>%\n  pull(id)\n\nhighlight      <- filter(nodes, id == largest_id)\nleaves_plain   <- filter(leaves, id != largest_id)\nleaves_focal   <- filter(leaves, id == largest_id)\n\n# --- Plot --------------------------------------------------------------------\ntitle_text <- \"icicle-basic · r · ggplot2 · anyplot.ai\"\n\np <- ggplot(nodes) +\n  geom_rect(\n    aes(xmin = x0, xmax = x1, ymin = y0, ymax = y1,\n        fill = fill_color, alpha = depth_alpha),\n    color = PAGE_BG, linewidth = 0.4\n  ) +\n  geom_rect(\n    data = highlight,\n    aes(xmin = x0, xmax = x1, ymin = y0, ymax = y1),\n    color = INK, fill = NA, linewidth = 1.1\n  ) +\n  geom_text(\n    data = headers,\n    aes(x = (x0 + x1) / 2, y = (y0 + y1) / 2, label = id, color = label_color),\n    size = 3.2, fontface = \"bold\"\n  ) +\n  geom_text(\n    data = leaves_plain,\n    aes(x = (x0 + x1) / 2, y = (y0 + y1) / 2, label = id, color = label_color),\n    size = 2.8\n  ) +\n  geom_text(\n    data = leaves_focal,\n    aes(x = (x0 + x1) / 2, y = (y0 + y1) / 2, label = id, color = label_color),\n    size = 3.4, fontface = \"bold\"\n  ) +\n  scale_fill_identity() +\n  scale_alpha_identity() +\n  scale_color_identity() +\n  scale_x_continuous(expand = c(0, 0)) +\n  scale_y_continuous(expand = c(0, 0)) +\n  labs(title = title_text) +\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 = 12, hjust = 0, margin = margin(b = 10)),\n    plot.margin = margin(t = 14, r = 18, b = 10, l = 18)\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"}