{"spec_id":"slope-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' slope-basic: Basic Slope Chart (Slopegraph)\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 85/100 | Created: 2026-07-25\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(tidyr)\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\"\nELEVATED_BG <- if (THEME == \"light\") \"#FFFDF6\" else \"#242420\"\nINK         <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\nIMPRINT_PALETTE <- c(\n  \"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n  \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"\n)\n\n# --- Data --------------------------------------------------------------------\n# Quarterly sales ($ thousands) for an outdoor-gear product line, Q1 vs Q4.\nproduct_line <- c(\n  \"Alpine Tent\", \"Ridge Boots\", \"Trail Runner\", \"Peak Headlamp\",\n  \"Glacier Jacket\", \"Summit Pack\", \"Canyon Hydration\", \"Basecamp Stove\"\n)\nq1_sales <- c(85, 135, 185, 230, 275, 325, 375, 430)\nq4_sales <- c(240, 145, 330, 195, 425, 285, 380, 475)\n\n# Pushes rows that land closer than min_gap apart so labels never collide,\n# regardless of how close the underlying values are — a general substitute\n# for ggrepel (unavailable in this runtime) rather than relying on the raw\n# values happening to already be well spaced.\nenforce_min_gap <- function(values, min_gap) {\n  ord    <- order(values)\n  spaced <- values[ord]\n  for (i in seq_along(spaced)[-1]) {\n    if (spaced[i] - spaced[i - 1] < min_gap) spaced[i] <- spaced[i - 1] + min_gap\n  }\n  spaced       <- spaced - mean(spaced) + mean(values)\n  out          <- numeric(length(values))\n  out[ord]     <- spaced\n  out\n}\nmin_gap <- diff(range(c(q1_sales, q4_sales))) * 0.06\n\nsales <- tibble::tibble(\n  entity      = product_line,\n  value_start = q1_sales,\n  value_end   = q4_sales,\n  y_start     = enforce_min_gap(q1_sales, min_gap),\n  y_end       = enforce_min_gap(q4_sales, min_gap)\n) %>%\n  mutate(\n    direction   = if_else(value_end > value_start, \"Increased\", \"Decreased\"),\n    direction   = factor(direction, levels = c(\"Increased\", \"Decreased\")),\n    glyph       = if_else(direction == \"Increased\", \"▲\", \"▼\"),\n    label_start = sprintf(\"%s   %s\", entity, dollar(value_start, suffix = \"K\")),\n    label_end   = sprintf(\"%s   %s\", dollar(value_end, suffix = \"K\"), entity)\n  )\n\nsales_long <- sales %>%\n  pivot_longer(\n    cols          = c(value_start, value_end, y_start, y_end),\n    names_to      = c(\".value\", \"period\"),\n    names_pattern = \"(value|y)_(start|end)\"\n  ) %>%\n  mutate(\n    x     = if_else(period == \"start\", 1, 2),\n    label = if_else(period == \"start\", label_start, label_end)\n  )\n\n# --- Plot ---------------------------------------------------------------------\np <- ggplot(sales_long, aes(x = x, y = y, group = entity, color = direction)) +\n  geom_line(aes(linetype = direction), linewidth = 1.1) +\n  geom_point(size = 3) +\n  geom_text(\n    data    = filter(sales_long, x == 1),\n    aes(label = label),\n    hjust   = 1,\n    nudge_x = -0.06,\n    size    = 3,\n    show.legend = FALSE\n  ) +\n  geom_text(\n    data    = filter(sales_long, x == 2),\n    aes(label = label),\n    hjust   = 0,\n    nudge_x = 0.06,\n    size    = 3,\n    show.legend = FALSE\n  ) +\n  geom_text(\n    aes(label = glyph),\n    vjust       = -1.2,\n    size        = 3,\n    fontface    = \"bold\",\n    show.legend = FALSE\n  ) +\n  scale_x_continuous(\n    breaks = c(1, 2),\n    labels = c(\"Q1 2024\", \"Q4 2024\"),\n    limits = c(0.35, 2.65),\n    expand = c(0, 0)\n  ) +\n  scale_color_manual(\n    name   = \"Change\",\n    values = c(\"Increased\" = IMPRINT_PALETTE[1], \"Decreased\" = \"#AE3030\")\n  ) +\n  scale_linetype_manual(\n    name   = \"Change\",\n    values = c(\"Increased\" = \"solid\", \"Decreased\" = \"dashed\")\n  ) +\n  labs(\n    title = \"slope-basic · r · ggplot2 · anyplot.ai\",\n    x     = NULL,\n    y     = NULL\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          = element_blank(),\n    axis.title          = element_blank(),\n    axis.text.y         = element_blank(),\n    axis.ticks          = element_blank(),\n    axis.text.x         = element_text(color = INK, size = 10, face = \"bold\"),\n    plot.title          = element_text(color = INK, size = 12),\n    legend.position     = \"bottom\",\n    legend.key.width    = unit(1.6, \"cm\"),\n    legend.background   = element_rect(fill = ELEVATED_BG, color = INK_SOFT),\n    legend.text         = element_text(color = INK_SOFT, size = 8),\n    legend.title        = element_text(color = INK, size = 10),\n    plot.margin         = margin(t = 10, r = 65, b = 10, l = 65)\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"}