{"spec_id":"network-bipartite","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' network-bipartite: Bipartite Network Graph\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 92/100 | Created: 2026-09-05\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(tidyr)\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\"\nIMPRINT_PALETTE <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n                      \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# --- Data --------------------------------------------------------------------\n# Student-course enrollment: which students registered in which courses,\n# with attendance rate as the edge weight.\nstudents <- sprintf(\"Student %02d\", 1:16)\ncourses <- c(\"Calculus I\", \"Linear Algebra\", \"Data Structures\", \"Organic Chemistry\",\n             \"Cell Biology\", \"Macroeconomics\", \"Art History\", \"Statistics\",\n             \"Physics I\", \"World History\")\n\nedges <- bind_rows(lapply(students, function(s) {\n  chosen <- sample(courses, sample(3:5, 1))\n  tibble(source = s, target = chosen, weight = round(runif(length(chosen), 0.5, 1.0), 2))\n}))\n\nstudent_degree <- tibble(node = students) %>%\n  left_join(count(edges, source, name = \"degree\"), by = c(\"node\" = \"source\")) %>%\n  mutate(degree = replace_na(degree, 0))\n\ncourse_degree <- tibble(node = courses) %>%\n  left_join(count(edges, target, name = \"degree\"), by = c(\"node\" = \"target\")) %>%\n  mutate(degree = replace_na(degree, 0))\n\n# Two fixed columns, nodes ordered by degree so hubs cluster near the top.\nstudents_pos <- student_degree %>%\n  arrange(desc(degree), node) %>%\n  mutate(x = 0, y = seq(1, 0, length.out = n()), set = \"Students\")\n\ncourses_pos <- course_degree %>%\n  arrange(desc(degree), node) %>%\n  mutate(x = 1, y = seq(1, 0, length.out = n()), set = \"Courses\")\n\nnodes <- bind_rows(students_pos, courses_pos)\n\nedge_coords <- edges %>%\n  left_join(students_pos %>% select(node, x, y), by = c(\"source\" = \"node\")) %>%\n  rename(x_start = x, y_start = y) %>%\n  left_join(courses_pos %>% select(node, x, y), by = c(\"target\" = \"node\")) %>%\n  rename(x_end = x, y_end = y)\n\n# --- Plot ----------------------------------------------------------------\np <- ggplot() +\n  geom_curve(\n    data = edge_coords,\n    aes(x = x_start, y = y_start, xend = x_end, yend = y_end, alpha = weight),\n    color = INK_MUTED, linewidth = 0.35, curvature = 0.25, ncp = 8\n  ) +\n  geom_point(data = nodes, aes(x = x, y = y, size = degree, color = set)) +\n  geom_text(\n    data = students_pos, aes(x = x, y = y, label = node),\n    hjust = 1, nudge_x = -0.04, size = 3.1, color = INK\n  ) +\n  geom_text(\n    data = courses_pos, aes(x = x, y = y, label = node),\n    hjust = 0, nudge_x = 0.04, size = 3.1, color = INK\n  ) +\n  scale_color_manual(values = c(\"Students\" = IMPRINT_PALETTE[1], \"Courses\" = IMPRINT_PALETTE[2]),\n                      name = NULL) +\n  scale_size_continuous(range = c(3, 9), guide = \"none\") +\n  scale_alpha_continuous(range = c(0.12, 0.75), guide = \"none\") +\n  coord_cartesian(xlim = c(-0.55, 1.55), ylim = c(-0.05, 1.05), clip = \"off\") +\n  labs(\n    title = \"network-bipartite · r · ggplot2 · anyplot.ai\",\n    caption = \"Node size encodes number of connections; edge opacity encodes attendance rate\"\n  ) +\n  guides(color = guide_legend(override.aes = list(size = 5))) +\n  theme_void(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    plot.title       = element_text(color = INK, size = 15, face = \"bold\", hjust = 0.5, margin = margin(b = 8)),\n    plot.caption     = element_text(color = INK_MUTED, size = 8, hjust = 0.5, margin = margin(t = 10)),\n    legend.position  = \"top\",\n    legend.text      = element_text(color = INK_SOFT, size = 8),\n    legend.key       = element_rect(fill = PAGE_BG, color = NA),\n    plot.margin      = margin(t = 20, r = 50, b = 20, l = 50)\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"}