{"spec_id":"network-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' network-basic: Basic Network Graph\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 87/100 | Created: 2026-07-24\n\nlibrary(ggplot2)\nlibrary(dplyr)\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\"\nIMPRINT_PALETTE <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n                     \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# --- Data: friendship network across three social circles ---------------\nnames <- c(\n  \"Ava\", \"Liam\", \"Mia\", \"Noah\", \"Zoe\", \"Ethan\",\n  \"Lily\", \"Mason\", \"Grace\", \"Owen\", \"Ella\", \"Lucas\",\n  \"Nora\", \"Jack\", \"Ruby\", \"Leo\", \"Ivy\", \"Felix\", \"Maya\", \"Theo\"\n)\n# Each id belongs to one of three friend circles; a handful of cross-circle\n# edges below bridge them, matching the spec's \"identify communities\" use case.\ngroup <- c(rep(\"Circle A\", 6), rep(\"Circle B\", 6), rep(\"Circle C\", 8))\nnodes <- tibble::tibble(id = seq_along(names), name = names, group = group)\n\nedges <- tibble::tibble(\n  from = c(1, 1, 2, 2, 3, 4, 4, 1,\n           7, 7, 8, 8, 9, 10, 10, 7,\n           13, 13, 14, 14, 15, 16, 16, 17, 18, 18, 19, 13,\n           4, 6, 10),\n  to   = c(2, 3, 3, 4, 5, 5, 6, 6,\n           8, 9, 9, 10, 11, 11, 12, 12,\n           14, 15, 15, 16, 17, 17, 18, 19, 19, 20, 20, 20,\n           8, 13, 16),\n  # Friendship strength: within-circle ties run stronger than the sparse\n  # cross-circle bridges (last 3 edges), giving edges a second data dimension.\n  weight = c(4, 3, 4, 3, 5, 3, 4, 3,\n             3, 4, 5, 3, 4, 3, 4, 3,\n             4, 3, 5, 3, 4, 3, 4, 3, 4, 3, 5, 3,\n             1, 1, 1)\n)\n\nn_nodes <- nrow(nodes)\nedge_i  <- edges$from\nedge_j  <- edges$to\n\n# --- Force-directed layout (Fruchterman-Reingold) ------------------------\n# Positions start random, then repulsion (all pairs) and attraction (edges)\n# iterate under a cooling schedule until the network settles into a\n# readable, cluster-revealing arrangement. No layout package required.\narea <- 4\nk    <- sqrt(area / n_nodes)\ntemp <- 0.15\niterations <- 300\n\npos <- tibble::tibble(x = runif(n_nodes, -1, 1), y = runif(n_nodes, -1, 1))\n\nfor (iter in seq_len(iterations)) {\n  dx <- outer(pos$x, pos$x, \"-\")\n  dy <- outer(pos$y, pos$y, \"-\")\n  dist_mat <- sqrt(dx^2 + dy^2)\n  diag(dist_mat) <- 1\n\n  rep_force <- (k^2) / dist_mat\n  fx <- rowSums(rep_force * dx / dist_mat)\n  fy <- rowSums(rep_force * dy / dist_mat)\n\n  edx <- pos$x[edge_i] - pos$x[edge_j]\n  edy <- pos$y[edge_i] - pos$y[edge_j]\n  edist <- pmax(sqrt(edx^2 + edy^2), 0.01)\n  att_force <- (edist^2) / k\n  fax <- att_force * edx / edist\n  fay <- att_force * edy / edist\n\n  for (e in seq_along(edge_i)) {\n    fx[edge_i[e]] <- fx[edge_i[e]] - fax[e]\n    fy[edge_i[e]] <- fy[edge_i[e]] - fay[e]\n    fx[edge_j[e]] <- fx[edge_j[e]] + fax[e]\n    fy[edge_j[e]] <- fy[edge_j[e]] + fay[e]\n  }\n\n  disp <- pmax(sqrt(fx^2 + fy^2), 0.01)\n  step <- pmin(disp, temp)\n  pos$x <- pos$x + (fx / disp) * step\n  pos$y <- pos$y + (fy / disp) * step\n\n  temp <- temp * 0.99\n}\n\n# Rescale to a fixed coordinate span so text nudges/sizes stay predictable\nspan <- max(abs(c(pos$x, pos$y)))\nnodes$x <- pos$x / span * 5\nnodes$y <- pos$y / span * 5\n\n# Degree = number of connections per node (drives node size)\ndegree <- tabulate(c(edge_i, edge_j), nbins = n_nodes)\nnodes$degree <- degree\n\nedges_pos <- edges %>%\n  left_join(nodes %>% select(id, x, y), by = c(\"from\" = \"id\")) %>%\n  rename(x_from = x, y_from = y) %>%\n  left_join(nodes %>% select(id, x, y), by = c(\"to\" = \"id\")) %>%\n  rename(x_to = x, y_to = y)\n\n# --- Plot -----------------------------------------------------------------\nanyplot_theme <- 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         = element_blank(),\n    axis.ticks        = element_blank(),\n    legend.position   = \"bottom\",\n    legend.title      = element_text(color = INK, size = 8),\n    legend.text       = element_text(color = INK_SOFT, size = 8),\n    legend.key        = element_rect(fill = PAGE_BG, color = NA),\n    legend.background = element_rect(fill = PAGE_BG, color = NA),\n    legend.margin     = margin(t = -4),\n    plot.title        = element_text(color = INK, size = 12, hjust = 0.5),\n    plot.caption      = element_text(color = INK_SOFT, size = 7, hjust = 0.5, margin = margin(t = 6)),\n    plot.margin       = margin(14, 14, 10, 14, \"pt\")\n  )\n\np <- ggplot() +\n  geom_segment(\n    data = edges_pos,\n    aes(x = x_from, y = y_from, xend = x_to, yend = y_to, linewidth = weight),\n    color = INK_SOFT, alpha = 0.45, lineend = \"round\"\n  ) +\n  geom_point(\n    data = nodes,\n    aes(x = x, y = y, size = degree, fill = group),\n    shape = 21, color = INK_SOFT, stroke = 0.4, alpha = 0.95\n  ) +\n  geom_text(\n    data = nodes,\n    aes(x = x, y = y, label = name),\n    color = INK, size = 3.1, nudge_y = -0.42\n  ) +\n  scale_size(range = c(3.5, 7), guide = \"none\") +\n  scale_linewidth(range = c(0.3, 1.0), guide = \"none\") +\n  scale_fill_manual(values = IMPRINT_PALETTE[1:3], name = \"Friend group\") +\n  coord_equal(clip = \"off\") +\n  labs(\n    title   = \"network-basic · r · ggplot2 · anyplot.ai\",\n    caption = \"Node size = number of friendships  ·  edge thickness = friendship strength\"\n  ) +\n  guides(fill = guide_legend(override.aes = list(size = 4))) +\n  anyplot_theme\n\n# --- Save -------------------------------------------------------------------\nggsave(\n  filename = sprintf(\"plot-%s.png\", THEME),\n  plot     = p,\n  device   = ragg::agg_png,\n  width    = 6,\n  height   = 6,\n  units    = \"in\",\n  dpi      = 400\n)\n"}