metrumresearchgroup / pmplots

Plots for Pharmacometrics
https://metrumresearchgroup.github.io/pmplots
8 stars 1 forks source link

Rotate axis tick labels in a list of plots; name plot lists #96

Closed kylebaron closed 3 months ago

kylebaron commented 3 months ago

Summary

This PR tries to accomplish several things, with the common thread of modifying or using lists of plots, referring to names in that list.

  1. Global update to add names to lists of plots where / when this is the output
  2. Add a function to rotate axis tick labels in a list of plots by name
  3. Arrange plots in a list using patchwork

New function rot_xy(). This is a wrapper around all of the other rot functions. It is generic with methods for gg, patchwork and list. The list method just calls rot_at(). The other methods call this internal worker .rotxy(), which knows how to use rot_x() and rot_y() for either gg objects (+) or patchwork (&). I wanted this master wrapper so we can pass in any of these objects and use a consistent interface. This also provides a "functional" approach for rotating axes, which I've wanted for some time; but I still expect to use lots of rot_x() and rot_y() getting "added" to the ggplot.

New function: rot_at(), which provides functionality like rot_x() and rot_y() for a list of plots, including gg and patchwork objects. The function expects all the plots to be named so we can selectively rotate particular plots in the list. By default, all plot axes will be rotated. To selectively rotate, use the at argument for exact names or re to pass a regular expression.

Rotation is done by an non-exported generic function (.rotxy()) with methods for gg and patchwork objects. We also pass an axis argument indicating if we want x or y rotated. 99.9% of the time, we do this to the x axis so this is reasonable IMO in order to cut down the amount of code we need to write.

Because this is expecting names, this PR also adds names to list plot outputs; these plots were already vectorized (pass a data frame and a vector of y or x values and get a list of plots back) but we are now naming them so we can rotate. The functions are list_plot_x(), list_plot_y(), list_plot_xy() and list_plot_yx().

Now that we're generating lists of plots with names, I wanted to be able to arrange them using patchwork through a with() method (e.g., with(plots, WT + ALB / SCR). Rather than adding a class attribute to these lists of plots and creating a new method for with(), I decided to just create pm_with() which checks inputs and evaluates the plot assembly expression.

EDIT: after testing rot_at() with axis = "y", I wanted to make rot_y() more consistent with rot_x(); this means adding some equivalent convenience stuff I wrote into rot_x(); I'm not crazy about it, but it works and I think having the vertical argument to rot_y() is nice.

Example

library(patchwork)
library(pmplots)
#> Loading required package: ggplot2
library(dplyr)
#> 
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#> 
#>     filter, lag
#> The following objects are masked from 'package:base':
#> 
#>     intersect, setdiff, setequal, union
library(purrr)
#> 
#> Attaching package: 'purrr'
#> The following object is masked from 'package:base':
#> 
#>     %||%

data <- pmplots_data_obs()
id <- pmplots_data_id()
cont <- c("WT//Weight (kg)", "ALB//Albumin (mg/dL)", "AGE//Age (years)")
cats <- c("STUDYc//Study", "CPc//Child-Pugh")
etas <- paste0("ETA", 1:3)

Rotate

Default output

x <- eta_cat(id, x = cats, y = etas[1])
names(x)
#> [1] "STUDYc" "CPc"
pm_grid(x)

Rotated

UPDATE: umbrella wrapper to rotate anything

A list

a <- rot_xy(x, at = "STUDYc", vertical = TRUE)
pm_grid(a)

Ok not to name

pm_grid(rot_xy(x, vertical = TRUE))

But expecting an error if the list is not named

try(rot_xy(unname(x)))
#> Error in rot_at(x, axis = axis, ...) : `x` must be named.

In this case, map

map(unname(x), rot_xy, vertical = TRUE) %>% pm_grid()

or

map_at(unname(x), rot_xy, .at = 2, vertical = TRUE) %>% pm_grid()

ggplot

gg <- dv_pred(data)
gg <- rot_xy(gg, angle = 180)
gg
#> `geom_smooth()` using formula = 'y ~ x'

patchwork

