{"spec_id":"burndown-sprint","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' burndown-sprint: Agile Sprint Burndown Chart\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 88/100 | Created: 2026-06-14\n\nlibrary(ggplot2)\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\"\nINK_MUTED   <- if (THEME == \"light\") \"#6B6A63\" else \"#A8A79F\"\n\nIMPRINT_PALETTE <- c(\n    \"#009E73\",  # 1 — brand green (actual burndown)\n    \"#C475FD\",  # 2 — lavender\n    \"#4467A3\",  # 3 — blue (ideal line)\n    \"#BD8233\",  # 4 — ochre\n    \"#AE3030\",  # 5 — matte red\n    \"#2ABCCD\",  # 6 — cyan\n    \"#954477\",  # 7 — rose\n    \"#99B314\"   # 8 — lime\n)\n\nANYPLOT_AMBER <- \"#DDCC77\"  # scope-change marker (warning/caution)\n\n# Sprint data — 10 working days, starting with 52 story points\n# Scope change on day 4 (+8 points added)\nsprint_days      <- 1:10\ntotal_scope      <- 52\nscope_change_day <- 4\n\n# Ideal burndown: straight line from total_scope to 0 over 10 days\nideal_initial <- seq(total_scope, 0, length.out = 11)[sprint_days]\n\n# Actual remaining work (step function — discrete completions)\nactual_remaining <- c(52, 47, 43, 40, 56, 50, 44, 35, 22, 0)\n\ndf <- data.frame(\n    day    = sprint_days,\n    actual = actual_remaining,\n    ideal  = ideal_initial\n)\n\n# Scope change annotation position (between day 4 and 5)\nscope_change_x <- scope_change_day + 0.5\n\n# Title\nplot_title <- \"burndown-sprint · r · ggplot2 · anyplot.ai\"\ntitle_chars <- nchar(plot_title)\ntitle_size  <- max(8, round(12 * 67 / title_chars))\n\np <- ggplot(df, aes(x = day)) +\n\n    # Weekend shading band (between working week 1 and 2)\n    annotate(\n        \"rect\",\n        xmin = 5.5, xmax = 6.5,\n        ymin = -Inf, ymax = Inf,\n        fill = INK_SOFT, alpha = 0.08\n    ) +\n\n    # Ideal burndown line (dashed, blue) — mapped for legend\n    geom_line(\n        aes(y = ideal, color = \"Ideal\", linetype = \"Ideal\"),\n        linewidth = 0.9\n    ) +\n\n    # Actual remaining work as step series — mapped for legend\n    geom_step(\n        aes(y = actual, color = \"Actual\", linetype = \"Actual\"),\n        linewidth = 1.2\n    ) +\n\n    # Points on actual line for daily readings\n    geom_point(\n        aes(y = actual, color = \"Actual\"),\n        size = 3.0, shape = 16, show.legend = FALSE\n    ) +\n\n    # Scope change marker — vertical dotted line in amber\n    geom_vline(\n        xintercept = scope_change_x,\n        color      = ANYPLOT_AMBER,\n        linewidth  = 0.8,\n        linetype   = \"dotted\"\n    ) +\n\n    # Scope change annotation label\n    annotate(\n        \"text\",\n        x        = scope_change_x + 0.12,\n        y        = 54,\n        label    = \"+8 pts\\nscope added\",\n        color    = ANYPLOT_AMBER,\n        size     = 2.8,\n        hjust    = 0,\n        fontface = \"bold\"\n    ) +\n\n    # Weekend annotation\n    annotate(\n        \"text\",\n        x     = 6.0,\n        y     = 2,\n        label = \"weekend\",\n        color = INK_MUTED,\n        size  = 2.2,\n        angle = 90\n    ) +\n\n    # Native legend via color + linetype aesthetic mappings\n    scale_color_manual(\n        values = c(\"Actual\" = IMPRINT_PALETTE[1], \"Ideal\" = IMPRINT_PALETTE[3]),\n        name   = NULL,\n        breaks = c(\"Actual\", \"Ideal\"),\n        guide  = guide_legend(\n            override.aes = list(\n                linetype  = c(\"solid\", \"dashed\"),\n                linewidth = c(1.2, 0.9),\n                shape     = c(16, NA)\n            )\n        )\n    ) +\n    scale_linetype_manual(\n        values = c(\"Actual\" = \"solid\", \"Ideal\" = \"dashed\"),\n        name   = NULL,\n        guide  = guide_none()\n    ) +\n\n    # Axes\n    scale_x_continuous(\n        breaks = sprint_days,\n        labels = paste0(\"Day \", sprint_days),\n        expand = expansion(mult = c(0.03, 0.05))\n    ) +\n    scale_y_continuous(\n        limits = c(0, 62),\n        breaks = seq(0, 60, by = 10),\n        expand = expansion(mult = c(0.02, 0.04))\n    ) +\n\n    labs(\n        title = plot_title,\n        x     = \"Sprint Day\",\n        y     = \"Remaining Story Points\"\n    ) +\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     = element_line(color = INK_SOFT, linewidth = 0.2),\n        panel.grid.minor     = element_blank(),\n        panel.border         = element_blank(),\n        axis.line            = element_line(color = INK_SOFT, linewidth = 0.4),\n        axis.ticks           = element_line(color = INK_SOFT, linewidth = 0.3),\n        axis.title           = element_text(color = INK,      size = 10),\n        axis.text            = element_text(color = INK_SOFT, size = 8),\n        axis.text.x          = element_text(angle = 30, hjust = 1),\n        plot.title           = element_text(color = INK, size = title_size,\n                                            margin = margin(b = 10)),\n        plot.margin          = margin(20, 20, 15, 15),\n        legend.position        = \"inside\",\n        legend.position.inside = c(0.98, 0.98),\n        legend.justification   = c(1, 1),\n        legend.background    = element_rect(fill = PAGE_BG, color = NA),\n        legend.key           = element_rect(fill = PAGE_BG, color = NA),\n        legend.text          = element_text(color = INK_SOFT, size = 8),\n        legend.key.width     = unit(2, \"lines\"),\n        legend.margin        = margin(2, 4, 2, 4)\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"}