{"spec_id":"ternary-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' ternary-basic: Basic Ternary Plot\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 92/100 | Created: 2026-08-04\n\nlibrary(ggplot2)\nlibrary(dplyr)\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\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\nIMPRINT_PALETTE <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n                     \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\nBRAND <- IMPRINT_PALETTE[1]\n\n# --- Data: soil texture samples (sand / silt / clay, % sum to 100) ------\nn_samples <- 90\nsand_raw <- -log(runif(n_samples))\nsilt_raw <- -log(runif(n_samples))\nclay_raw <- -log(runif(n_samples))\ntotal_raw <- sand_raw + silt_raw + clay_raw\n\nsoil <- tibble(\n  sand = sand_raw / total_raw,\n  silt = silt_raw / total_raw,\n  clay = clay_raw / total_raw\n) %>%\n  mutate(\n    x = clay + 0.5 * sand,\n    y = sand * sqrt(3) / 2,\n    balance_dist = sqrt((sand - 1 / 3)^2 + (silt - 1 / 3)^2 + (clay - 1 / 3)^2)\n  )\n\n# Focal point: the sample closest to an even 1/3-1/3-1/3 split (\"balanced loam\")\nbalanced_sample <- soil %>% slice_min(balance_dist, n = 1)\n\n# --- Ternary scaffold: triangle border, grid lines, edge ticks ----------\n# Barycentric layout: sand -> top vertex, silt -> bottom-left, clay -> bottom-right\nvertices <- tibble(\n  x     = c(0.5, 0, 1),\n  y     = c(sqrt(3) / 2, 0, 0),\n  label = c(\"Sand\", \"Silt\", \"Clay\")\n)\ntriangle_outline <- bind_rows(vertices, vertices[1, ])\n\ngrid_fracs <- seq(0.2, 0.8, by = 0.2)\n\ngrid_lines <- bind_rows(\n  # constant sand (parallel to the Silt-Clay bottom edge)\n  tibble(\n    x    = 0.5 * grid_fracs,\n    y    = grid_fracs * sqrt(3) / 2,\n    xend = 1 - 0.5 * grid_fracs,\n    yend = grid_fracs * sqrt(3) / 2\n  ),\n  # constant silt (parallel to the Sand-Clay edge)\n  tibble(\n    x    = 0.5 * (1 - grid_fracs),\n    y    = (1 - grid_fracs) * sqrt(3) / 2,\n    xend = 1 - grid_fracs,\n    yend = 0\n  ),\n  # constant clay (parallel to the Sand-Silt edge)\n  tibble(\n    x    = 0.5 + 0.5 * grid_fracs,\n    y    = (1 - grid_fracs) * sqrt(3) / 2,\n    xend = grid_fracs,\n    yend = 0\n  )\n)\n\ntick_len <- 0.035\nedge_ticks <- bind_rows(\n  # bottom edge (Silt-Clay), ticks point straight down\n  tibble(x = grid_fracs, y = 0, xend = grid_fracs, yend = -tick_len),\n  # left edge (Sand-Silt), ticks point out to the upper-left\n  tibble(\n    x    = 0.5 * grid_fracs, y = grid_fracs * sqrt(3) / 2,\n    xend = 0.5 * grid_fracs - tick_len * sqrt(3) / 2,\n    yend = grid_fracs * sqrt(3) / 2 + tick_len * 0.5\n  ),\n  # right edge (Sand-Clay), ticks point out to the upper-right\n  tibble(\n    x    = 1 - 0.5 * grid_fracs, y = grid_fracs * sqrt(3) / 2,\n    xend = 1 - 0.5 * grid_fracs + tick_len * sqrt(3) / 2,\n    yend = grid_fracs * sqrt(3) / 2 + tick_len * 0.5\n  )\n)\n\n# Percentage labels beyond each tick — one component scale per edge, each\n# verified against the barycentric mapping so every edge reads a different,\n# non-redundant value: bottom = Silt (x = clay when sand = 0, so silt = 1-x),\n# left = Sand (y = sand*sqrt(3)/2 everywhere), right = Clay (silt = 0, so\n# clay = 1 - sand there).\nlabel_gap <- 0.045\nedge_labels <- bind_rows(\n  tibble(\n    x = grid_fracs, y = -tick_len - label_gap,\n    label = paste0(round((1 - grid_fracs) * 100), \"%\")\n  ),\n  tibble(\n    x = 0.5 * grid_fracs - (tick_len + label_gap) * sqrt(3) / 2,\n    y = grid_fracs * sqrt(3) / 2 + (tick_len + label_gap) * 0.5,\n    label = paste0(round(grid_fracs * 100), \"%\")\n  ),\n  tibble(\n    x = 1 - 0.5 * grid_fracs + (tick_len + label_gap) * sqrt(3) / 2,\n    y = grid_fracs * sqrt(3) / 2 + (tick_len + label_gap) * 0.5,\n    label = paste0(round((1 - grid_fracs) * 100), \"%\")\n  )\n)\n\n# --- Title (fontsize scales down for titles longer than the 67-char baseline)\nplot_title <- \"Soil Texture Composition · ternary-basic · r · ggplot2 · anyplot.ai\"\ntitle_ratio <- if (nchar(plot_title) > 67) 67 / nchar(plot_title) else 1.0\ntitle_size <- max(8, round(12 * title_ratio))\n\n# --- Plot -----------------------------------------------------------------\np <- ggplot() +\n  geom_segment(\n    data = grid_lines, aes(x = x, y = y, xend = xend, yend = yend),\n    color = INK, alpha = 0.2, linewidth = 0.3\n  ) +\n  geom_segment(\n    data = edge_ticks, aes(x = x, y = y, xend = xend, yend = yend),\n    color = INK_SOFT, alpha = 0.85, linewidth = 0.55\n  ) +\n  geom_text(\n    data = edge_labels, aes(x = x, y = y, label = label),\n    color = INK_SOFT, size = 2.3, alpha = 0.95\n  ) +\n  geom_path(\n    data = triangle_outline, aes(x = x, y = y),\n    color = INK_SOFT, linewidth = 0.7\n  ) +\n  geom_point(\n    data = soil, aes(x = x, y = y),\n    color = BRAND, size = 2.5, alpha = 0.75\n  ) +\n  geom_point(\n    data = balanced_sample, aes(x = x, y = y),\n    color = IMPRINT_PALETTE[4], fill = IMPRINT_PALETTE[4],\n    shape = 21, size = 5, stroke = 1\n  ) +\n  geom_label(\n    data = balanced_sample,\n    aes(x = x + 0.1, y = y + 0.03, label = \"Balanced loam\"),\n    color = IMPRINT_PALETTE[4], fill = PAGE_BG, label.size = 0,\n    size = 2.8, fontface = \"bold\", hjust = 0\n  ) +\n  geom_text(\n    data = vertices %>% mutate(\n      y_offset = y + c(0.06, -0.05, -0.05),\n      x_offset = x + c(0, -0.03, 0.03)\n    ),\n    aes(x = x_offset, y = y_offset, label = label),\n    color = INK, size = 4, fontface = \"bold\"\n  ) +\n  labs(title = plot_title) +\n  coord_fixed(ratio = 1, xlim = c(-0.15, 1.15), ylim = c(-0.14, 1.02), expand = FALSE) +\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(\n      color = INK, size = title_size, face = \"plain\",\n      hjust = 0.5, margin = margin(b = 12)\n    ),\n    plot.margin = margin(t = 16, r = 16, b = 8, l = 16)\n  )\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"}