A ternary plot displays three-component compositional data on an equilateral triangle where each vertex represents 100% of one component. Points inside the triangle show compositions that sum to a constant total (usually 100%), with position indicating relative proportions. This visualization is essential for data where three variables are interdependent and constrained to sum to a fixed value.

#' anyplot.ai
#' ternary-basic: Basic Ternary Plot
#' Library: ggplot2 3.5.1 | R 4.4.1
#' Quality: 92/100 | Created: 2026-08-04
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"
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")
BRAND <- IMPRINT_PALETTE[1]
# --- Data: soil texture samples (sand / silt / clay, % sum to 100) ------
n_samples <- 90
sand_raw <- -log(runif(n_samples))
silt_raw <- -log(runif(n_samples))
clay_raw <- -log(runif(n_samples))
total_raw <- sand_raw + silt_raw + clay_raw
soil <- tibble(
sand = sand_raw / total_raw,
silt = silt_raw / total_raw,
clay = clay_raw / total_raw
) %>%
mutate(
x = clay + 0.5 * sand,
y = sand * sqrt(3) / 2,
balance_dist = sqrt((sand - 1 / 3)^2 + (silt - 1 / 3)^2 + (clay - 1 / 3)^2)
)
# Focal point: the sample closest to an even 1/3-1/3-1/3 split ("balanced loam")
balanced_sample <- soil %>% slice_min(balance_dist, n = 1)
# --- Ternary scaffold: triangle border, grid lines, edge ticks ----------
# Barycentric layout: sand -> top vertex, silt -> bottom-left, clay -> bottom-right
vertices <- tibble(
x = c(0.5, 0, 1),
y = c(sqrt(3) / 2, 0, 0),
label = c("Sand", "Silt", "Clay")
)
triangle_outline <- bind_rows(vertices, vertices[1, ])
grid_fracs <- seq(0.2, 0.8, by = 0.2)
grid_lines <- bind_rows(
# constant sand (parallel to the Silt-Clay bottom edge)
tibble(
x = 0.5 * grid_fracs,
y = grid_fracs * sqrt(3) / 2,
xend = 1 - 0.5 * grid_fracs,
yend = grid_fracs * sqrt(3) / 2
),
# constant silt (parallel to the Sand-Clay edge)
tibble(
x = 0.5 * (1 - grid_fracs),
y = (1 - grid_fracs) * sqrt(3) / 2,
xend = 1 - grid_fracs,
yend = 0
),
# constant clay (parallel to the Sand-Silt edge)
tibble(
x = 0.5 + 0.5 * grid_fracs,
y = (1 - grid_fracs) * sqrt(3) / 2,
xend = grid_fracs,
yend = 0
)
)
tick_len <- 0.035
edge_ticks <- bind_rows(
# bottom edge (Silt-Clay), ticks point straight down
tibble(x = grid_fracs, y = 0, xend = grid_fracs, yend = -tick_len),
# left edge (Sand-Silt), ticks point out to the upper-left
tibble(
x = 0.5 * grid_fracs, y = grid_fracs * sqrt(3) / 2,
xend = 0.5 * grid_fracs - tick_len * sqrt(3) / 2,
yend = grid_fracs * sqrt(3) / 2 + tick_len * 0.5
),
# right edge (Sand-Clay), ticks point out to the upper-right
tibble(
x = 1 - 0.5 * grid_fracs, y = grid_fracs * sqrt(3) / 2,
xend = 1 - 0.5 * grid_fracs + tick_len * sqrt(3) / 2,
yend = grid_fracs * sqrt(3) / 2 + tick_len * 0.5
)
)
# Percentage labels beyond each tick — one component scale per edge, each
# verified against the barycentric mapping so every edge reads a different,
# non-redundant value: bottom = Silt (x = clay when sand = 0, so silt = 1-x),
# left = Sand (y = sand*sqrt(3)/2 everywhere), right = Clay (silt = 0, so
# clay = 1 - sand there).
label_gap <- 0.045
edge_labels <- bind_rows(
tibble(
x = grid_fracs, y = -tick_len - label_gap,
label = paste0(round((1 - grid_fracs) * 100), "%")
),
tibble(
x = 0.5 * grid_fracs - (tick_len + label_gap) * sqrt(3) / 2,
y = grid_fracs * sqrt(3) / 2 + (tick_len + label_gap) * 0.5,
label = paste0(round(grid_fracs * 100), "%")
),
tibble(
x = 1 - 0.5 * grid_fracs + (tick_len + label_gap) * sqrt(3) / 2,
y = grid_fracs * sqrt(3) / 2 + (tick_len + label_gap) * 0.5,
label = paste0(round((1 - grid_fracs) * 100), "%")
)
)
# --- Title (fontsize scales down for titles longer than the 67-char baseline)
plot_title <- "Soil Texture Composition · ternary-basic · r · ggplot2 · anyplot.ai"
title_ratio <- if (nchar(plot_title) > 67) 67 / nchar(plot_title) else 1.0
title_size <- max(8, round(12 * title_ratio))
# --- Plot -----------------------------------------------------------------
p <- ggplot() +
geom_segment(
data = grid_lines, aes(x = x, y = y, xend = xend, yend = yend),
color = INK, alpha = 0.2, linewidth = 0.3
) +
geom_segment(
data = edge_ticks, aes(x = x, y = y, xend = xend, yend = yend),
color = INK_SOFT, alpha = 0.85, linewidth = 0.55
) +
geom_text(
data = edge_labels, aes(x = x, y = y, label = label),
color = INK_SOFT, size = 2.3, alpha = 0.95
) +
geom_path(
data = triangle_outline, aes(x = x, y = y),
color = INK_SOFT, linewidth = 0.7
) +
geom_point(
data = soil, aes(x = x, y = y),
color = BRAND, size = 2.5, alpha = 0.75
) +
geom_point(
data = balanced_sample, aes(x = x, y = y),
color = IMPRINT_PALETTE[4], fill = IMPRINT_PALETTE[4],
shape = 21, size = 5, stroke = 1
) +
geom_label(
data = balanced_sample,
aes(x = x + 0.1, y = y + 0.03, label = "Balanced loam"),
color = IMPRINT_PALETTE[4], fill = PAGE_BG, label.size = 0,
size = 2.8, fontface = "bold", hjust = 0
) +
geom_text(
data = vertices %>% mutate(
y_offset = y + c(0.06, -0.05, -0.05),
x_offset = x + c(0, -0.03, 0.03)
),
aes(x = x_offset, y = y_offset, label = label),
color = INK, size = 4, fontface = "bold"
) +
labs(title = plot_title) +
coord_fixed(ratio = 1, xlim = c(-0.15, 1.15), ylim = c(-0.14, 1.02), expand = FALSE) +
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_size, face = "plain",
hjust = 0.5, margin = margin(b = 12)
),
plot.margin = margin(t = 16, r = 16, b = 8, l = 16)
)
# --- Save -------------------------------------------------------------------
ggsave(
filename = sprintf("plot-%s.png", THEME),
plot = p,
device = ragg::agg_png,
width = 6,
height = 6,
units = "in",
dpi = 400
)
Part of Basic Ternary Plot on anyplot.ai.