Skip to content

Commit b29b8e7

Browse files
Add sprinzl_coords parameter to plot_tRNA_structure() (#20)
When `sprinzl_coords` is provided, position columns in `modifications`, `outlines`, `text_colors`, and `linkages` are interpreted as Sprinzl labels and converted to 1-based sequence positions automatically. The tRNA is resolved in the Sprinzl table via `trna_id` or auto-matched from the `trna` argument.
1 parent c3c8f17 commit b29b8e7

File tree

5 files changed

+256
-32
lines changed

5 files changed

+256
-32
lines changed

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# clover 0.0.0.9000
22

3+
* `plot_tRNA_structure()` gains `sprinzl_coords` and `trna_id` parameters. When `sprinzl_coords` is provided, position columns in `modifications`, `outlines`, `text_colors`, and `linkages` are interpreted as Sprinzl labels and converted to 1-based sequence positions automatically (#20).
4+
35
* `compute_bcerror_delta()` computes per-position differences in base-calling error rates between two conditions from a summarized bcerror tibble.
46

57
* `prep_mod_heatmap()` prepares bcerror delta data for `plot_mod_heatmap()` by joining Sprinzl coordinates, annotating known modifications, and shortening tRNA labels.

R/plot-structure.R

Lines changed: 108 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -60,27 +60,30 @@ structure_trnas <- function(organism) {
6060
#' @param organism Character string specifying the organism name
6161
#' (e.g., `"Escherichia coli"`).
6262
#' @param modifications A tibble with columns `pos` (1-based
63-
#' position in the tRNA sequence) and `mod1` (short modification
64-
#' name, e.g., `"m1A"`). Output of [modomics_mods()] works
65-
#' directly after filtering to the tRNA of interest.
66-
#' @param outlines A tibble with columns `pos` (1-based position)
67-
#' and `group` (category name for palette lookup). Draws circle
68-
#' outlines (stroke only, no fill) around each nucleotide.
69-
#' @param linkages A tibble with columns `pos1`, `pos2`, and
70-
#' optionally `value` (e.g., log odds ratio) for coloring arcs.
71-
#' If a `log_odds_ratio` column is present and `value` is not, it
72-
#' is automatically used as `value`, so output of
73-
#' [clean_odds_ratios()] or [filter_linkages()] works directly.
63+
#' position or Sprinzl label when `sprinzl_coords` is provided)
64+
#' and `mod1` (short modification name, e.g., `"m1A"`). Output
65+
#' of [modomics_mods()] works directly after filtering to the
66+
#' tRNA of interest.
67+
#' @param outlines A tibble with columns `pos` (1-based position
68+
#' or Sprinzl label) and `group` (category name for palette
69+
#' lookup). Draws circle outlines (stroke only, no fill) around
70+
#' each nucleotide.
71+
#' @param linkages A tibble with columns `pos1`, `pos2` (1-based
72+
#' positions or Sprinzl labels), and optionally `value` (e.g.,
73+
#' log odds ratio) for coloring arcs. If a `log_odds_ratio`
74+
#' column is present and `value` is not, it is automatically
75+
#' used as `value`, so output of [clean_odds_ratios()] or
76+
#' [filter_linkages()] works directly.
7477
#' @param output Path for the output SVG file. If `NULL` (default),
7578
#' writes to a temporary file.
7679
#' @param mod_palette Named character vector of colors keyed by
7780
#' modification short name. If `NULL`, uses a default palette.
7881
#' @param outline_palette Named character vector of colors keyed by
7982
#' outline group name. If `NULL`, uses `"#333333"` for all.
80-
#' @param text_colors A tibble with columns `pos` (1-based position)
81-
#' and `color` (hex color string). Changes the nucleotide letter
82-
#' color at specified positions. Unspecified positions keep the
83-
#' default color.
83+
#' @param text_colors A tibble with columns `pos` (1-based position
84+
#' or Sprinzl label) and `color` (hex color string). Changes the
85+
#' nucleotide letter color at specified positions. Unspecified
86+
#' positions keep the default color.
8487
#' @param position_markers Logical; if `TRUE` (default), draw
8588
#' small grey position numbers every 10 nucleotides around the
8689
#' cloverleaf to help orient readers.
@@ -89,6 +92,16 @@ structure_trnas <- function(organism) {
8992
#' linkage values. Default `c("#0072B2", "#D55E00")` (blue for
9093
#' exclusive, vermillion for co-occurring). Stroke width encodes
9194
#' the magnitude of the value.
95+
#' @param sprinzl_coords A tibble of Sprinzl coordinates as
96+
#' returned by [read_sprinzl_coords()], or `NULL` (default). When
97+
#' provided, position columns in `modifications`, `outlines`,
98+
#' `text_colors`, and `linkages` are interpreted as Sprinzl
99+
#' labels and converted to 1-based sequence positions
100+
#' automatically.
101+
#' @param trna_id Character string identifying the tRNA in
102+
#' `sprinzl_coords` (e.g.,
103+
#' `"nuc-tRNA-Glu-UUC-1-1"`). If `NULL` (default), the tRNA
104+
#' name is resolved from `trna` automatically.
92105
#'
93106
#' @return The path to the annotated SVG file (invisibly).
94107
#'
@@ -109,10 +122,56 @@ plot_tRNA_structure <- function(
109122
outline_palette = NULL,
110123
text_colors = NULL,
111124
position_markers = TRUE,
112-
linkage_palette = c("#0072B2", "#D55E00")
125+
linkage_palette = c("#0072B2", "#D55E00"),
126+
sprinzl_coords = NULL,
127+
trna_id = NULL
113128
) {
114129
rlang::check_installed("jsonlite", reason = "to read structure metadata.")
115130

131+
if (!is.null(sprinzl_coords)) {
132+
if (is.null(trna_id)) {
133+
trna_id <- find_sprinzl_id(trna, sprinzl_coords)
134+
if (is.null(trna_id)) {
135+
# Fallback: try matching without "nuc-" prefix
136+
trna_id <- find_sprinzl_id_bare(trna, sprinzl_coords)
137+
}
138+
if (is.null(trna_id)) {
139+
cli::cli_abort(
140+
"Could not find {.val {trna}} in {.arg sprinzl_coords}."
141+
)
142+
}
143+
}
144+
trna_coords <- sprinzl_coords[sprinzl_coords$trna_id == trna_id, ]
145+
if (!is.null(modifications)) {
146+
modifications <- convert_sprinzl_positions(
147+
modifications,
148+
"pos",
149+
trna_coords
150+
)
151+
}
152+
if (!is.null(outlines)) {
153+
outlines <- convert_sprinzl_positions(
154+
outlines,
155+
"pos",
156+
trna_coords
157+
)
158+
}
159+
if (!is.null(text_colors)) {
160+
text_colors <- convert_sprinzl_positions(
161+
text_colors,
162+
"pos",
163+
trna_coords
164+
)
165+
}
166+
if (!is.null(linkages)) {
167+
linkages <- convert_sprinzl_positions(
168+
linkages,
169+
c("pos1", "pos2"),
170+
trna_coords
171+
)
172+
}
173+
}
174+
116175
org_dir <- structure_org_dir(organism)
117176

118177
svg_path <- file.path(org_dir, paste0(trna, ".svg"))
@@ -296,6 +355,39 @@ structure_html <- function(svg_path) {
296355

297356
# Internal helpers -------------------------------------------------------------
298357

358+
find_sprinzl_id_bare <- function(trna, sprinzl_coords) {
359+
parts <- strsplit(trna, "-")[[1]]
360+
if (length(parts) >= 3) {
361+
parts[3] <- gsub("T", "U", parts[3])
362+
}
363+
rna_name <- paste(parts, collapse = "-")
364+
pattern <- paste0("^", rna_name, "-")
365+
366+
ids <- unique(sprinzl_coords$trna_id)
367+
matches <- grep(pattern, ids, value = TRUE)
368+
if (length(matches) == 0) {
369+
return(NULL)
370+
}
371+
sort(matches)[1]
372+
}
373+
374+
convert_sprinzl_positions <- function(df, pos_cols, trna_coords) {
375+
lookup <- trna_coords[, c("sprinzl_label", "pos")]
376+
for (col in pos_cols) {
377+
original <- as.character(df[[col]])
378+
matched <- lookup$pos[match(original, lookup$sprinzl_label)]
379+
unmatched <- original[is.na(matched) & !is.na(original)]
380+
if (length(unmatched) > 0) {
381+
n <- length(unmatched)
382+
cli::cli_warn(
383+
"Sprinzl position{cli::qty(length(unique(unmatched)))} {?s} {.val {unique(unmatched)}} not found; dropping {n} row{cli::qty(n)}{?s}."
384+
)
385+
}
386+
df[[col]] <- matched
387+
}
388+
df[stats::complete.cases(df[pos_cols]), , drop = FALSE]
389+
}
390+
299391
# R2R SVGs use font-size 7.1 Helvetica. The text x/y attributes give the
300392
# left baseline of the character. These offsets shift to the visual center
301393
# of the uppercase letter (approximately half character-width right, half

man/plot_tRNA_structure.Rd

Lines changed: 33 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/_snaps/plot-structure.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,19 @@
4141
Error in `structure_html()`:
4242
! SVG file not found: 'nonexistent.svg'.
4343

44+
# convert_sprinzl_positions warns on unmatched and drops rows
45+
46+
Code
47+
result <- convert_sprinzl_positions(df, "pos", trna_coords)
48+
Condition
49+
Warning:
50+
Sprinzl position "99" not found; dropping 1 row.
51+
52+
# plot_tRNA_structure errors when tRNA not in sprinzl_coords
53+
54+
Code
55+
plot_tRNA_structure(trna, org, sprinzl_coords = fake_coords)
56+
Condition
57+
Error in `plot_tRNA_structure()`:
58+
! Could not find "tRNA-Ala-GGC" in `sprinzl_coords`.
59+

tests/testthat/test-plot-structure.R

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,103 @@ test_that("structure_html errors on missing file", {
284284
)
285285
})
286286

287+
# Sprinzl coordinate conversion ------------------------------------------------
288+
289+
test_that("convert_sprinzl_positions maps labels to positions", {
290+
trna_coords <- dplyr::tibble(
291+
sprinzl_label = c("1", "2", "34", "35", "36"),
292+
pos = c(1L, 2L, 30L, 31L, 32L)
293+
)
294+
df <- dplyr::tibble(pos = c("34", "35", "36"), mod1 = c("m1A", "m5C", "D"))
295+
result <- convert_sprinzl_positions(df, "pos", trna_coords)
296+
expect_equal(result$pos, c(30L, 31L, 32L))
297+
expect_equal(result$mod1, c("m1A", "m5C", "D"))
298+
})
299+
300+
test_that("convert_sprinzl_positions coerces numeric input", {
301+
trna_coords <- dplyr::tibble(
302+
sprinzl_label = c("34", "35"),
303+
pos = c(30L, 31L)
304+
)
305+
df <- dplyr::tibble(pos = c(34, 35), mod1 = c("m1A", "m5C"))
306+
result <- convert_sprinzl_positions(df, "pos", trna_coords)
307+
expect_equal(result$pos, c(30L, 31L))
308+
})
309+
310+
test_that("convert_sprinzl_positions warns on unmatched and drops rows", {
311+
trna_coords <- dplyr::tibble(
312+
sprinzl_label = c("1", "2"),
313+
pos = c(1L, 2L)
314+
)
315+
df <- dplyr::tibble(pos = c("1", "99"), mod1 = c("m1A", "m5C"))
316+
expect_snapshot(
317+
result <- convert_sprinzl_positions(df, "pos", trna_coords)
318+
)
319+
expect_equal(nrow(result), 1)
320+
expect_equal(result$pos, 1L)
321+
})
322+
323+
test_that("convert_sprinzl_positions converts two columns for linkages", {
324+
trna_coords <- dplyr::tibble(
325+
sprinzl_label = c("34", "35", "36"),
326+
pos = c(30L, 31L, 32L)
327+
)
328+
df <- dplyr::tibble(
329+
pos1 = c("34", "35"),
330+
pos2 = c("36", "34"),
331+
value = c(1.5, -0.5)
332+
)
333+
result <- convert_sprinzl_positions(df, c("pos1", "pos2"), trna_coords)
334+
expect_equal(result$pos1, c(30L, 31L))
335+
expect_equal(result$pos2, c(32L, 30L))
336+
})
337+
338+
test_that("plot_tRNA_structure errors when tRNA not in sprinzl_coords", {
339+
skip_if(
340+
length(structure_organisms()) == 0,
341+
"No bundled structure SVGs"
342+
)
343+
344+
org <- structure_organisms()[1]
345+
trna <- structure_trnas(org)[1]
346+
fake_coords <- dplyr::tibble(
347+
trna_id = "nuc-tRNA-Fake-AAA-1-1",
348+
pos = 1L,
349+
sprinzl_label = "1"
350+
)
351+
expect_snapshot(
352+
plot_tRNA_structure(trna, org, sprinzl_coords = fake_coords),
353+
error = TRUE
354+
)
355+
})
356+
357+
test_that("plot_tRNA_structure converts sprinzl coords with real data", {
358+
skip_if(
359+
length(structure_organisms()) == 0,
360+
"No bundled structure SVGs"
361+
)
362+
coords_path <- system.file(
363+
"extdata",
364+
"sprinzl",
365+
"ecoliK12_global_coords.tsv.gz",
366+
package = "clover"
367+
)
368+
skip_if(coords_path == "", "No bundled sprinzl coords")
369+
370+
coords <- read_sprinzl_coords(coords_path)
371+
org <- "Escherichia coli"
372+
trna <- "tRNA-Glu-TTC"
373+
374+
mods <- dplyr::tibble(pos = c("34", "35"), mod1 = c("m1A", "m5C"))
375+
svg <- plot_tRNA_structure(
376+
trna,
377+
org,
378+
modifications = mods,
379+
sprinzl_coords = coords
380+
)
381+
expect_true(file.exists(svg))
382+
})
383+
287384
test_that("plot_tRNA_structure respects position_markers = FALSE", {
288385
skip_if(
289386
length(structure_organisms()) == 0,

0 commit comments

Comments
 (0)