{"spec_id":"windbarb-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' windbarb-basic: Wind Barb Plot for Meteorological Data\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 86/100 | Created: 2026-05-19\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\"\nINK_MUTED   <- if (THEME == \"light\") \"#6B6A63\" else \"#A8A79F\"\nGRID_COLOR  <- if (THEME == \"light\") \"#E4E2DB\" else \"#2F2F2C\"\nIMPRINT   <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n                 \"#AE3030\", \"#2ABCCD\", \"#954477\")\nBARB_COLOR  <- IMPRINT[1]\n\n# --- Wind field data ----------------------------------------------------------\n# 9x7 grid of surface weather stations over eastern North America\n# Simulates a Northern Hemisphere cyclone (counterclockwise circulation)\nnx <- 9; ny <- 7\nlons <- seq(-82, -56, length.out = nx)\nlats <- seq(32, 48, length.out = ny)\nstations <- expand.grid(lon = lons, lat = lats)\n\ncx <- -70; cy <- 41             # low-pressure centre\ndr_lon <- stations$lon - cx\ndr_lat <- stations$lat - cy\nr      <- sqrt(dr_lon^2 + dr_lat^2)\n\n# Rankine vortex: speed rises to r_max then falls off\nr_max   <- 6.0\nmax_spd <- 55                   # knots at peak radius\nspeed_kt <- ifelse(r < r_max,\n                   max_spd * r / r_max,\n                   max_spd * r_max / pmax(r, 0.01))\n\n# CCW rotation (NH cyclone) with 15 % inward convergence\ntan_lon  <- -dr_lat;  tan_lat <- dr_lon\ntan_norm <- pmax(sqrt(tan_lon^2 + tan_lat^2), 0.01)\ninward_lon <- -dr_lon / pmax(r, 0.01)\ninward_lat <- -dr_lat / pmax(r, 0.01)\ninflow  <- 0.15\n\nstations$u <- ((tan_lon / tan_norm) * (1 - inflow) + inward_lon * inflow) * speed_kt\nstations$v <- ((tan_lat / tan_norm) * (1 - inflow) + inward_lat * inflow) * speed_kt\n\n# --- Barb geometry ------------------------------------------------------------\nSTAFF_LEN <- 1.15   # staff length in degrees\nBARB_LEN  <- 0.42   # full barb feather length in degrees\nBARB_SP   <- 0.16   # spacing between barbs along the staff\n\n# Build segment list and pennant polygon list for one station.\n# Standard meteorological barb convention (Northern Hemisphere):\n#   staff  = points FROM which wind blows\n#   barbs  = right of the staff when looking from base to tip\n#   half barb = 5 kt, full barb = 10 kt, pennant (filled triangle) = 50 kt\nmake_barb_geometry <- function(x0, y0, u, v, staff_len, barb_len, barb_sp) {\n  spd <- sqrt(u^2 + v^2)\n  if (is.na(spd) || spd < 2.5) {\n    return(list(segs = NULL, pens = NULL, calm = data.frame(x = x0, y = y0)))\n  }\n\n  ux <- -u / spd;  uy <- -v / spd   # staff unit vector (upwind direction)\n  bx <-  uy;       by <- -ux        # barb unit vector (right of staff, NH)\n\n  segs_list <- list()\n  pen_list  <- list()\n\n  # Staff\n  segs_list[[1]] <- data.frame(\n    x    = x0,                   y    = y0,\n    xend = x0 + staff_len * ux,  yend = y0 + staff_len * uy\n  )\n\n  # Decompose rounded speed into barb counts\n  spd_r <- round(spd / 5) * 5\n  n50   <- spd_r %/% 50;  rem <- spd_r - n50 * 50\n  n10   <- rem   %/% 10;  rem <- rem   - n10 * 10\n  n5    <- rem   %/% 5\n\n  pos <- staff_len   # current position along staff from base, start at tip\n\n  # Pennants (50 kt each) — filled triangles placed first from the tip\n  for (i in seq_len(n50)) {\n    tx  <- x0 + pos * ux;                ty  <- y0 + pos * uy\n    bx2 <- x0 + (pos - barb_sp) * ux;   by2 <- y0 + (pos - barb_sp) * uy\n    px  <- bx2 + barb_len * bx;         py  <- by2 + barb_len * by\n    pen_list[[length(pen_list) + 1]] <- data.frame(\n      x      = c(tx, bx2, px),\n      y      = c(ty, by2, py),\n      pen_id = paste0(round(x0, 3), \"_\", round(y0, 3), \"_p\", i)\n    )\n    pos <- pos - barb_sp\n  }\n\n  # Full barbs (10 kt each)\n  for (i in seq_len(n10)) {\n    ax <- x0 + pos * ux;  ay <- y0 + pos * uy\n    segs_list[[length(segs_list) + 1]] <- data.frame(\n      x    = ax,  y    = ay,\n      xend = ax + barb_len * bx,  yend = ay + barb_len * by\n    )\n    pos <- pos - barb_sp\n  }\n\n  # Half barbs (5 kt each)\n  for (i in seq_len(n5)) {\n    ax <- x0 + pos * ux;  ay <- y0 + pos * uy\n    segs_list[[length(segs_list) + 1]] <- data.frame(\n      x    = ax,  y    = ay,\n      xend = ax + 0.5 * barb_len * bx,  yend = ay + 0.5 * barb_len * by\n    )\n    pos <- pos - barb_sp\n  }\n\n  list(\n    segs = do.call(rbind, segs_list),\n    pens = if (length(pen_list) > 0) do.call(rbind, pen_list) else NULL,\n    calm = NULL\n  )\n}\n\n# Collect geometry for all stations\ngeom_list <- lapply(seq_len(nrow(stations)), function(i) {\n  make_barb_geometry(stations$lon[i], stations$lat[i],\n                     stations$u[i], stations$v[i],\n                     STAFF_LEN, BARB_LEN, BARB_SP)\n})\n\nbarb_segs <- do.call(rbind, lapply(geom_list, function(g) g$segs))\npennants  <- do.call(rbind, lapply(geom_list, function(g) g$pens))\ncalm_pts  <- do.call(rbind, lapply(geom_list, function(g) g$calm))\nstation_pts <- data.frame(x = stations$lon, y = stations$lat)\n\n# --- Plot --------------------------------------------------------------------\np <- ggplot() +\n  geom_point(data = station_pts, aes(x = x, y = y),\n             color = BARB_COLOR, size = 1.4, alpha = 0.9) +\n  geom_segment(data = barb_segs,\n               aes(x = x, y = y, xend = xend, yend = yend),\n               color = BARB_COLOR, linewidth = 1.2, lineend = \"round\")\n\nif (!is.null(pennants) && nrow(pennants) > 0) {\n  p <- p + geom_polygon(data = pennants, aes(x = x, y = y, group = pen_id),\n                         fill = BARB_COLOR, color = NA)\n}\n\nif (!is.null(calm_pts) && nrow(calm_pts) > 0) {\n  p <- p + geom_point(data = calm_pts, aes(x = x, y = y),\n                       shape = 1, color = BARB_COLOR, size = 5, stroke = 1.5)\n}\n\np <- p +\n  annotate(\"text\", x = cx, y = cy, label = \"L\",\n           color = INK, size = 10, fontface = \"bold\") +\n  scale_x_continuous(breaks = seq(-80, -60, by = 5),\n                     labels = function(x) paste0(abs(x), \"°W\")) +\n  scale_y_continuous(breaks = seq(35, 45, by = 5),\n                     labels = function(y) paste0(y, \"°N\")) +\n  coord_cartesian(xlim = c(-84, -54), ylim = c(30, 50)) +\n  labs(\n    title    = paste0(\"Surface Winds · windbarb-basic · r · ggplot2 · anyplot.ai\"),\n    subtitle = \"Simulated NH cyclone — half barb = 5 kt  ·  full barb = 10 kt  ·  pennant = 50 kt\",\n    x        = \"Longitude\",\n    y        = \"Latitude\"\n  ) +\n  theme_minimal(base_size = 14) +\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 = element_line(color = GRID_COLOR, linewidth = 0.3),\n    panel.grid.minor = element_blank(),\n    panel.border     = element_blank(),\n    axis.line        = element_line(color = INK_SOFT, linewidth = 0.4),\n    axis.ticks       = element_line(color = INK_SOFT, linewidth = 0.3),\n    axis.title       = element_text(color = INK,       size = 20),\n    axis.text        = element_text(color = INK_SOFT,  size = 16),\n    plot.title       = element_text(color = INK,       size = 22, face = \"bold\"),\n    plot.subtitle    = element_text(color = INK_MUTED, size = 16),\n    plot.margin      = margin(t = 20, r = 20, b = 15, l = 15)\n  )\n\n# --- Save --------------------------------------------------------------------\nggsave(\n  filename = sprintf(\"plot-%s.png\", THEME),\n  plot     = p,\n  device   = ragg::agg_png,\n  width    = 16,\n  height   = 9,\n  units    = \"in\",\n  dpi      = 300\n)\n"}