Extracting and Padding Field Trial Layouts

padTrial() and plot_padTrial()

Author

biomAid package

Published

April 27, 2026


Overview

Field trials commonly contain a mix of plot types — test lines (e.g. DH or breeding lines), check varieties, border plots, and guard rows — arranged across one or more blocks. Before fitting a spatial model it is often necessary to isolate the rectangular sub-grid occupied by the target plot type and ensure that every grid position within that rectangle is represented in the data frame, even if some plots are physically absent.

padTrial() automates this preparation in four steps, applied independently within each group (block):

  1. Duplicate check — stops with an informative error if any Row × Column position appears more than once within a group.
  2. Bounding box — identifies the minimum and maximum row and column coordinates of the target plot type (match) to define the extraction rectangle.
  3. Subset — retains only those rows that fall inside the bounding box, dropping guard rows and checks that lie outside it.
  4. Pad — detects any Row × Column cells absent from the bounding box and inserts placeholder rows for them, writing fill_value into character/factor columns and NA into numeric columns.

An add column flags every row as "old" (original) or "new" (inserted).

plot_padTrial() visualises the result as a Before / After tile map, with inserted cells shown in light grey.


Argument reference

padTrial()

Argument Default Description
data Input data frame containing the trial layout
pattern "Row:Column" Colon-separated names of the two spatial coordinate columns
match "DH" Plot type value(s) that define the target sub-trial
split "Block" Column(s) defining independent processing groups; NULL = whole dataset
pad TRUE Insert placeholder rows for missing grid cells
keep split Columns whose values are carried into padded rows
fill_value "Blank" String written into character/factor columns of padded rows
type_col "Type" Name of the plot-type column
verbose FALSE Print per-group bounding box and padding summary

plot_padTrial()

Argument Default Description
result Data frame returned by padTrial()
data NULL Original data; when supplied the Before panel includes guard rows
type_col "Type" Column used to colour tiles
pattern "Row:Column" Colon-separated spatial coordinate column names
split NULL Grouping column(s) — groups appear as facet columns
label NULL Column whose values are printed inside tiles
theme theme_bw() ggplot2 theme
return_data FALSE Return the tidy data frame instead of the plot

Example 1: Basic rectangular block with guard rows

The simplest case — a single rectangular block of test lines (DH) enclosed by guard rows on all four sides. padTrial() detects the bounding box of the DH plots, drops the guards, and (in this complete case) inserts no padding.

# DH core: rows 3-8, columns 2-7  (6 rows × 6 cols = 36 DH plots)
# Guard border: row 1-2, row 9-10, col 1, col 8  (all surrounding cells)
set.seed(1L)
rows <- 1:10
cols <- 1:8

ex1 <- expand.grid(Row = rows, Column = cols, KEEP.OUT.ATTRS = FALSE)
ex1$Type  <- "Guard"
ex1$Block <- "B1"
ex1$Geno  <- NA_character_

# Assign DH plots to the core
core <- ex1$Row >= 3L & ex1$Row <= 8L & ex1$Column >= 2L & ex1$Column <= 7L
ex1$Type[core] <- "DH"
ex1$Geno[core] <- paste0("G", sprintf("%02d", seq_len(sum(core))))
res1 <- padTrial(
  ex1,
  pattern  = "Row:Column",
  match    = "DH",
  split    = "Block",
  verbose  = TRUE
)

table(res1$add)

old 
 36 

The Before panel (left, with data = supplied) shows the full layout including guard rows. The After panel shows just the extracted DH core — no padding was needed because the block was complete.

p1 <- plot_padTrial(res1, data = ex1, split = "Block",
                    type_col = "Type", pattern = "Row:Column")
print(p1)


Example 2: Block with internal missing plots

Real trials frequently have plots that could not be sown, were lost to damage, or were deliberately left empty. Here several DH plots are absent from the interior of the block. padTrial() detects the gaps and inserts "Blank" placeholder rows so the grid is complete and rectangular — a requirement for most spatial modelling software.

