Any window-based genomic profile depends on the window size, which acts as a smoothing bandwidth: too large and local signal is averaged away, too small and single-window noise dominates. This vignette quantifies that dependence for TmCalculator by recomputing the Escherichia coli K-12 MG1655 Tm/GC profile at 50, 100, 200 and 500 bp and asking two separate questions.
The companion vignette
vignette("genome_wide_tm_ecoli", package = "TmCalculator")
develops the full case study at the 200 bp resolution used by Hasenauer
et al. (2025), including the MutL-associated region (MutL-AR)
comparison that is re-tested here at every window size.
We reproduce the minimum needed from the main case study: the genome, the MutL-AR peak coordinates, and the reference labels used for plotting.
The E. coli genome is supplied by the pre-forged
BSgenome.Ecoli.NCBI.ASM584v2 package. The chunk below
installs it if it is not already available, exactly as in the main
case-study vignette; see that vignette for how the package is forged
from the NCBI assembly accession.
ecoli_pkg <- "BSgenome.Ecoli.NCBI.ASM584v2"
genome_obj <- "Ecoli" # BSgenomeObjname in DESCRIPTION; not the package name
.ecoli_genome_ready <- function() {
if (!requireNamespace(ecoli_pkg, quietly = TRUE)) return(FALSE)
exists(genome_obj, envir = asNamespace(ecoli_pkg), inherits = FALSE)
}
if (!.ecoli_genome_ready()) {
if (!requireNamespace("remotes", quietly = TRUE)) {
utils::install.packages("remotes", repos = "https://cloud.r-project.org")
}
remotes::install_github(
"JunhuiLi1017/BSgenome.Ecoli.NCBI.ASM584v2",
upgrade = "never",
quiet = TRUE
)
}
if (!.ecoli_genome_ready()) {
stop(
"Could not load genome object '", genome_obj, "' from package '",
ecoli_pkg, "'.\n",
"See vignette(\"genome_wide_tm_ecoli\") for how to forge it locally.",
call. = FALSE
)
}suppressPackageStartupMessages(library(ecoli_pkg, character.only = TRUE))
genome <- base::get(genome_obj, envir = asNamespace(ecoli_pkg))
genome_name <- ecoli_pkg
chr_name <- "U00096.3"
chr_length <- GenomeInfoDb::seqlengths(genome)[[chr_name]]
data(ecoli_rep_hotspots)
## MutL-AR peaks, as in the main case study
mutH_peaks <- GRanges(
seqnames = ecoli_rep_hotspots$all_peaks_IP_mutH$chr,
ranges = IRanges(start = ecoli_rep_hotspots$all_peaks_IP_mutH$start,
end = ecoli_rep_hotspots$all_peaks_IP_mutH$end)
)
seqlevels(mutH_peaks) <- chr_name
mutH_peaks$peak_id <- paste0("mutH_", seq_along(mutH_peaks))
## Reference labels: replication origin (ori) and terminus (dif)
label <- data.frame(
seqnames = genome_name,
start = c(3925804, 1590777),
end = c(3925804, 1590777),
label = c("ori", "dif")
)For each window size we retile the chromosome, recompute Tm and GC,
and re-run the MutL-AR comparison, keeping every other setting fixed.
Nothing downstream needs to change: the annotation layers keep their own
native resolutions (1 kb bins for the microsatellite, cruciform and GATC
density tracks; variable-width intervals for MutL-AR peaks and ssDNA
regions), and both plot_genome_track() and the
overlap-based statistics work across mixed resolutions.
window_sizes <- c(50L, 100L, 200L, 500L)
sens <- lapply(window_sizes, function(w) {
bins_w <- make_genomiccoord(
bsgenome = genome_name, chromosomes = chr_name,
window = w, slide = w, start = 1, end = chr_length,
strand = "+", verbose = FALSE
)
gr_w <- to_genomic_ranges_fast(list(pkg_name = genome_name, seq = bins_w))
tm_w <- tm_calculate(gr_w, method = "tm_nn",
nn_table = "DNA_NN_Breslauer_1986", Na = 50)$gr
ann <- integrate_granges(gr_tm = tm_w, gr_features = mutH_peaks,
strategy = "overlap", feature_cols = "peak_id",
keep_unmatched = TRUE)
ann$in_mutH <- ifelse(is.na(ann$peak_id), "non_peak", "peak")
cg <- compare_groups(gr = ann, target = c("Tm", "GC"),
method = "wilcoxon", group = "in_mutH",
alternative = "greater", posthoc = FALSE)
list(gr = tm_w, ann = ann, test = cg)
})
names(sens) <- paste0("w", window_sizes)dist_tbl <- do.call(rbind, lapply(seq_along(window_sizes), function(i) {
g <- sens[[i]]$gr
data.frame(
window_bp = window_sizes[i],
n_windows = length(g),
Tm_mean = mean(g$Tm, na.rm = TRUE),
Tm_sd = stats::sd(g$Tm, na.rm = TRUE),
Tm_IQR = stats::IQR(g$Tm, na.rm = TRUE),
GC_mean = mean(g$GC, na.rm = TRUE)
)
}))
knitr::kable(dist_tbl, digits = 3,
caption = "Tm/GC distribution by window size.")| window_bp | n_windows | Tm_mean | Tm_sd | Tm_IQR | GC_mean |
|---|---|---|---|---|---|
| 50 | 92833 | 83.891 | 5.389 | 6.877 | 50.791 |
| 100 | 46416 | 93.818 | 4.423 | 5.320 | 50.791 |
| 200 | 23208 | 98.908 | 3.825 | 4.422 | 50.791 |
| 500 | 9283 | 101.998 | 3.251 | 3.628 | 50.791 |
The same two effects are easier to read as distributions than as summary statistics: the curve moves to the right as the window lengthens, and it narrows at the same time.
## Palettes are defined here because they are used both by this panel and by
## the multi-scale track figures further down.
## darkest = finest window (50 bp), lightest = coarsest (500 bp)
scale_cols_gc <- c("#1A5276", "#2E86C1", "#5DADE2", "#AED6F1")
scale_cols_tm <- c("#7B241C", "#CB4335", "#EC7063", "#F5B7B1")
## Densities are normalised, so the ~93,000 windows at 50 bp and the ~9,300
## at 500 bp can be compared directly on one pair of axes.
tm_by_size <- lapply(sens, function(x) x$gr$Tm[is.finite(x$gr$Tm)])
dens <- lapply(tm_by_size, stats::density)
xlim <- range(vapply(dens, function(z) range(z$x), numeric(2)))
ylim <- c(0, max(vapply(dens, function(z) max(z$y), numeric(1))))
op <- par(mar = c(4.2, 4.4, 0.8, 0.8), las = 1)
plot(NA, xlim = xlim, ylim = ylim, bty = "n",
xlab = expression(italic(T)[m] ~ "(" * degree * "C)"),
ylab = "Density")
for (i in seq_along(dens)) {
lines(dens[[i]], col = scale_cols_tm[i], lwd = 2)
abline(v = dist_tbl$Tm_mean[i], col = scale_cols_tm[i], lwd = 1, lty = 3)
}
legend("topleft", bty = "n", lwd = 2, seg.len = 1.6,
col = scale_cols_tm[seq_along(window_sizes)],
legend = sprintf("%d bp: mean %.1f, SD %.2f",
dist_tbl$window_bp, dist_tbl$Tm_mean, dist_tbl$Tm_sd))Tm distribution at each window size. The distribution shifts to higher Tm as the window lengthens and narrows at the same time; dotted vertical lines mark the group means. Colours match the multi-scale tracks below (darkest = 50 bp, lightest = 500 bp).
par(op)
## Spread of the finest window relative to the coarsest. This is the ratio
## quoted in the manuscript, so both come from the same computation.
sd_ratio <- dist_tbl$Tm_sd[1] / dist_tbl$Tm_sd[nrow(dist_tbl)]Two systematic effects are visible, both expected. First, mean Tm rises with window length (83.9 to 102.0 degrees C from 50 to 500 bp): duplex stability increases with length in the nearest-neighbor model, so absolute Tm values are only comparable within one window size. Second, shorter windows widen the distribution (SD 5.39 at 50 bp vs 3.25 at 500 bp, a ratio of 1.66) – window size acts as a smoothing bandwidth. The mean GC is invariant (50.791 throughout), confirming that the composition landscape itself does not depend on the tiling.
Every profile is aggregated onto one common grid (mean Tm per bin) and the resulting series are correlated.
The grid is 1 kb rather than 500 bp, and the choice matters. Correlating against the 500 bp profile directly makes 200 bp the odd one out: 50 and 100 divide 500 exactly, into ten and five windows per bin, but 200 does not, so each bin receives two or three windows that straddle its boundaries and the aggregation is itself inexact. That alone drove the 200 bp correlation down to 0.976 while the two finer profiles reached 0.997 and 0.999, an ordering that would invite the reader to conclude that the closest resolution agrees worst. All four window sizes divide 1 kb (into 20, 10, 5 and 2), so on that grid every profile is aggregated exactly and the comparison is between profiles rather than between remainders. The 500 bp profile is aggregated too, and serves as the reference.
grid_bp <- 1000L
stopifnot(all(grid_bp %% window_sizes == 0L)) # exact aggregation for each
gr_ref <- sens[["w500"]]$gr
ref_agg <- tapply(gr_ref$Tm, (start(gr_ref) - 1L) %/% grid_bp,
mean, na.rm = TRUE)
cor_tbl <- vapply(c("w50", "w100", "w200"), function(k) {
g <- sens[[k]]$gr
agg <- tapply(g$Tm, (start(g) - 1L) %/% grid_bp, mean, na.rm = TRUE)
common <- intersect(names(agg), names(ref_agg))
stats::cor(agg[common], ref_agg[common], use = "complete.obs")
}, numeric(1))
round(cor_tbl, 3)## w50 w100 w200
## 0.998 0.999 1.000
Aggregated onto the common 1 kb grid, the profiles are nearly interchangeable (r = 0.998, 0.999, 1.000 for 50, 100 and 200 bp respectively). Agreement now rises monotonically as the window approaches the grid, which is what averaging fewer windows per bin should do, and is the behaviour the 500 bp grid obscured. Window size rescales and smooths the profile but preserves the spatial landscape.
Finally, the vignette’s biological conclusion – that Tm and GC differ between MutL-AR peak windows and the genomic background – is re-tested at every window size:
## Per-window-size test statistics
test_results <- do.call(rbind, lapply(seq_along(window_sizes), function(i) {
data.frame(window_bp = window_sizes[i], sens[[i]]$test$results)
}))
test_results## window_bp target group method test statistic df p.value
## 1 50 Tm in_mutH wilcoxon Wilcoxon rank-sum 168746210 NA 2.432173e-90
## 2 50 GC in_mutH wilcoxon Wilcoxon rank-sum 160420084 NA 7.452204e-48
## 3 100 Tm in_mutH wilcoxon Wilcoxon rank-sum 44130350 NA 1.530886e-65
## 4 100 GC in_mutH wilcoxon Wilcoxon rank-sum 41400570 NA 1.192782e-32
## 5 200 Tm in_mutH wilcoxon Wilcoxon rank-sum 11650602 NA 4.824689e-45
## 6 200 GC in_mutH wilcoxon Wilcoxon rank-sum 10806568 NA 8.549859e-22
## 7 500 Tm in_mutH wilcoxon Wilcoxon rank-sum 2033341 NA 4.905312e-25
## 8 500 GC in_mutH wilcoxon Wilcoxon rank-sum 1873428 NA 1.417503e-12
## n_groups n_total group_levels group_n
## 1 2 92833 non_peak | peak non_peak=89730, peak=3103
## 2 2 92833 non_peak | peak non_peak=89730, peak=3103
## 3 2 46416 non_peak | peak non_peak=44845, peak=1571
## 4 2 46416 non_peak | peak non_peak=44845, peak=1571
## 5 2 23208 non_peak | peak non_peak=22402, peak=806
## 6 2 23208 non_peak | peak non_peak=22402, peak=806
## 7 2 9283 non_peak | peak non_peak=8940, peak=343
## 8 2 9283 non_peak | peak non_peak=8940, peak=343
## Per-window-size group summaries, plus the median Tm effect size
test_summary <- do.call(rbind, lapply(seq_along(window_sizes), function(i) {
ann <- sens[[i]]$ann
med <- tapply(ann$Tm, ann$in_mutH, stats::median, na.rm = TRUE)
data.frame(window_bp = window_sizes[i],
n_peak = sum(ann$in_mutH == "peak"),
Tm_med_diff = unname(med["peak"] - med["non_peak"]),
sens[[i]]$test$summary)
}))
test_summary## window_bp n_peak Tm_med_diff group n mean sd median
## 1 50 3103 -2.003979 non_peak 89730 83.95988 5.365263 84.44240
## 2 50 3103 -2.003979 peak 3103 81.88670 5.694383 82.43842
## 3 50 3103 -2.003979 non_peak 89730 50.87955 8.832840 52.00000
## 4 50 3103 -2.003979 peak 3103 48.22172 9.789787 50.00000
## 5 100 1571 -1.663404 non_peak 44845 93.88741 4.393684 94.51009
## 6 100 1571 -1.663404 peak 1571 91.85059 4.780258 92.84668
## 7 100 1571 -1.663404 non_peak 44845 50.88135 7.326739 52.00000
## 8 100 1571 -1.663404 peak 1571 48.21260 8.384205 50.00000
## 9 200 806 -1.763693 non_peak 22402 98.97778 3.791850 99.65325
## 10 200 806 -1.763693 peak 806 96.96551 4.206166 97.88956
## 11 200 806 -1.763693 non_peak 22402 50.88340 6.346952 52.00000
## 12 200 806 -1.763693 peak 806 48.22333 7.394710 50.00000
## 13 500 343 -1.703604 non_peak 8940 102.06820 3.213329 102.71352
## 14 500 343 -1.703604 peak 343 100.16319 3.679005 101.00992
## 15 500 343 -1.703604 non_peak 8940 50.88456 5.386122 51.80000
## 16 500 343 -1.703604 peak 343 48.35219 6.463921 50.20000
## target
## 1 Tm
## 2 Tm
## 3 GC
## 4 GC
## 5 Tm
## 6 Tm
## 7 GC
## 8 GC
## 9 Tm
## 10 Tm
## 11 GC
## 12 GC
## 13 Tm
## 14 Tm
## 15 GC
## 16 GC
Both questions are answered by the same four rows, so they are assembled into a single table: the first four columns say what window size does to the landscape, and the last three say what it does to the conclusion drawn from it.
sens_tbl <- do.call(rbind, lapply(seq_along(window_sizes), function(i) {
g <- sens[[i]]$gr
ann <- sens[[i]]$ann
med <- tapply(ann$Tm, ann$in_mutH, stats::median, na.rm = TRUE)
res <- sens[[i]]$test$results
w <- window_sizes[i]
data.frame(
`Window (bp)` = w,
Windows = length(g),
`Tm mean (C)` = mean(g$Tm, na.rm = TRUE),
`Tm SD` = stats::sd(g$Tm, na.rm = TRUE),
`GC mean (%)` = mean(g$GC, na.rm = TRUE),
## The reference profile correlates with itself by construction.
`r vs 1 kb grid` = if (w == max(window_sizes)) NA_real_
else unname(cor_tbl[[paste0("w", w)]]),
`MutL-AR windows` = sum(ann$in_mutH == "peak"),
`Tm peak - bg (C)` = unname(med["peak"] - med["non_peak"]),
## Formatted as character: these p values span sixty-five orders of
## magnitude, and rounding them to a fixed number of decimals prints
## every one of them as zero.
`p (Tm)` = format(res$p.value[res$target == "Tm"],
digits = 2, scientific = TRUE),
check.names = FALSE, stringsAsFactors = FALSE)
}))
knitr::kable(sens_tbl, digits = c(0, 0, 2, 2, 2, 3, 0, 3, 0),
caption = paste("Window-size sensitivity. GC mean is invariant;",
"the Tm distribution shifts and narrows; the",
"MutL-AR effect size varies within a few tenths",
"of a degree while the p value tracks the number",
"of windows."))| Window (bp) | Windows | Tm mean (C) | Tm SD | GC mean (%) | r vs 1 kb grid | MutL-AR windows | Tm peak - bg (C) | p (Tm) |
|---|---|---|---|---|---|---|---|---|
| 50 | 92833 | 83.89 | 5.39 | 50.79 | 0.998 | 3103 | -2.004 | 2.4e-90 |
| 100 | 46416 | 93.82 | 4.42 | 50.79 | 0.999 | 1571 | -1.663 | 1.5e-65 |
| 200 | 23208 | 98.91 | 3.82 | 50.79 | 1.000 | 806 | -1.764 | 4.8e-45 |
| 500 | 9283 | 102.00 | 3.25 | 50.79 | NA | 343 | -1.704 | 4.9e-25 |
The last two columns are the point of the section. The p value falls by sixty-five orders of magnitude between the coarsest and the finest tiling, which is a statement about how many windows were tested rather than about how far apart the two groups are. The difference in medians, which is that statement, stays between -1.7 and -2.0 degrees C. The finest tiling gives the largest separation, as expected: a 500 bp window overlapping a MutL-AR peak also contains flanking sequence that does not, and averaging over it dilutes the contrast, whereas a 50 bp window resolves the peak more sharply. The three coarser tilings agree within 0.1 degrees C.
Because every track keeps its native resolution, the four Tm/GC profiles can be drawn as concentric layers of one integrated figure, together with the annotation data:
## scale_cols_gc / scale_cols_tm are defined in the density-panel chunk above
sens_dfs <- lapply(sens, function(x) as.data.frame(x$gr[, c("Tm", "GC")]))
tracks_scale <- c(
list(list(type = "rect", data = ecoli_rep_hotspots$all_peaks_IP_mutH,
col = "#2C3E50", bg.col = "grey", name = "MutL-AR",
legend_font_col = "#2C3E50", ideogram = TRUE, height = 0.5)),
lapply(seq_along(window_sizes), function(i)
list(type = "line", data = sens_dfs[[i]], value_col = "GC",
name = paste0("GC ", window_sizes[i], " bp"),
col = scale_cols_gc[i], legend_font_col = scale_cols_gc[i])),
lapply(seq_along(window_sizes), function(i)
list(type = "line", data = sens_dfs[[i]], value_col = "Tm",
name = paste0("Tm ", window_sizes[i], " bp"),
col = scale_cols_tm[i], legend_font_col = scale_cols_tm[i])),
list(list(type = "line", data = ecoli_rep_hotspots$bins_rep,
value_col = "count", name = "Microsatellites", col = "#2ECC71",
legend_font_col = "#2ECC71"),
list(type = "highlight",
data = ecoli_rep_hotspots$all_peaks_IP_mutH,
col = "#F1C40F", alpha = 0.18))
)
plot_genome_track(
genome_name = genome_name,
genome_size = chr_length,
track_list = tracks_scale,
circular = TRUE,
label = label
)Multi-scale integration. Concentric rings from outside in: MutL-AR peaks (ideogram), GC content at 50/100/200/500 bp (blues), Tm at 50/100/200/500 bp (reds), and microsatellite density (green). Yellow bands mark MutL-AR peaks.
At whole-genome scale the four bandwidths look nearly identical – the landscape is invariant. The difference window size makes is local smoothing, best seen in a zoomed view of the same interval used by the main case study:
## Only the four Tm profiles are drawn. Ten line tracks in one linear panel
## leave each too little vertical space for its own axis labels, which then
## collide, and everything except Tm is answering a different question: the
## GC layers duplicate what the summary table already gives numerically, and
## the microsatellite density is the subject of the main case study's figure,
## not of this one. Removing both leaves five tracks with room to be read.
## The full ten-track version is the circular figure above.
##
## The interval is the one used in the main case study's zoomed panel, so the
## two figures can be read against each other rather than against different
## parts of the chromosome.
tracks_zoom <- c(
list(list(type = "rect", data = ecoli_rep_hotspots$all_peaks_IP_mutH,
col = "#2C3E50", bg.col = "grey", name = "MutL-AR",
legend_font_col = "#2C3E50", ideogram = TRUE, height = 0.6)),
lapply(seq_along(window_sizes), function(i)
list(type = "line", data = sens_dfs[[i]], value_col = "Tm",
name = paste0("Tm ", window_sizes[i], " bp"),
col = scale_cols_tm[i], legend_font_col = scale_cols_tm[i],
height = 1.2)),
list(list(type = "highlight",
data = ecoli_rep_hotspots$all_peaks_IP_mutH,
col = "#F1C40F", alpha = 0.18))
)
plot_genome_track(
genome_name = genome_name,
genome_size = chr_length,
track_list = tracks_zoom,
zoom = "U00096.3:100000-300000",
track.gap = 0.03,
axis.cex = 0.55
)The 0.1-0.3 Mb region, within the interval shown in the genome-wide figure of the main case study, with Tm computed at 50, 100, 200 and 500 bp (dark to light). The 50 bp profile resolves single-window fluctuations that the 500 bp profile averages away, but all four trace the same underlying landscape. Yellow bands mark MutL-AR peaks.
The association is reproduced at every scale. MutL-AR windows are lower in median Tm than the background at all four window sizes (-2.00, -1.66, -1.76, -1.70 degrees C at 50, 100, 200, 500 bp) and lower in GC (48.252 in peaks vs 50.882 in the background), and every Wilcoxon test is significant (largest p = 1.4e-12). As expected, p values shrink with the number of windows while the effect size stays essentially constant, so the biological conclusion does not depend on the window choice. The primary analysis uses 200 bp to match Hasenauer et al. (2025), whose Methods compute GC content and melting temperature in 200 bp bins (using TmCalculator) and bin the ChIP-seq coverage at the same 200 bp resolution.
## R version 4.4.1 (2024-06-14)
## Platform: x86_64-apple-darwin20
## Running under: macOS Sonoma 14.6
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/4.4-x86_64/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.4-x86_64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.0
##
## locale:
## [1] C/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
##
## time zone: America/New_York
## tzcode source: internal
##
## attached base packages:
## [1] stats4 stats graphics grDevices utils datasets methods
## [8] base
##
## other attached packages:
## [1] BSgenome.Ecoli.NCBI.ASM584v2_1.0.0 BSgenome_1.72.0
## [3] rtracklayer_1.64.0 BiocIO_1.14.0
## [5] Biostrings_2.72.1 XVector_0.44.0
## [7] GenomicRanges_1.56.2 GenomeInfoDb_1.40.1
## [9] IRanges_2.38.1 S4Vectors_0.42.1
## [11] BiocGenerics_0.50.0 TmCalculator_1.1.0
##
## loaded via a namespace (and not attached):
## [1] DBI_1.2.3 bitops_1.0-9
## [3] gridExtra_2.3 rlang_1.1.7
## [5] magrittr_2.0.4 biovizBase_1.52.0
## [7] otel_0.2.0 matrixStats_1.5.0
## [9] compiler_4.4.1 RSQLite_2.4.0
## [11] GenomicFeatures_1.56.0 png_0.1-8
## [13] vctrs_0.7.1 ProtGenerics_1.36.0
## [15] stringr_1.6.0 pkgconfig_2.0.3
## [17] crayon_1.5.3 fastmap_1.2.0
## [19] backports_1.5.0 Rsamtools_2.20.0
## [21] rmarkdown_2.30 UCSC.utils_1.0.0
## [23] bit_4.6.0 xfun_0.58
## [25] zlibbioc_1.50.0 cachem_1.1.0
## [27] jsonlite_2.0.0 blob_1.2.4
## [29] DelayedArray_0.30.1 BiocParallel_1.38.0
## [31] parallel_4.4.1 cluster_2.1.6
## [33] R6_2.6.1 VariantAnnotation_1.50.0
## [35] stringi_1.8.7 bslib_0.10.0
## [37] RColorBrewer_1.1-3 bezier_1.1.2
## [39] rpart_4.1.23 jquerylib_0.1.4
## [41] Rcpp_1.1.2 SummarizedExperiment_1.34.0
## [43] knitr_1.51 base64enc_0.1-6
## [45] Matrix_1.7-0 nnet_7.3-19
## [47] tidyselect_1.2.1 rstudioapi_0.18.0
## [49] dichromat_2.0-0.1 abind_1.4-8
## [51] yaml_2.3.12 codetools_0.2-20
## [53] curl_6.2.3 lattice_0.22-6
## [55] tibble_3.2.1 regioneR_1.36.0
## [57] Biobase_2.64.0 KEGGREST_1.44.1
## [59] evaluate_1.0.5 foreign_0.8-87
## [61] karyoploteR_1.30.0 pillar_1.10.2
## [63] MatrixGenerics_1.16.0 checkmate_2.3.2
## [65] generics_0.1.4 RCurl_1.98-1.17
## [67] ensembldb_2.28.1 ggplot2_3.5.2
## [69] scales_1.4.0 glue_1.8.0
## [71] lazyeval_0.2.2 Hmisc_5.2-3
## [73] tools_4.4.1 data.table_1.17.4
## [75] GenomicAlignments_1.40.0 XML_3.99-0.18
## [77] grid_4.4.1 colorspace_2.1-1
## [79] AnnotationDbi_1.66.0 GenomeInfoDbData_1.2.12
## [81] htmlTable_2.4.3 restfulr_0.0.15
## [83] Formula_1.2-5 cli_3.6.5
## [85] S4Arrays_1.4.1 dplyr_1.1.4
## [87] AnnotationFilter_1.28.0 gtable_0.3.6
## [89] sass_0.4.10 digest_0.6.39
## [91] SparseArray_1.4.8 rjson_0.2.23
## [93] htmlwidgets_1.6.4 farver_2.1.2
## [95] memoise_2.0.1 htmltools_0.5.9
## [97] lifecycle_1.0.5 httr_1.4.7
## [99] bit64_4.6.0-1 bamsignals_1.36.0