{"spec_id":"bubble-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' bubble-basic: Basic Bubble Chart\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 76/100 | Updated: 2026-09-30\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\"\nELEVATED_BG <- if (THEME == \"light\") \"#FFFDF6\" else \"#242420\"\nINK         <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\nGRID        <- if (THEME == \"light\") \"#D3D1CA\" else \"#3A3A37\"\n\n# Slot 5 (#AE3030, matte red) is the deferred semantic anchor for bad/loss/\n# error — there's no such category here, so Sporting Goods takes slot 6\n# (cyan) instead of spending red on an ordinary category.\nIMPRINT_PALETTE <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#2ABCCD\")\n\n# Data — synthetic retail product-portfolio scenario: customer satisfaction\n# score vs. average retail price, bubble = monthly sales volume, colored by\n# product category. Price bands are the dimension that keeps the five\n# categories visually separated (satisfaction scores alone overlap a lot more\n# across categories than price does), which avoids stacking every category\n# into one dense cluster.\nn_per_category <- 22\n\ncategory_params <- tibble::tibble(\n    category      = c(\"Consumer Electronics\", \"Apparel & Footwear\", \"Home & Kitchen\", \"Beauty & Personal Care\", \"Sporting Goods\"),\n    code          = c(\"ELEC\", \"APRL\", \"HOMK\", \"BEAU\", \"SPRT\"),\n    quality_mu    = c(74, 66, 79, 84, 70),\n    quality_sd    = c(8, 9, 7, 6, 8),\n    price_mu      = c(280, 52, 90, 36, 130),\n    price_sd      = c(70, 10, 22, 7, 35),\n    sales_meanlog = log(c(22, 68, 40, 75, 30)),\n    sales_sd      = c(0.30, 0.28, 0.32, 0.28, 0.32)\n)\n\n# Flat, vectorized generation: repeat each category's params n_per_category\n# times, then draw all rows in one rnorm()/rlnorm() call each (both accept\n# vectorized mean/sd arguments) instead of looping per category.\nrow_params <- category_params[rep(seq_len(nrow(category_params)), each = n_per_category), ]\nn_total <- nrow(row_params)\n\nproducts <- tibble::tibble(\n    category      = row_params$category,\n    satisfaction  = pmin(98, pmax(35, rnorm(n_total, mean = row_params$quality_mu, sd = row_params$quality_sd))),\n    price         = pmin(650, pmax(22, rnorm(n_total, mean = row_params$price_mu, sd = row_params$price_sd))),\n    sales_volume  = pmin(100, pmax(10, rlnorm(n_total, meanlog = row_params$sales_meanlog, sdlog = row_params$sales_sd)))\n) |>\n    dplyr::mutate(category = factor(category, levels = category_params$category)) |>\n    # Draw largest bubbles first (bottom layer) so smaller bubbles stay\n    # visible on top instead of being buried under high-volume sellers.\n    dplyr::arrange(dplyr::desc(sales_volume))\n\ncategory_colors <- stats::setNames(IMPRINT_PALETTE, levels(products$category))\n\n# Bubble-size domain floor: anchoring scale_size_area() at an absolute zero\n# buries the smallest real values at a couple of visible pixels. Flooring the\n# lower limit just below the observed minimum keeps sizing strictly area-true\n# across the data range while giving the smallest bubbles real presence.\nsales_range <- range(products$sales_volume)\nsize_limits <- c(sales_range[1] * 0.75, sales_range[2])\n\n# Plot\np <- ggplot(products, aes(\n    x    = satisfaction,\n    y    = price,\n    size = sales_volume,\n    fill = category\n)) +\n    geom_point(\n        shape  = 21,\n        color  = PAGE_BG,\n        alpha  = 0.42,\n        stroke = 1.0\n    ) +\n    scale_x_continuous(\n        breaks = seq(40, 100, 10),\n        expand = expansion(mult = c(0.08, 0.06))\n    ) +\n    scale_y_continuous(\n        breaks = seq(0, 600, 100),\n        labels = label_dollar(),\n        expand = expansion(mult = c(0.08, 0.08))\n    ) +\n    scale_size_area(\n        max_size = 13,\n        limits   = size_limits,\n        breaks   = c(10, 40, 70, 100),\n        labels   = c(\"10\", \"40\", \"70\", \"100\"),\n        name     = \"Monthly Sales Volume (K units)\"\n    ) +\n    scale_fill_manual(values = category_colors, name = \"Product Category\") +\n    labs(\n        title    = \"bubble-basic · r · ggplot2 · anyplot.ai\",\n        subtitle = \"Bubble size encodes monthly sales volume\",\n        x        = \"Customer Satisfaction Score (0-100)\",\n        y        = \"Average Retail Price\"\n    ) +\n    guides(\n        fill = guide_legend(override.aes = list(size = 4, alpha = 0.9))\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_line(color = GRID,       linewidth = 0.25),\n        panel.grid.major.y = element_line(color = GRID,       linewidth = 0.25),\n        panel.grid.minor   = element_blank(),\n        axis.title         = element_text(color = INK,        size = 10),\n        axis.text          = element_text(color = INK_SOFT,   size = 8),\n        plot.title         = element_text(color = INK,        size = 12),\n        plot.subtitle      = element_text(color = INK_SOFT,   size = 9, margin = margin(b = 8)),\n        legend.background  = element_rect(fill = ELEVATED_BG, color = NA),\n        legend.text        = element_text(color = INK_SOFT,   size = 8),\n        legend.title       = element_text(color = INK,        size = 10),\n        legend.key         = element_rect(fill = NA,          color = NA),\n        legend.key.size    = unit(0.35, \"cm\"),\n        legend.key.spacing.y = unit(1, \"pt\"),\n        legend.spacing.y   = unit(2, \"pt\"),\n        legend.justification.right = \"center\",\n        legend.margin      = margin(4, 6, 4, 6),\n        legend.box.spacing = unit(6, \"pt\"),\n        plot.margin        = margin(12, 12, 10, 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"}