set.seed(2L)
ex2 <- expand.grid(Row = 1:8, Column = 1:6, KEEP.OUT.ATTRS = FALSE)
ex2$Type  <- "DH"
ex2$Block <- "B1"
ex2$Geno  <- paste0("G", sprintf("%03d", seq_len(nrow(ex2))))

# Remove 6 interior plots to simulate missing observations
missing_idx <- which(
  (ex2$Row == 2L & ex2$Column == 3L) |
  (ex2$Row == 4L & ex2$Column == 2L) |
  (ex2$Row == 4L & ex2$Column == 5L) |
  (ex2$Row == 5L & ex2$Column == 4L) |
  (ex2$Row == 6L & ex2$Column == 1L) |
  (ex2$Row == 7L & ex2$Column == 6L)
)
ex2 <- ex2[-missing_idx, ]

cat("Rows before padding:", nrow(ex2),
    "\nExpected after padding:", 8L * 6L, "\n")
Rows before padding: 42 
Expected after padding: 48 
res2 <- padTrial(
  ex2,
  match      = "DH",
  split      = "Block",
  fill_value = "Blank",
  verbose    = TRUE
)

cat("'new' rows inserted:", sum(res2$add == "new"), "\n")
'new' rows inserted: 6 
subset(res2, add == "new")[, c("Row", "Column", "Type", "Geno", "add")]
   Row Column  Type  Geno add
45   2      3 Blank Blank new
44   4      2 Blank Blank new
47   4      5 Blank Blank new
46   5      4 Blank Blank new
43   6      1 Blank Blank new
48   7      6 Blank Blank new

The Before panel shows the six white gaps where plots are missing. The After panel shows the completed grid with inserted cells in light grey.

p2 <- plot_padTrial(res2, data = ex2, split = "Block")
print(p2)


Example 3: Non-rectangular block

Not all trial blocks are perfect rectangles. A common scenario is an end-of-field block where some rows are shorter than others — the block is trapezoidal or L-shaped. padTrial() computes the bounding box of all target plots and pads the missing corner and edge positions to produce a rectangular grid.

Here we construct a block shaped like a staircase: each successive pair of rows has one fewer column on the right-hand side.

# Staircase layout:
#   Rows 1-2: columns 1-8
#   Rows 3-4: columns 1-6  (right 2 cols absent)
#   Rows 5-6: columns 1-4  (right 4 cols absent)
#   Rows 7-8: columns 1-2  (right 6 cols absent)

make_staircase <- function() {
  parts <- list(
    expand.grid(Row = 1:2, Column = 1:8),
    expand.grid(Row = 3:4, Column = 1:6),
    expand.grid(Row = 5:6, Column = 1:4),
    expand.grid(Row = 7:8, Column = 1:2)
  )
  d        <- do.call(rbind, parts)
  d$Type   <- "DH"
  d$Block  <- "B1"
  d$Geno   <- paste0("G", sprintf("%03d", seq_len(nrow(d))))
  d
}

ex3 <- make_staircase()
cat("Original rows:", nrow(ex3),
    "\nBounding box would be 8 rows × 8 cols =", 8L * 8L, "cells\n")
Original rows: 40 
Bounding box would be 8 rows × 8 cols = 64 cells
res3 <- padTrial(
  ex3,
  match      = "DH",
  split      = "Block",
  fill_value = "Blank",
  verbose    = TRUE
)

cat("'new' rows inserted:", sum(res3$add == "new"), "\n")
'new' rows inserted: 24 

The Before panel clearly shows the staircase shape — the missing corner positions appear as white space. The After panel fills them all in with "Blank" placeholder tiles (light grey) to produce a complete 8 × 8 grid.

p3 <- plot_padTrial(res3, data = ex3, split = "Block")
print(p3)


Example 4: Multiple blocks — each processed independently

When a trial has multiple blocks, split ensures that each block is processed with its own bounding box. This is critical: blocks often occupy different row/column ranges and have different numbers of missing plots. Mixing them into a single pass would produce the wrong bounding box.

