This package implements utilities for an opinionated way of re-using content in bookdown books. The general idea is that you can take objects from a “donor” chapter and re-use them in another “acceptor” chapter, where the two chapters can be in different books altogether. We can also generate links between books hosted by Bioconductor, allowing us to compartmentalize content without sacrificing integration of book contents. Most of these ideas were developed in the process for the Orchestrating single-cell analysis book, but are hopefully applicable to other projects.
A Bioconductor book is implemented as an R package that contains
bookdown
book in its inst/book directory. That is to say,
inst/book contains at least index.Rmd and
_bookdown.yml such that
bookdown::render_book("inst/book/index.Rmd") will compile
the book. We aim to support any bookdown-compatible
configuration within inst/book, though the most well-tested
configuration avoids using input Rmarkdown files from subdirectories and
places the compilation output in a docs/ subdirectory.
The DESCRIPTION file should contain, in its
Depends:, all packages that are used within the book
chapters. This is somewhat tedious to extract manually so we provide the
updateDependencies() function to automate the process. We
recommend setting up a CI/CD job (e.g., via GitHub Actions, see examples
here)
to run this automatically. This allows users to install the book package
and automatically install all of the required dependencies.
The vignettes/ directory should contain a Makefile that
builds the book as part of the package build process. This Makefile is
created by running the configureBook() function, amongst
other things that we will discuss later.
Sometimes we generate objects in one “donor” chapter that we want to re-use in other “acceptor” chapters. To do so, we compile potential donor chapters with knitr caching turned on and retrieve arbitrary objects from the cache in acceptor chapters. This avoids the need to (i) re-type the code required to generate the object and (ii) repeat the possibly time-consuming compilation process.
To demonstrate, we will use an example donor chapter included inside the rebook package itself. We copy the Rmarkdown file to a temporary location so as avoid modifying the contents of the installation directory.
example0 <- system.file("example", "test.Rmd", package="rebook")
example <- tempfile(fileext=".Rmd")
file.copy(example0, example)## [1] TRUE
To perform this retrieval inside some acceptor chapter (in this case,
this vignette), we call the extractCached() function. We
supply the path to the donor chapter, the object we want to extract, and
the name of the latest chunk in which that object might appear. The
function will then search through the cache to identify the relevant
version of the object and pull it out into the current R session. For
example:
## [1] "RAWR!"
The code leading up to and including the named chunk is also included
in a collapsible box, to unobtrusively provide users with the context in
which the object was generated. Note that proper functioning of the
collapsible box depends on chapterPreamble() having been
called, see below for details. In a real book, a link is also created to
the donor chapter in place of the ?? shown here.
Multiple objects can be retrieved in this manner:
## [1] "GAO GAO"
## [1] "pew pew"
Searching is done by chunk so as to disambiguate between objects with the same name across multiple chunks. This includes objects that are repeatedly modified, allowing us to retrieve the object different states within the donor. For example, we can pull out the same named variable but from a later chunk (and thus with a different value):
## [1] "I'm back."
We can also pull out objects that are not actually referenced in the requested chunk, as long as it was created in one of the preceding chunks:
## [1] "I'm back."
## [1] "beep beep"
If the donor chapter has not yet been compiled,
extractCached() will automatically compile it to create the
cache from which to extract content. This allows us to refer to donor
chapters that follow the current acceptor chapter; no extra
time is used as the bookdown
compilation of the donor can simply use the newly cached content.
Note that this system imposes some restrictions on the formatting of
the code chunks in the donor report. This is mostly caused by the
limitations of our custom Rmarkdown parser - see
?extractCached for more details.
When writing a large book, we experience a tension between modularization and interconnectivity. Ideally, we would break up a large book into multiple smaller components, reducing the fragility of the compilation process and simplifying development. However, this would make it harder to create links between related parts of the book, given that bookdown’s automatic link resolution is limited to references within the same book. Without links, we lose the synergistic benefits of making a book in the first place.
To get around this, rebook
provides the link() function to easily link to references
in other Bioconductor books. If the destination book is installed as a
package, we can simply use the following in inline code chunks:
## [1] "[Introduction Chapter 1](http://bioconductor.org/books/3.23/OSCA.intro/installation.html#installation)"
## [1] "[Introduction Section 3.2](http://bioconductor.org/books/3.23/OSCA.intro/getting-scrna-seq-datasets.html#some-comments-on-experimental-design)"
## [1] "[Introduction Figure 4.1](http://bioconductor.org/books/3.23/OSCA.intro/the-singlecellexperiment-class.html#fig:sce-structure)"
This allows developers to create highly modular books while retaining
the convenience of easily linking between books. Note that, if we
link() to another book, we should include the relevant book
package as a Suggests: for our book.
Conversely, calling configureBook() will automatically
ensure that our current book - once it builds on Bioconductor - can
serve as a link destination for other books. This is done by running
through all the book chapters, extracting the references and building an
index for re-use in other packages.
In much the same way that extractCached() works for
sharing objects between chapters of the same book,
extractFromPackage() enables use to share objects between
chapters of different books. Namely:
extractFromPackage("lun-416b.Rmd", chunk="clustering",
objects='sce.416b', package="OSCA.workflows")#--- loading ---#
library(scRNAseq)
sce.416b <- LunSpikeInData(which="416b")
sce.416b$block <- factor(sce.416b$block)
#--- gene-annotation ---#
library(AnnotationHub)
ens.mm.v97 <- AnnotationHub()[["AH73905"]]
rowData(sce.416b)$ENSEMBL <- rownames(sce.416b)
rowData(sce.416b)$SYMBOL <- mapIds(ens.mm.v97, keys=rownames(sce.416b),
keytype="GENEID", column="SYMBOL")
rowData(sce.416b)$SEQNAME <- mapIds(ens.mm.v97, keys=rownames(sce.416b),
keytype="GENEID", column="SEQNAME")
library(scater)
rownames(sce.416b) <- uniquifyFeatureNames(rowData(sce.416b)$ENSEMBL,
rowData(sce.416b)$SYMBOL)
#--- quality-control ---#
mito <- which(rowData(sce.416b)$SEQNAME=="MT")
stats <- perCellQCMetrics(sce.416b, subsets=list(Mt=mito))
qc <- quickPerCellQC(stats, percent_subsets=c("subsets_Mt_percent",
"altexps_ERCC_percent"), batch=sce.416b$block)
sce.416b <- sce.416b[,!qc$discard]
#--- normalization ---#
library(scran)
sce.416b <- computeSumFactors(sce.416b)
sce.416b <- logNormCounts(sce.416b)
#--- variance-modelling ---#
dec.416b <- modelGeneVarWithSpikes(sce.416b, "ERCC", block=sce.416b$block)
chosen.hvgs <- getTopHVGs(dec.416b, prop=0.1)
#--- batch-correction ---#
library(limma)
assay(sce.416b, "corrected") <- removeBatchEffect(logcounts(sce.416b),
design=model.matrix(~sce.416b$phenotype), batch=sce.416b$block)
#--- dimensionality-reduction ---#
sce.416b <- runPCA(sce.416b, ncomponents=10, subset_row=chosen.hvgs,
exprs_values="corrected", BSPARAM=BiocSingular::ExactParam())
set.seed(1010)
sce.416b <- runTSNE(sce.416b, dimred="PCA", perplexity=10)
#--- clustering ---#
my.dist <- dist(reducedDim(sce.416b, "PCA"))
my.tree <- hclust(my.dist, method="ward.D2")
library(dynamicTreeCut)
my.clusters <- unname(cutreeDynamic(my.tree, distM=as.matrix(my.dist),
minClusterSize=10, verbose=0))
colLabels(sce.416b) <- factor(my.clusters)## class: SingleCellExperiment
## dim: 46604 185
## metadata(0):
## assays(3): counts logcounts corrected
## rownames(46604): 4933401J01Rik Gm26206 ... CAAA01147332.1
## CBFB-MYH11-mcherry
## rowData names(4): Length ENSEMBL SYMBOL SEQNAME
## colnames(185): SLX-9555.N701_S502.C89V9ANXX.s_1.r_1
## SLX-9555.N701_S503.C89V9ANXX.s_1.r_1 ...
## SLX-11312.N712_S507.H5H5YBBXX.s_8.r_1
## SLX-11312.N712_S517.H5H5YBBXX.s_8.r_1
## colData names(10): cell line cell type ... sizeFactor label
## reducedDimNames(2): PCA TSNE
## mainExpName: endogenous
## altExpNames(2): ERCC SIRV
This is achieved by exploiting the cache that is created when the
donor book undergoes R CMD build. However, if the donor has
not yet been compiled, then extractFromPackage() will do so
to make sure that the cache exists. This allows for efficient yet robust
re-use of objects across multiple books, at least for donor chapters
that meet certain requirements.
If we use content from another book, we should include the donor
package as a Suggests: for our book. (Sometimes they are
placed as Imports: to encourage the build system to use the
most efficient compilation order.) If we want our book to serve as a
donor, we need to ensure that the book compilation generates the cache
in an appropriate location - this is automatically handled by
configureBook()’s Makefile.
As one can see from the examples above, extractCached()
will create a collapsible HTML element containing the code used to
generate the requested object(s). This informs reader about the
provenance of the object without overwhelming them. This is also used to
achieve pretty sessionInfo() printing, as shown below.
R version 4.6.1 (2026-06-24)
Platform: x86_64-pc-linux-gnu
Running under: Ubuntu 26.04.1 LTS
Matrix products: default
BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.32.so; LAPACK version 3.12.0
locale:
[1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
[3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
[5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
[7] LC_PAPER=en_US.UTF-8 LC_NAME=C
[9] LC_ADDRESS=C LC_TELEPHONE=C
[11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
time zone: Etc/UTC
tzcode source: system (glibc)
attached base packages:
[1] stats graphics grDevices utils datasets methods base
other attached packages:
[1] rebook_1.22.0 BiocStyle_2.40.0
loaded via a namespace (and not attached):
[1] Matrix_1.7-6 jsonlite_2.0.0
[3] compiler_4.6.1 BiocManager_1.30.27
[5] filelock_1.0.3 SummarizedExperiment_1.42.0
[7] Biobase_2.72.0 GenomicRanges_1.64.0
[9] jquerylib_0.1.4 Seqinfo_1.2.0
[11] IRanges_2.46.0 yaml_2.3.12
[13] fastmap_1.2.0 lattice_0.23-1
[15] XVector_0.52.0 R6_2.6.1
[17] S4Arrays_1.12.1 generics_0.1.4
[19] knitr_1.52 BiocGenerics_0.58.1
[21] graph_1.90.0 XML_3.99-0.25
[23] DelayedArray_0.38.2 MatrixGenerics_1.24.0
[25] maketools_1.3.2 bslib_0.12.0
[27] rlang_1.3.0 cachem_1.1.0
[29] dir.expiry_1.20.0 xfun_0.61
[31] sass_0.4.10 sys_3.4.3
[33] otel_0.2.0 SparseArray_1.12.3
[35] cli_3.6.6 grid_4.6.1
[37] digest_0.6.39 rappdirs_0.3.4
[39] lifecycle_1.0.5 S4Vectors_0.50.3
[41] SingleCellExperiment_1.34.0 evaluate_1.0.5
[43] codetools_0.2-20 buildtools_1.0.0
[45] CodeDepends_0.6.7 abind_1.4-8
[47] stats4_4.6.1 rmarkdown_2.32
[49] matrixStats_1.5.0 tools_4.6.1
[51] htmltools_0.5.9
The collapsible element class is defined using code in
chapterPreamble(), which should be called at the top of
every chapter with the results="asis" chunk option to set
up the compilation environment. We suggest calling
chapterPreamble() after the chapter title is
defined with # Chapter Title, as bookdown
seems to strip out all preceding content.