{"spec_id":"waterfall-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' waterfall-basic: Basic Waterfall Chart\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 91/100 | Created: 2026-08-04\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(scales)\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\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\n\n# Imprint palette — profit/loss carry a strong sentiment color cue (see\n# default-style-guide.md \"Semantic exception\"), so increases use brand green,\n# decreases use the matte-red semantic anchor, and totals use blue (position 3)\n# to stand out as the spec's \"distinct start/end total bars\" requirement.\nIMPRINT_INCREASE <- \"#009E73\"\nIMPRINT_DECREASE <- \"#AE3030\"\nIMPRINT_TOTAL    <- \"#4467A3\"\n\n# --- Data: quarterly profit bridge, revenue to net profit ($K) --------------\nsteps <- tibble::tibble(\n  category = factor(\n    c(\n      \"Gross Revenue\", \"Cost of Goods Sold\", \"Operating Expenses\",\n      \"R&D Investment\", \"Other Income\", \"Taxes\", \"Net Profit\"\n    ),\n    levels = c(\n      \"Gross Revenue\", \"Cost of Goods Sold\", \"Operating Expenses\",\n      \"R&D Investment\", \"Other Income\", \"Taxes\", \"Net Profit\"\n    )\n  ),\n  raw_value = c(850, -320, -180, -95, 40, -85, NA),\n  is_total  = c(TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, TRUE)\n)\n\nrunning <- numeric(nrow(steps))\ncum <- 0\nfor (i in seq_len(nrow(steps))) {\n  if (steps$is_total[i] && i == 1) {\n    cum <- steps$raw_value[i]\n  } else if (steps$is_total[i]) {\n    steps$raw_value[i] <- cum\n  } else {\n    cum <- cum + steps$raw_value[i]\n  }\n  running[i] <- cum\n}\n\nsteps <- steps %>%\n  mutate(\n    x_num    = row_number(),\n    cum_end  = running,\n    cum_prev = lag(cum_end, default = 0),\n    ymin     = ifelse(is_total, 0, pmin(cum_prev, cum_end)),\n    ymax     = ifelse(is_total, raw_value, pmax(cum_prev, cum_end)),\n    type     = factor(\n      case_when(\n        is_total ~ \"Total\",\n        raw_value >= 0 ~ \"Increase\",\n        TRUE ~ \"Decrease\"\n      ),\n      levels = c(\"Increase\", \"Decrease\", \"Total\")\n    ),\n    label = ifelse(\n      is_total,\n      dollar(raw_value, suffix = \"K\"),\n      paste0(ifelse(raw_value >= 0, \"+\", \"-\"), dollar(abs(raw_value), suffix = \"K\"))\n    ),\n    label_y = ifelse(type == \"Decrease\", ymin - 25, ymax + 25),\n    label_vjust = ifelse(type == \"Decrease\", 1, 0),\n    # Secondary label: the literal running total reached after this step\n    # (intermediate bars only — total bars already display cum_end as `label`).\n    running_label = ifelse(is_total, NA_character_, dollar(cum_end, suffix = \"K\")),\n    running_label_y = ifelse(type == \"Decrease\", ymin - 70, ymax + 70),\n    is_final = row_number() == n(),\n    label_face = ifelse(is_final, \"bold\", \"plain\")\n  )\n\nbar_width <- 0.62\nconnectors <- steps %>%\n  mutate(x_to = lead(x_num) - bar_width / 2) %>%\n  filter(row_number() < n()) %>%\n  transmute(\n    x_from = x_num + bar_width / 2,\n    x_to,\n    y = cum_end\n  )\n\n# --- Plot ---------------------------------------------------------------------\ntitle_text <- \"Quarterly Profit Bridge · waterfall-basic · r · ggplot2 · anyplot.ai\"\ntitle_n <- nchar(title_text)\ntitle_size <- if (title_n > 67) max(round(12 * 67 / title_n), 8) else 12\n\np <- ggplot(steps) +\n  geom_segment(\n    data = connectors,\n    aes(x = x_from, xend = x_to, y = y, yend = y),\n    color = INK_SOFT, linewidth = 0.4, linetype = \"dotted\"\n  ) +\n  geom_rect(\n    aes(xmin = x_num - bar_width / 2, xmax = x_num + bar_width / 2,\n        ymin = ymin, ymax = ymax, fill = type),\n    color = PAGE_BG, linewidth = 0.5\n  ) +\n  # Focal treatment for the bottom-line bar: bold outline on top of the fill.\n  geom_rect(\n    data = filter(steps, is_final),\n    aes(xmin = x_num - bar_width / 2, xmax = x_num + bar_width / 2,\n        ymin = ymin, ymax = ymax),\n    fill = NA, color = INK, linewidth = 1.3\n  ) +\n  geom_text(\n    aes(x = x_num, y = label_y, label = label, vjust = label_vjust, fontface = label_face),\n    color = INK, size = 3.2\n  ) +\n  geom_text(\n    data = filter(steps, !is_total),\n    aes(x = x_num, y = running_label_y, label = running_label, vjust = label_vjust),\n    color = INK_SOFT, size = 2.6, fontface = \"italic\"\n  ) +\n  scale_x_continuous(breaks = steps$x_num, labels = steps$category) +\n  scale_y_continuous(\n    labels = label_dollar(suffix = \"K\"),\n    expand = expansion(mult = c(0.03, 0.14))\n  ) +\n  scale_fill_manual(\n    values = c(\n      \"Increase\" = IMPRINT_INCREASE,\n      \"Decrease\" = IMPRINT_DECREASE,\n      \"Total\"    = IMPRINT_TOTAL\n    )\n  ) +\n  labs(x = NULL, y = \"Amount ($K)\", fill = NULL, title = title_text) +\n  theme_minimal(base_size = 8)\n\n# --- Style ---------------------------------------------------------------------\np <- p +\n  theme(\n    plot.background      = element_rect(fill = PAGE_BG, color = PAGE_BG),\n    panel.background     = element_rect(fill = PAGE_BG, color = NA),\n    panel.grid.major.x   = element_blank(),\n    panel.grid.minor     = element_blank(),\n    panel.grid.major.y   = element_line(color = INK, linewidth = 0.25),\n    axis.title.y         = element_text(color = INK, size = 10),\n    axis.text.x          = element_text(color = INK_SOFT, size = 8, angle = 25, hjust = 1),\n    axis.text.y          = element_text(color = INK_SOFT, size = 8),\n    axis.ticks           = element_blank(),\n    axis.line            = element_blank(),\n    plot.title           = element_text(color = INK, size = title_size, face = \"bold\", margin = margin(b = 10)),\n    legend.position       = \"top\",\n    legend.justification  = \"left\",\n    legend.text           = element_text(color = INK_SOFT, size = 8),\n    legend.key            = element_rect(fill = PAGE_BG, color = NA),\n    plot.margin           = margin(t = 10, r = 20, b = 10, l = 10)\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"}