Here we build three blocks that differ in both shape and completeness:

  • Block B1 — complete 5 × 4 rectangle, no padding needed
  • Block B2 — 6 × 5 rectangle with 4 internal missing plots
  • Block B3 — non-rectangular (right-truncated), 5 rows × up to 6 cols
set.seed(4L)

# B1: complete 5×4
b1 <- expand.grid(Row = 1:5, Column = 1:4, KEEP.OUT.ATTRS = FALSE)
b1$Type  <- "DH"
b1$Block <- "B1"
b1$Geno  <- paste0("G", sprintf("%03d", seq_len(nrow(b1))))

# B2: 6×5 with 4 missing interior plots
b2 <- expand.grid(Row = 1:6, Column = 1:5, KEEP.OUT.ATTRS = FALSE)
b2$Type  <- "DH"
b2$Block <- "B2"
b2$Geno  <- paste0("G", sprintf("%03d", seq_len(nrow(b2))))
miss_b2  <- which(
  (b2$Row == 2L & b2$Column == 4L) |
  (b2$Row == 3L & b2$Column == 2L) |
  (b2$Row == 5L & b2$Column == 3L) |
  (b2$Row == 6L & b2$Column == 5L)
)
b2 <- b2[-miss_b2, ]

# B3: non-rectangular — right side truncated
b3_parts <- list(
  expand.grid(Row = 1:2, Column = 1:6),
  expand.grid(Row = 3:4, Column = 1:4),
  expand.grid(Row = 5:5, Column = 1:2)
)
b3        <- do.call(rbind, b3_parts)
b3$Type   <- "DH"
b3$Block  <- "B3"
b3$Geno   <- paste0("G", sprintf("%03d", seq_len(nrow(b3))))

ex4 <- rbind(b1, b2, b3)

cat("Rows per block (before padding):\n")
Rows per block (before padding):
print(table(ex4$Block))

B1 B2 B3 
20 26 22 
res4 <- padTrial(
  ex4,
  match      = "DH",
  split      = "Block",
  fill_value = "Blank",
  verbose    = TRUE
)

cat("\nPadded rows per block:\n")

Padded rows per block:
print(table(res4$Block[res4$add == "new"]))

B2 B3 
 4  8 

With split = "Block" each block gets its own bounding box and its own padding pass. The plot shows Before / After side-by-side for all three blocks.

p4 <- plot_padTrial(res4, data = ex4, split = "Block")
print(p4)


Example 5: Mixed plot types — DH lines and checks

Trials often contain both test lines and check varieties within the same block. padTrial() uses match to define the bounding box from the target type only, but retains all non-guard plots that fall inside that bounding box — including checks.

set.seed(5L)
ex5 <- expand.grid(Row = 1:8, Column = 1:6, KEEP.OUT.ATTRS = FALSE)
ex5$Block <- "B1"

# Assign plot types: checks occupy every 6th position; guards on rows 1 & 8
ex5$Type  <- "DH"
check_idx <- seq(3L, nrow(ex5), by = 6L)
ex5$Type[check_idx]        <- "Check"
ex5$Type[ex5$Row == 1L]    <- "Guard"
ex5$Type[ex5$Row == 8L]    <- "Guard"

ex5$Geno <- ifelse(ex5$Type == "Guard", NA_character_,
                   paste0(ex5$Type, sprintf("%02d", seq_len(nrow(ex5)))))

# Remove 3 DH interior plots
miss5 <- which(ex5$Type == "DH")[c(2L, 10L, 18L)]
ex5   <- ex5[-miss5, ]
res5 <- padTrial(
  ex5,
  match    = "DH",          # bounding box from DH only
  split    = "Block",
  verbose  = TRUE
)

cat("\nPlot types in result:\n")

Plot types in result:
print(table(res5$Type))

Blank Check    DH 
    3     6    27 

Guards on rows 1 and 8 are outside the DH bounding box and are dropped. Checks inside the bounding box are retained. The three missing DH positions are padded.

p5 <- plot_padTrial(res5, data = ex5, split = "Block")
print(p5)


