{"spec_id":"bar-spine","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' bar-spine: Spine Plot for Two-Variable Proportions\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 91/100 | Created: 2026-09-02\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\"\nIMPRINT_PALETTE <- c(\n  \"#009E73\", # 1 - brand green (semantic: retained / good)\n  \"#AE3030\"  # 5 - matte red   (semantic: churned / bad)\n)\nSEGMENT_TEXT <- \"#FFFDF6\" # warm near-white, legible on both fills\n\n# --- Data -----------------------------------------------------------------\n# SaaS customer base split by subscription tier (bar width = tier size) and\n# retention outcome over the last billing cycle (segment height = share).\nTIER_LEVELS   <- c(\"Basic\", \"Standard\", \"Premium\", \"Enterprise\")\nSTATUS_LEVELS <- c(\"Retained\", \"Churned\")\n\ndf_counts <- tibble::tibble(\n  tier   = factor(rep(TIER_LEVELS, each = 2), levels = TIER_LEVELS),\n  status = factor(rep(STATUS_LEVELS, times = 4), levels = STATUS_LEVELS),\n  count  = c(816, 384, 738, 162, 460, 40, 288, 12)\n)\n\ntier_totals <- df_counts %>%\n  group_by(tier) %>%\n  summarise(total = sum(count), .groups = \"drop\") %>%\n  arrange(tier) %>%\n  mutate(\n    width = total / sum(total),\n    xmax  = cumsum(width),\n    xmin  = xmax - width,\n    xmid  = (xmin + xmax) / 2\n  )\n\nspine_df <- df_counts %>%\n  left_join(tier_totals, by = \"tier\") %>%\n  group_by(tier) %>%\n  arrange(tier, status) %>%\n  mutate(\n    prop = count / total,\n    ymax = cumsum(prop),\n    ymin = ymax - prop,\n    ymid = (ymin + ymax) / 2\n  ) %>%\n  ungroup()\n\n# --- Plot -------------------------------------------------------------------\nplot_title <- \"Customer Retention by Subscription Tier · bar-spine · r · ggplot2 · anyplot.ai\"\ntitle_fontsize <- max(8, round(12 * 67 / nchar(plot_title)))\n\np <- ggplot(spine_df) +\n  geom_rect(\n    aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax, fill = status),\n    color = NA\n  ) +\n  geom_text(\n    data = filter(spine_df, prop > 0.06),\n    aes(x = xmid, y = ymid, label = percent(prop, accuracy = 1)),\n    color = SEGMENT_TEXT, size = 3.2, fontface = \"bold\"\n  ) +\n  scale_fill_manual(\n    values = c(Retained = IMPRINT_PALETTE[1], Churned = IMPRINT_PALETTE[2]),\n    name = \"Status\"\n  ) +\n  scale_x_continuous(\n    breaks = tier_totals$xmid, labels = tier_totals$tier,\n    expand = c(0, 0)\n  ) +\n  scale_y_continuous(\n    labels = percent_format(accuracy = 1),\n    expand = expansion(mult = c(0, 0.02))\n  ) +\n  labs(\n    title = plot_title,\n    x = \"Subscription Tier (bar width ∝ customer base)\",\n    y = \"Share of Customers\"\n  ) +\n  theme_minimal(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    panel.grid.major.x = element_blank(),\n    panel.grid.minor   = element_blank(),\n    panel.grid.major.y = element_line(color = INK, linewidth = 0.3),\n    axis.title         = element_text(color = INK, size = 10),\n    axis.text          = element_text(color = INK_SOFT, size = 8),\n    axis.ticks         = element_blank(),\n    plot.title         = element_text(color = INK, size = title_fontsize, face = \"bold\"),\n    legend.text        = element_text(color = INK_SOFT, size = 8),\n    legend.title       = element_text(color = INK, size = 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"}