{"spec_id":"wordcloud-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' wordcloud-basic: Basic Word Cloud\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 91/100 | Created: 2026-08-04\n\nlibrary(ggplot2)\nlibrary(tibble)\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\"\nIMPRINT_PALETTE <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n                     \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\nBRAND <- IMPRINT_PALETTE[1]\n\n# --- Data: term frequencies from a renewable-energy report scan ---------------\nterms <- c(\n  \"solar\", \"wind\", \"energy\", \"renewable\", \"grid\", \"battery\", \"storage\",\n  \"efficiency\", \"carbon\", \"emissions\", \"turbine\", \"hydropower\", \"biomass\",\n  \"geothermal\", \"investment\", \"innovation\", \"climate\", \"capacity\",\n  \"generation\", \"transition\", \"policy\", \"technology\", \"electricity\",\n  \"consumption\", \"demand\", \"supply\", \"market\", \"subsidy\", \"reliability\",\n  \"recycling\"\n)\nrank_order <- seq_along(terms)\nbase_freq  <- 150 / rank_order^0.65\nfrequency  <- pmax(round(base_freq + rnorm(length(terms), mean = 0, sd = 4)), 5)\n\nword_freq <- tibble(word = terms, frequency = frequency)\nword_freq <- word_freq[order(-word_freq$frequency), ]\n\n# Font size scales with sqrt(frequency) so rendered AREA (not height) tracks\n# term frequency, matching how the eye compares word-cloud prominence.\nsize_min <- 3.2\nsize_max <- 18\nfreq_sqrt <- sqrt(word_freq$frequency)\nword_freq$text_size <- size_min + (freq_sqrt - min(freq_sqrt)) /\n  diff(range(freq_sqrt)) * (size_max - size_min)\n\n# --- Layout: box-bounded spiral placement with rectangle collision ------------\n# geom_text's `size` aesthetic is in millimetres, so the layout below works\n# directly in millimetres too (1 coordinate unit = 1 mm) and the panel is\n# rendered at that exact physical scale via coord_fixed(ratio = 1) below —\n# this keeps the hand-measured bounding boxes true to the rendered glyphs.\n# Bounding-box multipliers are calibrated from grid::grobWidth/grobHeight on\n# the bold sans face actually used for geom_text (see dev notes: width per\n# char ranges ~0.55-0.68x fontsize, height ~0.735x fontsize; padded up here\n# for a safety margin against overlap).\nhalf_w_per_char <- 0.35\nhalf_h_factor   <- 0.42\nbox_half_w <- 86   # mm — half-width of the placement box (172mm total)\nbox_half_h <- 44   # mm — half-height of the placement box (88mm total)\nx_stretch  <- box_half_w / box_half_h\n\nn_words  <- nrow(word_freq)\nplaced_x <- numeric(n_words)\nplaced_y <- numeric(n_words)\nplaced_hw <- numeric(n_words)\nplaced_hh <- numeric(n_words)\nmax_radius <- sqrt(box_half_w^2 + box_half_h^2)\n\n# Fallback grid, closest-to-center first, for words the spiral walk can't\n# place within its step budget (a spiral path only samples one point per\n# radius, so it can miss small pockets of free space that a grid catches).\ngrid_x <- seq(-box_half_w, box_half_w, by = 1)\ngrid_y <- seq(-box_half_h, box_half_h, by = 1)\nfallback_grid <- expand.grid(x = grid_x, y = grid_y)\nfallback_grid <- fallback_grid[order(sqrt((fallback_grid$x / x_stretch)^2 + fallback_grid$y^2)), ]\n\nfor (i in seq_len(n_words)) {\n  fs <- word_freq$text_size[i]\n  half_w <- nchar(word_freq$word[i]) * fs * half_w_per_char\n  half_h <- fs * half_h_factor\n  placed <- FALSE\n\n  if (i == 1) {\n    cx <- 0\n    cy <- 0\n    placed <- TRUE\n  } else {\n    theta <- 0\n    radius <- 0\n    step <- 0\n    repeat {\n      cx <- radius * cos(theta) * x_stretch\n      cy <- radius * sin(theta)\n\n      in_box <- (abs(cx) + half_w <= box_half_w) && (abs(cy) + half_h <= box_half_h)\n      overlap <- !in_box\n      if (in_box) {\n        for (j in seq_len(i - 1)) {\n          dx <- abs(cx - placed_x[j])\n          dy <- abs(cy - placed_y[j])\n          if (dx < (half_w + placed_hw[j]) * 1.06 && dy < (half_h + placed_hh[j]) * 1.15) {\n            overlap <- TRUE\n            break\n          }\n        }\n      }\n      if (!overlap) {\n        placed <- TRUE\n        break\n      }\n\n      theta <- theta + 0.12\n      radius <- min(radius + 0.18, max_radius)\n      step <- step + 1\n      if (step > 8000) break\n    }\n  }\n\n  if (!placed) {\n    for (k in seq_len(nrow(fallback_grid))) {\n      gx <- fallback_grid$x[k]\n      gy <- fallback_grid$y[k]\n      if (abs(gx) + half_w > box_half_w || abs(gy) + half_h > box_half_h) next\n      overlap <- FALSE\n      for (j in seq_len(i - 1)) {\n        dx <- abs(gx - placed_x[j])\n        dy <- abs(gy - placed_y[j])\n        if (dx < (half_w + placed_hw[j]) * 1.06 && dy < (half_h + placed_hh[j]) * 1.15) {\n          overlap <- TRUE\n          break\n        }\n      }\n      if (!overlap) {\n        cx <- gx\n        cy <- gy\n        placed <- TRUE\n        break\n      }\n    }\n  }\n\n  placed_x[i] <- cx\n  placed_y[i] <- cy\n  placed_hw[i] <- half_w\n  placed_hh[i] <- half_h\n}\n\nword_freq$x <- placed_x\nword_freq$y <- placed_y\n\n# --- Plot -----------------------------------------------------------------\nplot_title <- \"wordcloud-basic · r · ggplot2 · anyplot.ai\"\ntitle_ratio <- if (nchar(plot_title) > 67) 67 / nchar(plot_title) else 1.0\ntitle_size <- round(12 * title_ratio)\n\np <- ggplot(word_freq, aes(x = x, y = y, label = word,\n                            size = text_size, color = frequency)) +\n  geom_text(fontface = \"bold\", family = \"sans\") +\n  scale_size_identity() +\n  scale_color_gradient(low = BRAND, high = \"#4467A3\", guide = \"none\") +\n  scale_x_continuous(limits = c(-box_half_w, box_half_w), expand = c(0, 0)) +\n  scale_y_continuous(limits = c(-box_half_h, box_half_h), expand = c(0, 0)) +\n  coord_fixed(ratio = 1, clip = \"off\") +\n  labs(title = plot_title) +\n  theme_void(base_size = 8) +\n  theme(\n    plot.background = element_rect(fill = PAGE_BG, color = PAGE_BG),\n    plot.title = element_text(color = INK, size = title_size, hjust = 0.5,\n                               margin = margin(b = 10)),\n    plot.margin = margin(t = 16, r = 24, b = 12, l = 24)\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"}