diff --git a/.github/workflows/test-coverage-pak.yaml b/.github/workflows/test-coverage-pak.yaml deleted file mode 100644 index aa969c3f..00000000 --- a/.github/workflows/test-coverage-pak.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# Workflow derived from https://github.com/r-lib/actions/tree/master/examples -# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help -on: - push: - branches: [main, develop] - pull_request: - branches: [main, develop] - -name: test-coverage - -jobs: - test-coverage: - runs-on: ubuntu-latest - env: - GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} - - steps: - - uses: actions/checkout@v2 - - - uses: r-lib/actions/setup-r@master - with: - use-public-rspm: true - - - uses: r-lib/actions/setup-r-dependencies@master - with: - extra-packages: covr - - - name: Test coverage - run: covr::codecov() - shell: Rscript {0} diff --git a/DESCRIPTION b/DESCRIPTION index 938f32ee..30457611 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -26,7 +26,8 @@ Imports: ggplot2 (>= 3.3.0), R6, jsonlite, - ospsuite.utils + ospsuite.utils, + patchwork Depends: R (>= 3.6) Encoding: UTF-8 @@ -35,8 +36,7 @@ Roxygen: list(markdown = TRUE) Suggests: knitr, rmarkdown, - testthat (>= 2.1.0), - patchwork + testthat (>= 2.1.0) VignetteBuilder: knitr Collate: 'aaa-utilities.R' @@ -63,6 +63,7 @@ Collate: 'pkratio-get-measure.R' 'plot-boxwhisker.R' 'plot-ddiratio.R' + 'plot-grid.R' 'plot-histogram.R' 'plot-obs-vs-pred.R' 'plot-pkratio.R' diff --git a/NAMESPACE b/NAMESPACE index cb56695c..ceadba76 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -62,6 +62,7 @@ export(ObservedDataMapping) export(PKRatioDataMapping) export(PKRatioPlotConfiguration) export(PlotConfiguration) +export(PlotGridConfiguration) export(RangeDataMapping) export(ResVsPredDataMapping) export(ResVsPredPlotConfiguration) @@ -105,6 +106,7 @@ export(initializePlot) export(loadThemeFromJson) export(plotBoxWhisker) export(plotDDIRatio) +export(plotGrid) export(plotHistogram) export(plotObsVsPred) export(plotPKRatio) diff --git a/R/plot-grid.R b/R/plot-grid.R new file mode 100644 index 00000000..7e26b9f8 --- /dev/null +++ b/R/plot-grid.R @@ -0,0 +1,169 @@ +#' Create a plot grid +#' +#' @param plotGridConfiguration A `PlotGridConfiguration` object, which +#' is an `R6` class object that defines properties of a plot grid (like number +#' of rows, columns, labels, etc.). +#' +#' @description +#' +#' Create a plot grid using the `patchwork::wrap_plots()` function. The required +#' arguments are supplied through the `PlotGridConfiguration` object. +#' +#' @examples +#' +#' library(ggplot2) +#' library(tlf) +#' +#' # only `{tlf}` --------------------- +#' +#' # plots to be arranged in a grid +#' set.seed(123) +#' ls_plots <- list( +#' plotHistogram(x = rnorm(100)), +#' plotHistogram(x = rnorm(100, mean = 3)), +#' plotHistogram(x = rnorm(100, mean = 10)) +#' ) +#' +#' # create an instance of plot configuration class +#' plotGridObj <- PlotGridConfiguration$new(plotList = ls_plots) +#' +#' # specify further customizations for the plot grid +#' plotGridObj$title <- "my combined plot" +#' plotGridObj$subtitle <- "something clever" +#' plotGridObj$caption <- "my sources" +#' plotGridObj$nColumns <- 2L +#' plotGridObj$tagLevels <- "A" +#' plotGridObj$tagPrefix <- "Plot (" +#' plotGridObj$tagSuffix <- ")" +#' +#' # plot the grid +#' plotGrid(plotGridObj) +#' +#' # `{tlf}` and `{ggplot2}` --------------------- +#' +#' # `{tlf}` plot +#' set.seed(123) +#' p1 <- plotBoxWhisker(mtcars, +#' dataMapping = BoxWhiskerDataMapping$new(x = "am", y = "wt"), outliers = FALSE +#' ) +#' +#' # custom `{ggplot2}` plot +#' set.seed(123) +#' p2 <- ggplot(mtcars, aes(wt, mpg)) + +#' geom_point() +#' +#' # create an instance of plot configuration class +#' plotGridObj2 <- PlotGridConfiguration$new(list(p1, p2)) +#' +#' # specify further customizations for the plot grid +#' plotGridObj2$nColumns <- 1L +#' plotGridObj2$tagLevels <- "i" +#' +#' # plot the grid +#' plotGrid(plotGridObj2) +#' +#' @references +#' For more, see: +#' +#' @export +plotGrid <- function(plotGridConfiguration) { + validateIsOfType(plotGridConfiguration, "PlotGridConfiguration") + + patchwork::wrap_plots( + plotGridConfiguration$plotList, + ncol = plotGridConfiguration$nColumns, + nrow = plotGridConfiguration$nRows, + byrow = plotGridConfiguration$byRow, + widths = plotGridConfiguration$widths, + heights = plotGridConfiguration$heights, + guides = plotGridConfiguration$guides, + design = plotGridConfiguration$design + ) + + patchwork::plot_annotation( + title = plotGridConfiguration$title, + subtitle = plotGridConfiguration$subtitle, + caption = plotGridConfiguration$caption, + tag_levels = plotGridConfiguration$tagLevels, + tag_prefix = plotGridConfiguration$tagPrefix, + tag_suffix = plotGridConfiguration$tagSuffix, + tag_sep = plotGridConfiguration$tagSeparator, + theme = plotGridConfiguration$theme + ) +} + + +#' @title Class for creating a plot grid +#' +#' @description +#' +#' R6 class defining the configuration for `{patchwork}` plot grid used to +#' create a grid of plots from `{tlf}`. It holds values for all relevant +#' plot properties. +#' +#' @field plotList A list containing `ggplot` objects. +#' @field title,subtitle,caption Text strings to use for the various plot +#' annotations, where plot refers to the grid of plots as a whole. +#' @field tagLevels A character vector defining the enumeration format to use +#' at each level. Possible values are `'a'` for lowercase letters, `'A'` for +#' uppercase letters, `'1'` for numbers, `'i'` for lowercase Roman numerals, and +#' `'I'` for uppercase Roman numerals. It can also be a list containing +#' character vectors defining arbitrary tag sequences. If any element in the +#' list is a scalar and one of `'a'`, `'A'`, `'1'`, `'i`, or `'I'`, this level +#' will be expanded to the expected sequence. +#' @field tagPrefix,tagSuffix Strings that should appear before or after the +#' tag. +#' @field tagSeparator A separator between different tag levels +#' @field theme A ggplot theme specification to use for the plot. Only elements +#' related to the titles as well as plot margin and background will be used. +#' @field nColumns,nRows The dimensions of the grid to create - if both are +#' `NULL` it will use the same logic as [facet_wrap()][ggplot2::facet_wrap] to +#' set the dimensions +#' @field byRow Analogous to `byrow` in [matrix()][base::matrix]. If `FALSE` the +#' plots will be filled in in column-major order. +#' @field widths,heights The relative widths and heights of each column and row +#' in the grid. Will get repeated to match the dimensions of the grid. +#' @field guides A string specifying how guides should be treated in the layout. +#' `'collect'` will collect guides below to the given nesting level, removing +#' duplicates. `'keep'` will stop collection at this level and let guides be +#' placed alongside their plot. `auto` will allow guides to be collected if a +#' upper level tries, but place them alongside the plot if not. If you modify +#' default guide "position" with [theme(legend.position=...)][ggplot2::theme] +#' while also collecting guides you must apply that change to the overall +#' patchwork. +#' @field design Specification of the location of areas in the layout. Can +#' either be specified as a text string or by concatenating calls to [area()] +#' together. See the examples for further information on use. +#' +#' @return A `PlotGridConfiguration` object. +#' +#' @export +PlotGridConfiguration <- R6::R6Class( + "PlotGridConfiguration", + public = list( + plotList = NULL, + title = NULL, + subtitle = NULL, + caption = NULL, + tagLevels = NULL, + tagPrefix = NULL, + tagSuffix = NULL, + tagSeparator = NULL, + theme = NULL, + nColumns = NULL, + nRows = NULL, + byRow = NULL, + widths = NULL, + heights = NULL, + guides = NULL, + design = NULL, + + #' @description Create an instance of `PlotGridConfiguration` class. + #' + #' @param plotList A list containing `ggplot` objects. + #' + #' @return A `PlotGridConfiguration` object. + initialize = function(plotList = NULL) { + self$plotList <- plotList + } + ) +) diff --git a/man/PlotGridConfiguration.Rd b/man/PlotGridConfiguration.Rd new file mode 100644 index 00000000..1ab4b58b --- /dev/null +++ b/man/PlotGridConfiguration.Rd @@ -0,0 +1,125 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plot-grid.R +\name{PlotGridConfiguration} +\alias{PlotGridConfiguration} +\title{Class for creating a plot grid} +\value{ +A \code{PlotGridConfiguration} object. +} +\description{ +R6 class defining the configuration for \code{{patchwork}} plot grid used to +create a grid of plots from \code{{tlf}}. It holds values for all relevant +plot properties. +} +\section{Public fields}{ +\if{html}{\out{
}} +\describe{ +\item{\code{plotList}}{A list containing \code{ggplot} objects.} + +\item{\code{title, subtitle, caption}}{Text strings to use for the various plot +annotations, where plot refers to the grid of plots as a whole.} + +\item{\code{tagLevels}}{A character vector defining the enumeration format to use +at each level. Possible values are \code{'a'} for lowercase letters, \code{'A'} for +uppercase letters, \code{'1'} for numbers, \code{'i'} for lowercase Roman numerals, and +\code{'I'} for uppercase Roman numerals. It can also be a list containing +character vectors defining arbitrary tag sequences. If any element in the +list is a scalar and one of \code{'a'}, \code{'A'}, \code{'1'}, \verb{'i}, or \code{'I'}, this level +will be expanded to the expected sequence.} + +\item{\code{tagPrefix, tagSuffix}}{Strings that should appear before or after the +tag.} + +\item{\code{tagSeparator}}{A separator between different tag levels} + +\item{\code{theme}}{A ggplot theme specification to use for the plot. Only elements +related to the titles as well as plot margin and background will be used.} + +\item{\code{nColumns, nRows}}{The dimensions of the grid to create - if both are +\code{NULL} it will use the same logic as \link[ggplot2:facet_wrap]{facet_wrap()} to +set the dimensions} + +\item{\code{byRow}}{Analogous to \code{byrow} in \link[base:matrix]{matrix()}. If \code{FALSE} the +plots will be filled in in column-major order.} + +\item{\code{widths, heights}}{The relative widths and heights of each column and row +in the grid. Will get repeated to match the dimensions of the grid.} + +\item{\code{guides}}{A string specifying how guides should be treated in the layout. +\code{'collect'} will collect guides below to the given nesting level, removing +duplicates. \code{'keep'} will stop collection at this level and let guides be +placed alongside their plot. \code{auto} will allow guides to be collected if a +upper level tries, but place them alongside the plot if not. If you modify +default guide "position" with \link[ggplot2:theme]{theme(legend.position=...)} +while also collecting guides you must apply that change to the overall +patchwork.} + +\item{\code{design}}{Specification of the location of areas in the layout. Can +either be specified as a text string or by concatenating calls to \code{\link[=area]{area()}} +together. See the examples for further information on use.} +} +\if{html}{\out{
}} +} +\section{Active bindings}{ +\if{html}{\out{
}} +\describe{ +\item{\code{title, subtitle, caption}}{Text strings to use for the various plot +annotations, where plot refers to the grid of plots as a whole.} + +\item{\code{tagPrefix, tagSuffix}}{Strings that should appear before or after the +tag.} + +\item{\code{nColumns, nRows}}{The dimensions of the grid to create - if both are +\code{NULL} it will use the same logic as \link[ggplot2:facet_wrap]{facet_wrap()} to +set the dimensions} + +\item{\code{widths, heights}}{The relative widths and heights of each column and row +in the grid. Will get repeated to match the dimensions of the grid.} +} +\if{html}{\out{
}} +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-new}{\code{PlotGridConfiguration$new()}} +\item \href{#method-clone}{\code{PlotGridConfiguration$clone()}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-new}{}}} +\subsection{Method \code{new()}}{ +Create an instance of \code{PlotGridConfiguration} class. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{PlotGridConfiguration$new(plotList = NULL)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{plotList}}{A list containing \code{ggplot} objects.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A \code{PlotGridConfiguration} object. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{PlotGridConfiguration$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/man/plotGrid.Rd b/man/plotGrid.Rd new file mode 100644 index 00000000..4e306bab --- /dev/null +++ b/man/plotGrid.Rd @@ -0,0 +1,74 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plot-grid.R +\name{plotGrid} +\alias{plotGrid} +\title{Create a plot grid} +\usage{ +plotGrid(plotGridConfiguration) +} +\arguments{ +\item{plotGridConfiguration}{A \code{PlotGridConfiguration} object, which +is an \code{R6} class object that defines properties of a plot grid (like number +of rows, columns, labels, etc.).} +} +\description{ +Create a plot grid using the \code{patchwork::wrap_plots()} function. The required +arguments are supplied through the \code{PlotGridConfiguration} object. +} +\examples{ + +library(ggplot2) +library(tlf) + +# only `{tlf}` --------------------- + +# plots to be arranged in a grid +set.seed(123) +ls_plots <- list( + plotHistogram(x = rnorm(100)), + plotHistogram(x = rnorm(100, mean = 3)), + plotHistogram(x = rnorm(100, mean = 10)) +) + +# create an instance of plot configuration class +plotGridObj <- PlotGridConfiguration$new(plotList = ls_plots) + +# specify further customizations for the plot grid +plotGridObj$title <- "my combined plot" +plotGridObj$subtitle <- "something clever" +plotGridObj$caption <- "my sources" +plotGridObj$nColumns <- 2L +plotGridObj$tagLevels <- "A" +plotGridObj$tagPrefix <- "Plot (" +plotGridObj$tagSuffix <- ")" + +# plot the grid +plotGrid(plotGridObj) + +# `{tlf}` and `{ggplot2}` --------------------- + +# `{tlf}` plot +set.seed(123) +p1 <- plotBoxWhisker(mtcars, + dataMapping = BoxWhiskerDataMapping$new(x = "am", y = "wt"), outliers = FALSE +) + +# custom `{ggplot2}` plot +set.seed(123) +p2 <- ggplot(mtcars, aes(wt, mpg)) + + geom_point() + +# create an instance of plot configuration class +plotGridObj2 <- PlotGridConfiguration$new(list(p1, p2)) + +# specify further customizations for the plot grid +plotGridObj2$nColumns <- 1L +plotGridObj2$tagLevels <- "i" + +# plot the grid +plotGrid(plotGridObj2) + +} +\references{ +For more, see: \url{https://patchwork.data-imaginist.com/articles/patchwork.html} +} diff --git a/tests/testthat/test-plot-grid.R b/tests/testthat/test-plot-grid.R new file mode 100644 index 00000000..a3d762b5 --- /dev/null +++ b/tests/testthat/test-plot-grid.R @@ -0,0 +1,45 @@ +test_that("plots grid produces error with wrong input type", { + expect_error(plotGrid(DataSet$new(name = "DS"))) +}) + +test_that("plots grid is rendered correctly", { + skip_if_not_installed("vdiffr") + skip_if(getRversion() < "4.1") + + library(tlf) + + set.seed(123) + ls_plots <- list( + # first plot + plotBoxWhisker(mtcars, + dataMapping = BoxWhiskerDataMapping$new(x = "am", y = "wt"), outliers = FALSE + ), + # second plot + plotBoxWhisker(ToothGrowth, + dataMapping = BoxWhiskerDataMapping$new(x = "supp", y = "len") + ) + ) + + plotGridObj <- PlotGridConfiguration$new(ls_plots) + + plotGridObj$title <- "my combined plot" + plotGridObj$subtitle <- "something clever" + plotGridObj$caption <- "something dumb" + plotGridObj$nColumns <- 2L + plotGridObj$tagLevels <- "A" + plotGridObj$tagPrefix <- "Plot (" + plotGridObj$tagSuffix <- ")" + + pGrid <- plotGrid(plotGridObj) + + expect_s3_class(pGrid, "ggplot") + + # TODO: turn on once you figure out why Appveyor produces slightly different plot + # The differences are not discernible to the naked eye, so hard to diagnose at the moment + # but also makes it not risky to skip the test at the moment + # set.seed(123) + # vdiffr::expect_doppelganger( + # title = "plotGrid works as expected", + # fig = plotGrid(plotGridObj) + # ) +})