diff --git a/plots/circlepacking-basic/implementations/r/ggplot2.R b/plots/circlepacking-basic/implementations/r/ggplot2.R new file mode 100644 index 00000000000..19dc2a4f25b --- /dev/null +++ b/plots/circlepacking-basic/implementations/r/ggplot2.R @@ -0,0 +1,256 @@ +#' anyplot.ai +#' circlepacking-basic: Circle Packing Chart +#' Library: ggplot2 3.5.1 | R 4.4.1 +#' Quality: 90/100 | Created: 2026-09-02 + +library(ggplot2) +library(dplyr) +library(tibble) +library(ragg) + +set.seed(42) + +# --- Theme tokens ------------------------------------------------------- +THEME <- Sys.getenv("ANYPLOT_THEME", "light") +PAGE_BG <- if (THEME == "light") "#FAF8F1" else "#1A1A17" +ELEVATED_BG <- if (THEME == "light") "#FFFDF6" else "#242420" +INK <- if (THEME == "light") "#1A1A17" else "#F0EFE8" +INK_SOFT <- if (THEME == "light") "#4A4A44" else "#B8B7B0" +IMPRINT_PALETTE <- c("#009E73", "#C475FD", "#4467A3", "#BD8233", + "#AE3030", "#2ABCCD", "#954477", "#99B314") + +# --- Data: a repository's directory tree, sized by file weight (KB) ----- +categories <- c("src", "tests", "docs", "assets", "config", "scripts") +category_labels <- c( + src = "Source Code", + tests = "Tests", + docs = "Documentation", + assets = "Assets", + config = "Config", + scripts = "Scripts" +) +meanlog_by_cat <- c(src = 5.2, tests = 4.3, docs = 4.0, assets = 6.1, config = 3.0, scripts = 4.1) +sdlog_by_cat <- c(src = 0.55, tests = 0.6, docs = 0.7, assets = 0.9, config = 0.5, scripts = 0.6) +file_pool <- list( + src = c("router", "auth", "database", "utils", "server", "api", "cache", "logger", "parser", "scheduler"), + tests = c("test_auth", "test_api", "test_database", "test_utils", "test_router", "test_cache"), + docs = c("readme", "architecture", "api-guide", "changelog", "contributing", "faq"), + assets = c("logo", "banner", "icon-set", "hero-image", "background", "favicon"), + config = c("app", "database", "logging", "ci", "docker", "eslint"), + scripts = c("deploy", "build", "migrate", "seed", "backup", "release") +) +file_ext <- c(src = ".R", tests = ".R", docs = ".md", assets = ".png", config = ".yaml", scripts = ".sh") + +leaves <- bind_rows(lapply(categories, function(category_name) { + n_files <- sample(6:11, 1) + names <- sample(file_pool[[category_name]], n_files, replace = TRUE) + tibble( + category = category_name, + id = paste0(category_name, "_", sprintf("%02d", seq_len(n_files))), + parent = category_name, + label = paste0(names, file_ext[[category_name]]), + value = round(rlnorm(n_files, meanlog = meanlog_by_cat[[category_name]], sdlog = sdlog_by_cat[[category_name]]), 1) + ) +})) + +# --- Circle packing: force-relaxation algorithm ------------------------- +# Places circles of given radii tangent to their neighbours without +# overlap (spec: "Pack circles efficiently using force simulation"), +# then recenters the cluster on its own centroid. +pack_children <- function(radii, iterations = 500) { + n <- length(radii) + if (n == 1) { + return(tibble(x = 0, y = 0, r = radii)) + } + + ord <- order(radii, decreasing = TRUE) + r_sorted <- radii[ord] + padding <- 0.03 * mean(r_sorted) + + golden_angle <- pi * (3 - sqrt(5)) + idx <- seq_len(n) + spread <- sum(r_sorted) * 0.5 + x <- spread * sqrt(idx / n) * cos(idx * golden_angle) + y <- spread * sqrt(idx / n) * sin(idx * golden_angle) + + for (iter in seq_len(iterations)) { + for (i in seq_len(n - 1)) { + for (j in seq(i + 1, n)) { + dx <- x[j] - x[i] + dy <- y[j] - y[i] + dist <- sqrt(dx^2 + dy^2) + min_dist <- r_sorted[i] + r_sorted[j] + padding + if (dist < min_dist) { + if (dist < 1e-9) { + dx <- runif(1, -1, 1); dy <- runif(1, -1, 1) + dist <- sqrt(dx^2 + dy^2) + } + overlap <- (min_dist - dist) / 2 + ux <- dx / dist; uy <- dy / dist + x[i] <- x[i] - ux * overlap; y[i] <- y[i] - uy * overlap + x[j] <- x[j] + ux * overlap; y[j] <- y[j] + uy * overlap + } + } + } + x <- x - mean(x) * 0.02 + y <- y - mean(y) * 0.02 + } + + x <- x - mean(x) + y <- y - mean(y) + tibble(x = x[order(ord)], y = y[order(ord)], r = radii) +} + +circle_points <- function(id, cx, cy, r, n = 72) { + theta <- seq(0, 2 * pi, length.out = n) + tibble(id = id, x = cx + r * cos(theta), y = cy + r * sin(theta)) +} + +# Level 1: pack leaf circles inside each category (area-accurate radius) +leaves_packed <- leaves %>% + group_by(category) %>% + group_modify(~ bind_cols(.x, pack_children(sqrt(.x$value / pi)))) %>% + ungroup() + +# Level 2: derive each category's outer radius from its packed children, +# then pack the categories inside the root with the same algorithm +category_stats <- leaves_packed %>% + group_by(category) %>% + summarise(enclose_r = max(sqrt(x^2 + y^2) + r), .groups = "drop") %>% + mutate( + draw_r = enclose_r * 1.15, + label = category_labels[category] + ) %>% + arrange(match(category, categories)) + +cat_positions <- pack_children(category_stats$draw_r) +category_stats$x_cat <- cat_positions$x +category_stats$y_cat <- cat_positions$y + +root_r <- max(sqrt(category_stats$x_cat^2 + category_stats$y_cat^2) + category_stats$draw_r) * 1.14 + +leaves_final <- leaves_packed %>% + left_join(category_stats %>% select(category, x_cat, y_cat), by = "category") %>% + mutate(abs_x = x + x_cat, abs_y = y + y_cat) + +# --- Polygons for rendering ---------------------------------------------- +root_poly <- circle_points("root", 0, 0, root_r) + +cat_polys <- bind_rows(Map( + circle_points, + id = category_stats$category, cx = category_stats$x_cat, + cy = category_stats$y_cat, r = category_stats$draw_r +)) + +leaf_polys <- bind_rows(Map( + circle_points, + id = leaves_final$id, cx = leaves_final$abs_x, + cy = leaves_final$abs_y, r = leaves_final$r +)) %>% + left_join(leaves_final %>% select(id, category), by = "id") + +cat_labels <- category_stats %>% + mutate( + dist_from_root = pmax(sqrt(x_cat^2 + y_cat^2), 1e-6), + dir_x = x_cat / dist_from_root, + dir_y = y_cat / dist_from_root, + label_x = x_cat + dir_x * (draw_r + root_r * 0.035), + label_y = y_cat + dir_y * (draw_r + root_r * 0.035), + label_size = 2.6 + 1.1 * (draw_r / max(draw_r)), + # anchor the text edge (not its center) to the outward point, so the + # whole label clears the circle boundary regardless of approach angle + label_hjust = case_when( + abs(dir_x) < abs(dir_y) ~ 0.5, + dir_x >= 0 ~ 0, + TRUE ~ 1 + ), + label_vjust = case_when( + abs(dir_y) <= abs(dir_x) ~ 0.5, + dir_y >= 0 ~ 0, + TRUE ~ 1 + ) + ) + +# --- Title (fontsize scales with title length) --------------------------- +plot_title <- "circlepacking-basic · r · ggplot2 · anyplot.ai" +title_n <- nchar(plot_title) +title_ratio <- if (title_n > 67) 67 / title_n else 1.0 +title_fontsize <- max(8, round(12 * title_ratio)) + +fill_values <- setNames(IMPRINT_PALETTE[seq_along(categories)], categories) + +# Text color per category chosen for contrast against that category's fill +# (data-tied, so — like the fill colors themselves — it does not flip with +# THEME). +pal_rgb <- col2rgb(fill_values) +pal_luma <- (0.299 * pal_rgb["red", ] + 0.587 * pal_rgb["green", ] + 0.114 * pal_rgb["blue", ]) / 255 +leaf_label_ink <- ifelse(pal_luma < 0.5, "#F5F3EC", "#1A1A17") + +# --- The largest leaf in each category, kept only for the 2 biggest ------ +# categories, so every labeled circle is actually large enough to hold its +# text: a small category's own biggest leaf can still be too tiny to read. +leaf_top <- leaves_final %>% + group_by(category) %>% + slice_max(order_by = r, n = 1, with_ties = FALSE) %>% + ungroup() %>% + slice_max(order_by = r, n = 2, with_ties = FALSE) %>% + mutate( + label_size = pmin(3.2, pmax(1.8, 1.8 + 1.8 * (r / max(r)))), + text_color = leaf_label_ink[category] + ) + +focal_id <- leaf_top$id[which.max(leaf_top$r)] +focal_ring <- leaf_polys %>% filter(id == focal_id) + +# --- Plot ------------------------------------------------------------------ +p <- ggplot() + + geom_polygon(data = root_poly, aes(x, y), fill = ELEVATED_BG, color = NA) + + geom_polygon( + data = cat_polys, aes(x, y, group = id), + fill = NA, color = INK_SOFT, linewidth = 0.45, alpha = 0.7 + ) + + geom_polygon( + data = leaf_polys, aes(x, y, group = id, fill = category), + color = PAGE_BG, linewidth = 0.3, alpha = 0.9 + ) + + geom_polygon( + data = focal_ring, aes(x, y, group = id), + fill = NA, color = INK, linewidth = 0.9 + ) + + geom_text( + data = cat_labels, + aes(label_x, label_y, label = label, size = label_size, hjust = label_hjust, vjust = label_vjust), + color = INK, fontface = "bold" + ) + + geom_text( + data = leaf_top, + aes(abs_x, abs_y, label = label, size = label_size, color = text_color), + fontface = "bold" + ) + + scale_fill_manual(values = fill_values, guide = "none") + + scale_color_identity() + + scale_size_identity(guide = "none") + + coord_fixed( + xlim = c(-root_r * 1.12, root_r * 1.12), + ylim = c(-root_r * 1.12, root_r * 1.12), + expand = FALSE + ) + + labs(title = plot_title) + + theme_void(base_size = 8) + + theme( + plot.background = element_rect(fill = PAGE_BG, color = PAGE_BG), + panel.background = element_rect(fill = PAGE_BG, color = NA), + plot.title = element_text(color = INK, size = title_fontsize, face = "bold", hjust = 0.5, margin = margin(b = 12)), + plot.margin = margin(14, 14, 14, 14) + ) + +# --- Save -------------------------------------------------------------- +ggsave( + filename = sprintf("plot-%s.png", THEME), + plot = p, + device = ragg::agg_png, + width = 6, + height = 6, + units = "in", + dpi = 400 +) diff --git a/plots/circlepacking-basic/metadata/r/ggplot2.yaml b/plots/circlepacking-basic/metadata/r/ggplot2.yaml new file mode 100644 index 00000000000..b10a25e54e0 --- /dev/null +++ b/plots/circlepacking-basic/metadata/r/ggplot2.yaml @@ -0,0 +1,267 @@ +library: ggplot2 +language: r +specification_id: circlepacking-basic +created: '2026-09-02T15:36:31Z' +updated: '2026-09-02T16:01:44Z' +generated_by: claude-sonnet +workflow_run: 33648472192 +issue: 2498 +language_version: 4.4.1 +library_version: 3.5.1 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/circlepacking-basic/r/ggplot2/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/circlepacking-basic/r/ggplot2/plot-dark.png +preview_html_light: null +preview_html_dark: null +quality_score: 90 +review: + strengths: + - Force-relaxation packing algorithm correctly implements the 3-level hierarchy + (root ⊃ category ⊃ leaf) with area-accurate radii (r = sqrt(value / pi)), matching + the spec's area-encoding requirement precisely. + - 'Repair directly resolved both attempt-1 weaknesses: the top-2 largest leaf circles + are now labeled, and a bold focal ring highlights the single largest file (logo.png) + across the whole hierarchy, giving the chart a clear focal point.' + - 'Imprint palette applied in canonical order with the first categorical series + (Source Code) rendered in #009E73; data colors are pixel-identical between plot-light.png + and plot-dark.png, and chrome is fully theme-adaptive with no dark-on-dark or + light-on-light failures.' + - Angle-aware label anchoring (hjust/vjust derived from the outward direction vector) + keeps every category label clear of its circle and of neighboring labels regardless + of approach angle. + - Data scenario (a repository directory tree with lognormal file-size distributions + per category) is realistic, neutral, and produces genuine size variance and outliers + rather than uniform circles. + - Title format, set.seed(42) reproducibility, and ragg::agg_png output at the correct + square 2400x2400 canvas are all correct. + weaknesses: + - pack_children and circle_points helper functions still deviate from the strict + KISS 'no functions' guidance -- justified by the packing algorithm's genuine complexity + and reuse across two hierarchy levels, but worth watching for further creeping + abstraction. + - The distinctive technique (force-relaxation packing) remains general-purpose math + rather than an R/ggplot2-specific capability; a package-idiomatic circle-drawing + helper could reinforce Library Mastery in a future revision. + - A handful of the smallest leaf circles (e.g. in Config and Documentation) sit + near the lower visibility threshold -- fine at full resolution but worth a minor + size floor if scaled further down for mobile. + image_description: |- + Light render (plot-light.png): + Background: Warm off-white (#FAF8F1), matching the required light-theme page background. The root cluster sits on a very subtly lighter "elevated" circle (#FFFDF6) that encompasses all six category clusters with clear padding, giving faint depth without contradicting the palette rule. + Chrome: Title "circlepacking-basic · r · ggplot2 · anyplot.ai" is bold black text, fully readable, centered at top, well clear of the canvas edges. Six category labels (Assets, Config, Scripts, Tests, Source Code, Documentation) are bold dark-ink text placed outside their respective circles with no overlap onto data or onto each other. + Data: Six category clusters, each colored per the Imprint palette in canonical order (Source Code = #009E73 green, Tests = #C475FD lavender, Documentation = #4467A3 blue, Assets = #BD8233 ochre, Config = #AE3030 red, Scripts = #2ABCCD cyan). Leaf (file) circles within each cluster vary visibly in size, showing area-proportional encoding and genuine size outliers. The two largest leaf circles overall ("logo.png" in Assets, "utils.R" in Source Code) are now individually labeled with bold, contrast-matched text, and the single largest file across the whole hierarchy ("logo.png") is emphasized with a bold dark focal ring -- resolving the attempt-1 note that the storytelling lacked an explicit focal point. + Legibility verdict: PASS + + Dark render (plot-dark.png): + Background: Warm near-black (#1A1A17), matching the required dark-theme page background, with the root cluster on a subtly lighter elevated shade (#242420). + Chrome: Title and all six category labels are rendered in light off-white text, clearly legible against the dark background -- no dark-on-dark failures observed. Category circle outlines switch to a light grey stroke, remaining visible without dominating. The focal ring around "logo.png" switches to a light-ink stroke, staying visible against both the leaf fill and the elevated background. + Data: Colors are confirmed identical to the light render (same six Imprint palette hues) -- only the chrome (background, text, outline colors) flips between themes, exactly as required. Leaf label text colors (dark ink on ochre/red/blue fills, light ink on green/lavender/cyan fills) remain legible and unchanged from the light render since they are data-tied, not theme-tied. + Legibility verdict: PASS + criteria_checklist: + visual_quality: + score: 28 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 7 + max: 8 + passed: true + comment: Title, category labels, and the two new leaf labels use explicit, + size-scaled fonts; all readable in both themes at full and scaled-down size. + - id: VQ-02 + name: No Overlap + score: 6 + max: 6 + passed: true + comment: Angle-aware label anchoring keeps every label clear of circles and + of other labels; new leaf labels and focal ring introduce no collisions. + - id: VQ-03 + name: Element Visibility + score: 5 + max: 6 + passed: true + comment: Circle sizes are area-accurate and well adapted to the value distribution; + a few of the smallest leaf circles are near the lower visibility threshold. + - id: VQ-04 + name: Color Accessibility + score: 2 + max: 2 + passed: true + comment: Six distinct Imprint hues with good luminance separation; no red-green-only + encoding; leaf label ink is chosen per-fill luma for contrast. + - id: VQ-05 + name: Layout & Canvas + score: 4 + max: 4 + passed: true + comment: Composition fills a balanced central portion of the 2400x2400 canvas + with even margins; nothing cut off. + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: No axes needed for this chart type; title fully descriptive and correctly + formatted. + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: 'Canonical Imprint order, first series #009E73, identical data colors + across themes, theme-correct chrome in both renders.' + design_excellence: + score: 17 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 7 + max: 8 + passed: true + comment: Custom packing algorithm, elevated-background depth cue, dynamic + label geometry, and the new focal-ring device go well beyond a configured + default. + - id: DE-02 + name: Visual Refinement + score: 5 + max: 6 + passed: true + comment: theme_void removes all chrome, subtle single-weight outlines, generous + whitespace between clusters. + - id: DE-03 + name: Data Storytelling + score: 5 + max: 6 + passed: true + comment: The new focal ring around the single largest file, plus labels on + the two biggest leaves, now gives the chart an explicit focal point on top + of the existing size/color hierarchy -- directly resolving the attempt-1 + gap. + spec_compliance: + score: 15 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: Correct hierarchical circle-packing chart with root/category/leaf + nesting. + - id: SC-02 + name: Required Features + score: 4 + max: 4 + passed: true + comment: Efficient non-overlapping packing, color-by-category, area scaling, + padded root containment, and now labels on the largest leaf circles are + all present -- closes the attempt-1 gap. + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: Radius derived from sqrt(value/pi) so area (not radius) encodes value; + all nodes visible. + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Title format exact; category identity conveyed via direct labels + instead of a separate legend, which is an acceptable substitute here. + data_quality: + score: 14 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 5 + max: 6 + passed: true + comment: Lognormal per-category size distributions produce real outliers and + varied cluster totals across the hierarchy. + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Software repository directory structure (src/tests/docs/assets/config/scripts) + is a neutral, comprehensible real-world scenario. + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: File-size magnitudes (KB range, lognormal) are plausible for the + domain. + code_quality: + score: 9 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 2 + max: 3 + passed: true + comment: Two helper functions (pack_children, circle_points) deviate from + the strict no-functions guideline, justified by the packing algorithm's + reuse across two hierarchy levels. + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: set.seed(42) present. + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: ggplot2, dplyr, tibble, ragg all actively used; nothing unused. + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: No fake functionality; complexity is proportional to the chart type's + algorithmic requirements. + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: Saves via ggsave/ragg::agg_png as plot-{THEME}.png at the correct + dimensions. + library_mastery: + score: 7 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 4 + max: 5 + passed: true + comment: Idiomatic use of geom_polygon/geom_text/coord_fixed/theme_void/scale_fill_manual + to build a chart type ggplot2 has no native geom for. + - id: LM-02 + name: Distinctive Features + score: 3 + max: 5 + passed: true + comment: dplyr::group_modify-driven per-group packing is a tidy-eval-idiomatic + pattern, though the core packing math itself remains library-agnostic. + verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - annotations + - layer-composition + patterns: + - data-generation + - iteration-over-groups + dataprep: [] + styling: + - minimal-chrome + - alpha-blending