pw <- pm_grid(a)
pw <- rot_xy(pw, axis = "y", vertical = TRUE)
pw

Pass a specific column name Also, we can pass arguments through to rot_x() and rot_y()

y <- rot_at(x, "STUDYc", vertical = TRUE)
pm_grid(y)

Pass a regular expression

z <- rot_at(x, re = "CP", angle = 45)
pm_grid(z)

Change the axis

zz <- rot_at(x, axis = "y", vertical = TRUE)
pm_grid(zz)

Check out refactored rot_y()

Rotate both x and y axes with vertical = TRUE

rot_at(x, axis = "y", vertical = TRUE) %>%
  rot_at(axis = "x", vertical = TRUE) %>%
  pm_grid()

Name patterns

This gives us a single list

x <- cont_cat(id, x = cats, y  = cont[1])

We get the categorical names back (x)

names(x)
#> [1] "STUDYc" "CPc"
pm_grid(x)

We get the continuous names back (x)

x <- eta_cont(id, x = cont, y = etas[1])
names(x)
#> [1] "WT"  "ALB" "AGE"

We get the eta names back (y)

x <- eta_cont(id, x = cont[1], y = etas)
names(x)
#> [1] "ETA1" "ETA2" "ETA3"

We get YvX back

x <- eta_cont(id, x = cont, y = etas)
names(x)
#> [1] "ETA1vWT"  "ETA1vALB" "ETA1vAGE" "ETA2vWT"  "ETA2vALB" "ETA2vAGE" "ETA3vWT" 
#> [8] "ETA3vALB" "ETA3vAGE"

We get y names

x <- eta_covariate(id, x = cont, y = etas)
names(x)
#> [1] "ETA1" "ETA2" "ETA3"

We get y names

x <- eta_covariate(id, x = cont[1], y = etas)
names(x)
#> [1] "ETA1" "ETA2" "ETA3"

We still get y names

x <- eta_covariate(id, x = cont, y = etas[1])
names(x)
#> [1] "ETA1"

We get x names when we transpose

x <- eta_covariate(id, x = cont, y = etas[1], transpose = TRUE)
names(x)
#> [1] "WT"  "ALB" "AGE"

Check list_plot() behavior

x <- list_plot_xy(id, x = cats, y = cont, .fun = cont_cat)
names(x)
#> [1] "WTvSTUDYc"  "ALBvSTUDYc" "AGEvSTUDYc" "WTvCPc"     "ALBvCPc"   
#> [6] "AGEvCPc"
x$ALBvSTUDYc

x$AGEvCPc


x <- list_plot_yx(id, x = cats, y = cont, .fun = cont_cat)
names(x)
#> [1] "WTvSTUDYc"  "WTvCPc"     "ALBvSTUDYc" "ALBvCPc"    "AGEvSTUDYc"
#> [6] "AGEvCPc"
x$ALBvSTUDYc

x$AGEvCPc

pm_with()

We get y names when we take the list as-is

x <- eta_covariate_list(data, x = cont, y = etas[1])
pm_with(x$ETA1, WT+ALB/AGE)
#> `geom_smooth()` using formula = 'y ~ x'
#> `geom_smooth()` using formula = 'y ~ x'
#> `geom_smooth()` using formula = 'y ~ x'

Created on 2024-07-17 with reprex v2.1.1

kyleam commented 3 months ago
z <- rot_at(x, re = "CP", angle = 45)
pm_grid(x)

There's a typo in this example, but pm_grid(z) shows the expected output (Child-Pugh labels are rotated 45 degrees).

KatherineKayMRG commented 3 months ago

I love all this extra functionality! I was following along off-line as the options developed and this all looks to work as discussed off-line

kylebaron commented 3 months ago

Thanks @KatherineKayMRG and @kyleam for reviewing. This all started out as such a minor convenience function (probably replaced now with ggeasy), but we worked ourselves into a place where we could really benefit from some good tools to really simplify the code. I'm feeling good that we can drop the last vestiges of the workarounds we've been carrying along for a while now.

kylebaron commented 3 months ago

Going to merge; I believe all the comments were implemented as suggested.