Example 6: Multi-column split

When the trial spans multiple sites and blocks, split accepts a character vector. Each unique combination of the split columns is processed independently. The temporary composite key is dropped from the result — only the original columns are returned.

# Two sites × two blocks each, some blocks with missing plots
make_site_block <- function(site, block, miss_rows = NULL, miss_cols = NULL) {
  d       <- expand.grid(Row = 1:5, Column = 1:4, KEEP.OUT.ATTRS = FALSE)
  d$Site  <- site
  d$Block <- block
  d$Type  <- "DH"
  d$Geno  <- paste0(site, block, "_G", sprintf("%02d", seq_len(nrow(d))))
  if (!is.null(miss_rows)) {
    rem <- which(d$Row %in% miss_rows & d$Column %in% miss_cols)
    if (length(rem)) d <- d[-rem, ]
  }
  d
}

ex6 <- rbind(
  make_site_block("S1", "B1"),
  make_site_block("S1", "B2", miss_rows = c(2L, 4L), miss_cols = c(3L)),
  make_site_block("S2", "B1", miss_rows = c(3L),     miss_cols = c(1L, 4L)),
  make_site_block("S2", "B2")
)
res6 <- padTrial(
  ex6,
  match  = "DH",
  split  = c("Site", "Block"),
  keep   = c("Site", "Block"),
  verbose = TRUE
)

cat("\nPadded rows per Site × Block:\n")

Padded rows per Site × Block:
with(res6[res6$add == "new", ], table(Site, Block))
    Block
Site B1 B2
  S1  0  2
  S2  2  0

Extracting plot data — return_data = TRUE

plot_padTrial() with return_data = TRUE returns the tidy data frame used internally to build the tile map. Columns are standardised: row_num, col_num, fill_group, panel ("Before" / "After"), group, is_new, and label_text.

df <- plot_padTrial(res2, data = ex2, split = "Block", return_data = TRUE)
head(df)
   row_num col_num fill_group panel group is_new label_text
1        1       1         DH After    B1  FALSE           
8        1       2         DH After    B1  FALSE           
15       1       3         DH After    B1  FALSE           
22       1       4         DH After    B1  FALSE           
29       1       5         DH After    B1  FALSE           
36       1       6         DH After    B1  FALSE           

The data frame can be passed into any ggplot2 workflow:

df_aft <- df[df$panel == "After", ]

ggplot(df_aft, aes(x = col_num, y = row_num, fill = fill_group)) +
  geom_tile(colour = "white", linewidth = 0.4) +
  geom_tile(data = df_aft[df_aft$is_new, ],
            fill = "tomato", colour = "white", linewidth = 0.4) +
  scale_y_reverse() +
  scale_fill_manual(values = c(DH = "#4E79A7", Blank = "#DEDEDE"),
                    name = "Type") +
  labs(title = "Custom colour for padded cells",
       x = "Column", y = "Row") +
  theme_bw()

The ggplot object returned by plot_padTrial() can also be extended directly with +:

plot_padTrial(res4, data = ex4, split = "Block") +
  ggplot2::ggtitle("Three-block trial: before and after padding",
                   subtitle = "B1 complete · B2 internal gaps · B3 non-rectangular")


Summary

Task Specification
Basic extraction (single block) padTrial(data, split = "Block")
No blocking column split = NULL
Multi-environment split split = c("Site", "Block"), keep = c("Site", "Block")
Custom target plot type match = "Line" (or any value in type_col)
Custom type column name type_col = "PlotKind"
Custom fill string fill_value = "Missing"
Suppress padding (extract only) pad = FALSE
Per-group diagnostics verbose = TRUE
Count padded rows table(result$add)
Inspect padded rows subset(result, add == "new")
Before/After plot (reconstruct Before) plot_padTrial(result)
Before/After plot (full original layout) plot_padTrial(result, data = original)
Multi-block faceted plot plot_padTrial(result, data = original, split = "Block")
Label tiles with genotype label = "Geno"
Export plot data return_data = TRUE