diff --git a/NAMESPACE b/NAMESPACE index 84f0b44..7a6fa5e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -14,6 +14,14 @@ S3method(qp_mark_outliers,data.frame) S3method(qp_mark_outliers,list) S3method(qp_remove_empty,data.frame) S3method(qp_remove_empty,list) +S3method(qp_samples_all,data.frame) +S3method(qp_samples_all,list) +S3method(qp_samples_summary,data.frame) +S3method(qp_samples_summary,list) +S3method(qp_standards_all,data.frame) +S3method(qp_standards_all,list) +S3method(qp_standards_summary,data.frame) +S3method(qp_standards_summary,list) S3method(qp_summarize,data.frame) S3method(qp_summarize,list) S3method(qp_tidy,character) @@ -31,6 +39,7 @@ export(qp_mark_outliers) export(qp_plot_plate) export(qp_plot_standards) export(qp_remove_empty) +export(qp_report) export(qp_summarize) export(qp_tidy) importFrom(rlang,.data) diff --git a/R/color.R b/R/color.R new file mode 100644 index 0000000..d583901 --- /dev/null +++ b/R/color.R @@ -0,0 +1,18 @@ +qp_pal <- grDevices::colorRampPalette( + c("darkseagreen1", "#A763A0", "#3D1452"), + bias = 0.9 +)(100) + +# Notes: +# Abs vs apparent color +# 0.1 - gray - between green and purple +# 0.31 - around mediumorchid3 +# Let's call 0.5 the max since you shouldn't go too far above anyway + +abs_to_col <- function(abs, pal) { + scaled <- abs * 200 + idx <- ifelse(scaled < 1, 1, scaled) + idx <- ifelse(idx > 100, 100, idx) + idx <- round(idx) + pal[idx] +} diff --git a/R/qp_dilute.R b/R/qp_dilute.R index 2874fe3..747a07b 100644 --- a/R/qp_dilute.R +++ b/R/qp_dilute.R @@ -1,8 +1,8 @@ #' Calculate dilutions from predicted concentrations #' #' @param x A `data.frame` or `list` containing a `data.frame` named `qp` with -#' a column named `.pred_conc` or `.mean_pred_conc`. If both, will favor -#' `.mean_pred_conc`. +#' a column named `.pred_conc` or `.pred_conc_mean`. If both, will favor +#' `.pred_conc_mean`. #' @param target_conc Numeric vector. Target concentration in (mg/mL) protein. #' If length == 1, recycled. #' @param target_vol Target volume in uL. If length == 1, recycled. @@ -21,7 +21,7 @@ qp_dilute <- function(x, ...) { #' @export qp_dilute.data.frame <- function(x, target_conc = NULL, target_vol = 15, remove_standards = FALSE, ...) { - check_has_cols(x, c(".pred_conc", ".mean_pred_conc"), type = "or") + check_has_cols(x, c(".pred_conc", ".pred_conc_mean"), type = "or") if (remove_standards || is.null(target_conc)) { check_has_cols(x, "sample_type") @@ -36,7 +36,7 @@ qp_dilute.data.frame <- function(x, target_conc = NULL, target_vol = 15, } conc_col_name <- ifelse( - has_cols(x, ".mean_pred_conc"), ".mean_pred_conc", ".pred_conc" + has_cols(x, ".pred_conc_mean"), ".pred_conc_mean", ".pred_conc" ) if (is.null(target_conc)) { @@ -54,7 +54,8 @@ qp_dilute.data.frame <- function(x, target_conc = NULL, target_vol = 15, x |> dplyr::bind_cols(dils) |> - tidyr::unnest_wider("value") + tidyr::unnest_wider("value") |> + dplyr::mutate(.target_conc = target_conc, .target_vol = target_vol) } #' @rdname qp_dilute diff --git a/R/qp_plot_plate.R b/R/qp_plot_plate.R index 7498f17..b897db0 100644 --- a/R/qp_plot_plate.R +++ b/R/qp_plot_plate.R @@ -13,13 +13,16 @@ qp_plot_plate <- function(x, size = 15) { ggplot2::ggplot(x, ggplot2::aes( x = .data$.col, y = .data$.row, - color = .data$.abs + color = abs_to_col(.data$.abs, qp_pal) )) + ggplot2::geom_point(size = size) + ggplot2::geom_text( - ggplot2::aes(label = round(.data$.abs, 2)), - color = "black" + ggplot2::aes( + label = round(.data$.abs, 2), + color = ifelse(.data$.abs < 0.35, "black", "white") + ), + size = size / 3 ) + ggplot2::scale_y_reverse() + - ggplot2::scale_color_gradient(low = "darkseagreen1", high = "mediumpurple3") + ggplot2::scale_color_identity() } diff --git a/R/qp_report.R b/R/qp_report.R index 0db50a7..68d2807 100644 --- a/R/qp_report.R +++ b/R/qp_report.R @@ -1,3 +1,261 @@ -qp_report <- function(qp) { - "" +#' Create a report for a protein quantificaiton experiment +#' +#' @param qp Likely the output from `qp` AND `qp_dilute`. +#' @param output_file Character. The path of the file to export, +#' including `.html` +#' @param other Generally used for Shiny application. Assumes a +#' named list of key-values that will be used to document report +#' parameters. +#' +#' @export +#' @examples +#' \dontrun{ +#' absorbances |> +#' qp() |> +#' qp_dilute() |> +#' qp_report( +#' "~/my_report.html", +#' other = list(key = "value") # Essentially metadata +#' ) +#' } +qp_report <- function(qp, output_file, other = list()) { + rmarkdown::render( + system.file( + "rmarkdown/templates/quantify-protein-report/skeleton/skeleton.Rmd", + package = "qp" + ), + output_file = output_file, + params = list(qp = qp, other = other) + ) +} + +# Standards ---- +## All ---- +qp_standards_all <- function(x) { + UseMethod("qp_standards_all") +} + +#' @export +qp_standards_all.data.frame <- function(x) { + x <- add_coords_col(x) + table_data <- add_well_circles_plot_col( + x, + "standard", + c(".sample_name", ".abs", ".is_outlier", ".pred_conc", ".coord", ".conc"), + c(".sample_name", ".pred_conc", ".conc") + ) + table_data |> + make_gt(35, 1, c(".abs", ".pred_conc", ".conc")) |> + gt::cols_label( + .sample_name = "Sample", .conc = "[Actual]", + .pred_conc = "[Predicted]", .abs = "Absorbance", gg = "" + ) +} + +#' @export +qp_standards_all.list <- function(x) { + x$qp <- qp_standards_all.data.frame(x$qp) + x +} + +## Summary ---- +qp_standards_summary <- function(x) { + UseMethod("qp_standards_summary") +} + +#' @export +qp_standards_summary.data.frame <- function(x) { + x <- add_coords_col(x) + table_data <- add_well_circles_plot_col( + x, + "standard", + c(".sample_name", ".pred_conc_mean", ".is_outlier", ".abs", ".coord"), + c(".sample_name", ".pred_conc_mean"), + summary = TRUE + ) + table_data |> + make_gt(50, 2, ".pred_conc_mean") |> + gt::cols_label( + .sample_name = "Sample", .pred_conc_mean = "Concentration", gg = "" + ) +} + +#' @export +qp_standards_summary.list <- function(x) { + x$qp <- qp_standards_summary.data.frame(x$qp) + x +} + + +# Samples ----- +## All ---- +qp_samples_all <- function(x) { + UseMethod("qp_samples_all") +} + +#' @export +qp_samples_all.data.frame <- function(x) { + x <- add_coords_col(x) + table_data <- add_well_circles_plot_col( + x, + "unknown", + c(".sample_name", ".abs", ".is_outlier", ".pred_conc", ".coord"), + group_vars = c(".sample_name", ".pred_conc") + ) + table_data |> + make_gt(35, 1, c(".abs", ".pred_conc")) |> + gt::cols_label( + .sample_name = "Sample", .pred_conc = "[Predicted]", + .abs = "Absorbance", gg = "" + ) +} + +#' @export +qp_samples_all.list <- function(x) { + x$qp <- qp_samples_all.data.frame(x$qp) + x +} + +## Summary ---- +qp_samples_summary <- function(x) { + UseMethod("qp_samples_summary") +} + +#' @export +qp_samples_summary.data.frame <- function(x) { + x <- add_coords_col(x) + table_data <- add_well_circles_plot_col( + x, + "unknown", + c(".sample_name", ".pred_conc_mean", ".is_outlier", ".abs", ".coord"), + c(".sample_name", ".pred_conc_mean"), + summary = TRUE + ) + table_data |> + make_gt(50, 2, ".pred_conc_mean") |> + gt::cols_label( + .sample_name = "Sample", .pred_conc_mean = "Concentration", gg = "" + ) +} + +#' @export +qp_samples_summary.list <- function(x) { + x$qp <- qp_samples_summary.data.frame(x$qp) + x +} + +make_well_circles_plot <- function(df) { + x <- seq_along(df$.abs) + y <- 1 + if (nrow(df) == 1) { + size <- 120 + } else { + size <- 200 / nrow(df) + } + + well <- data.frame(x = x, y = y, color = abs_to_col(df$.abs, qp_pal)) + + text <- data.frame( + x = x, y = y, color = ifelse(df$.abs > 0.3, "white", "black"), + label = df$.coord + ) + + cross <- data.frame( + x = x, y = y, color = ifelse(df$.is_outlier, "red", "#FFFFFF00") + ) + + ggplot2::ggplot( + well, ggplot2::aes(.data$x, .data$y, color = .data$color) + ) + + ggplot2::geom_point(size = size, shape = 16) + + ggplot2::geom_text( + data = text, ggplot2::aes(label = .data$label), size = size / 2.5 + ) + + ggplot2::geom_point(data = cross, shape = 4, size = size, stroke = 4) + + ggplot2::theme_void() + + ggplot2::coord_cartesian(clip = "off") + + ggplot2::scale_color_identity() + + ggplot2::theme(plot.margin = ggplot2::margin(0, 1.5, 0, 1.5, unit = "cm")) +} + +add_well_circles_plot_col <- function(x, + sample_type, + select_vars, + group_vars, + summary = FALSE) { + x <- dplyr::filter(x, .data$sample_type == .env$sample_type) + + x <- x |> + dplyr::select(dplyr::all_of(select_vars)) |> + dplyr::group_by(dplyr::across(dplyr::all_of(group_vars))) |> + tidyr::nest() |> + dplyr::mutate(gg = purrr::map(.data$data, make_well_circles_plot)) + + if (summary) { + x <- dplyr::select(x, -"data") + } else { + x <- x |> + tidyr::unnest(.data$data) |> + dplyr::select(-c(".is_outlier", ".coord")) + } + + dplyr::ungroup(x) +} + +add_coords_col <- function(x) { + x$.coord <- paste0(LETTERS[x$.row], x$.col) + x +} + +make_gt <- function(x, well_img_height, well_img_aspect_ratio, cols_to_round) { + x |> + gt::gt() |> + gt::text_transform( + location = gt::cells_body( + columns = .data$gg + ), + fn = function(z) { + gt::ggplot_image( + x$gg, + height = gt::px(well_img_height), + aspect_ratio = well_img_aspect_ratio + ) + } + ) |> + gt::fmt_number( + dplyr::all_of(cols_to_round), + n_sigfig = 2 + ) +} + +dil_summary <- function(qp) { + check_has_cols(qp, c( + ".sample_name", ".pred_conc_mean", ".target_conc", + ".target_vol", "sample_type" + )) + + qp |> + dplyr::summarize( + .by = c( + ".sample_name", ".pred_conc_mean", ".target_conc", + ".target_vol", "sample_type" + ) + ) |> + qp_dilute() |> + dplyr::filter(.data$sample_type == "unknown") |> + dplyr::select( + Name = ".sample_name", + ".pred_conc_mean", ".target_conc", + `Final Vol` = ".target_vol", + `Sample to Add (uL)` = "sample_to_add", + `Diluent to Add (uL)` = "add_to" + ) |> + dplyr::mutate( + .target_conc = round(.data$.target_conc, 2), + .pred_conc_mean = round(.data$.pred_conc_mean, 2) + ) |> + dplyr::rename( + "[Target]" = .data$.target_conc, + "[Sample]" = .data$.pred_conc_mean + ) } diff --git a/R/qp_summarize.R b/R/qp_summarize.R index aa66af9..d7dc62e 100644 --- a/R/qp_summarize.R +++ b/R/qp_summarize.R @@ -21,7 +21,7 @@ qp_summarize.data.frame <- function(x) { } x |> dplyr::summarize( - .mean_pred_conc = mean(.data$.pred_conc_mean, na.rm = TRUE), + .pred_conc_mean = mean(.data$.pred_conc_mean, na.rm = TRUE), .by = c(".sample_name", "sample_type") ) |> dplyr::arrange(.data$sample_type) diff --git a/_pkgdown.yml b/_pkgdown.yml index 099eebf..d4aec09 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -30,6 +30,7 @@ reference: - qp_summarize - qp_plot_plate - qp_plot_standards + - qp_report - title: "Data" contents: - absorbances diff --git a/inst/rmarkdown/templates/quantify-protein-report/skeleton/skeleton.Rmd b/inst/rmarkdown/templates/quantify-protein-report/skeleton/skeleton.Rmd new file mode 100644 index 0000000..97bd2da --- /dev/null +++ b/inst/rmarkdown/templates/quantify-protein-report/skeleton/skeleton.Rmd @@ -0,0 +1,152 @@ +--- +title: "Protein Quantification Report" +output: + html_document: + css: "style.css" +params: + qp : NA + other : list() +--- + +```{r setup, include=FALSE} +knitr::opts_chunk$set(echo = FALSE) +library(qp) +library(ggplot2) +library(dplyr) +library(gt) +library(knitr) +``` + + +# Plots +```{r} +plate_plot <- qp_plot_plate(params$qp$qp) + + theme_void() + + coord_cartesian(clip = "off") + + theme( + plot.margin = margin(1, 0.5, 1, 0.5, unit = "cm"), + panel.background = element_rect(fill = "transparent", color = NA), + plot.background = element_rect(fill = "#FFFFFF33", color = NA) + ) + +ggsave( + "plate_plot.png", + plate_plot, + device = "png", + width = 17, height = max(params$qp$qp$.col), units = "cm", + bg = "transparent" +) +``` + +```{r} +standards_plot <- qp_plot_standards(params$qp) + + theme_minimal() + + theme( + panel.background = element_rect(fill = "transparent", color = NA), + plot.background = element_rect(fill = "#FFFFFF33", color = NA) + ) + +ggsave( + "standards_plot.png", + standards_plot, + device = "png", + width = 12, height = 9, units = "cm", + bg = "transparent" +) +``` + +![test](plate_plot.png){height=300px} ![test](standards_plot.png){height=300px} + +# Tables + + +```{r message=FALSE} + +params$qp$qp |> + dil_summary() |> + gt() |> + tab_options( + table.background.color = "#FFFFFF33", + table.border.top.color = "white", + table.border.bottom.color = "white" + ) + +``` + + +## Summaries +:::: {style="display: flex; justify-content: space-between;"} + +::: {} + +```{r} +mk_tbl <- function(qp_tbl_out, name) { + qp_tbl_out$qp |> + tab_options( + table.background.color = "#FFFFFF33", + table.border.top.color = "white", + table.border.bottom.color = "white" + ) |> + tab_header(name) +} + +mk_tbl(qp_samples_summary(params$qp), "Samples Summary") +``` + +::: + + +::: {} + +```{r} +mk_tbl(qp_standards_summary(params$qp), "Standards Summary") +``` + +::: + + +:::: + +## All +:::: {style="display: flex; justify-content: space-between;"} + +:::{} + +```{r} +mk_tbl(qp_samples_all(params$qp), "Samples") +``` + +::: + +:::{} + +```{r} +mk_tbl(qp_standards_all(params$qp), "Standards") +``` + +::: + + +:::: + + +```{r} +#load_all() +library(rmarkdown) +f <- system.file("rmarkdown/templates/quantify-protein-report/skeleton/skeleton.Rmd", package = "qp") +out_file <- "~/Desktop/test.html" +# render(f, output_file = out_file, params = list(qp = qp(absorbances) |> qp_dilute(), other = list(foo = "bar", fruit = "banana"))) +``` + +# Report Parameters + +```{r} +if (length(params$other) > 0) { + params$other |> + as.data.frame() |> + t() |> + as_tibble(rownames = "Parameter") |> + rename("Value" = "V1") |> + kable() +} +``` diff --git a/inst/rmarkdown/templates/quantify-protein-report/skeleton/style.css b/inst/rmarkdown/templates/quantify-protein-report/skeleton/style.css new file mode 100644 index 0000000..9df1e97 --- /dev/null +++ b/inst/rmarkdown/templates/quantify-protein-report/skeleton/style.css @@ -0,0 +1,31 @@ +body { + background: linear-gradient(45deg, #8965CD, #C1FFC1); + background-attachment: fixed; + color: white; + font-family: Arial; + font-weight: bold; + text-align: center; +} + +h1 { + font-size: 40px; + text-align: center; +} + +h2 { + font-size: 30px; + text-align: left; +} + +h1, h2 { + font-weight: bold; +} + + +h1.title { + font-size: 60px; +} + +body .main-container { + min-width: 30%; +} diff --git a/inst/rmarkdown/templates/quantify-protein-report/template.yaml b/inst/rmarkdown/templates/quantify-protein-report/template.yaml new file mode 100644 index 0000000..3efdf84 --- /dev/null +++ b/inst/rmarkdown/templates/quantify-protein-report/template.yaml @@ -0,0 +1,4 @@ +name: Quantify Protein Report +description: > + Report output from qp +create_dir: FALSE diff --git a/man/qp_dilute.Rd b/man/qp_dilute.Rd index cd27af2..116516b 100644 --- a/man/qp_dilute.Rd +++ b/man/qp_dilute.Rd @@ -26,8 +26,8 @@ qp_dilute(x, ...) } \arguments{ \item{x}{A \code{data.frame} or \code{list} containing a \code{data.frame} named \code{qp} with -a column named \code{.pred_conc} or \code{.mean_pred_conc}. If both, will favor -\code{.mean_pred_conc}.} +a column named \code{.pred_conc} or \code{.pred_conc_mean}. If both, will favor +\code{.pred_conc_mean}.} \item{...}{Unused} diff --git a/man/qp_report.Rd b/man/qp_report.Rd new file mode 100644 index 0000000..0880937 --- /dev/null +++ b/man/qp_report.Rd @@ -0,0 +1,32 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/qp_report.R +\name{qp_report} +\alias{qp_report} +\title{Create a report for a protein quantificaiton experiment} +\usage{ +qp_report(qp, output_file, other = list()) +} +\arguments{ +\item{qp}{Likely the output from \code{qp} AND \code{qp_dilute}.} + +\item{output_file}{Character. The path of the file to export, +including \code{.html}} + +\item{other}{Generally used for Shiny application. Assumes a +named list of key-values that will be used to document report +parameters.} +} +\description{ +Create a report for a protein quantificaiton experiment +} +\examples{ +\dontrun{ +absorbances |> + qp() |> + qp_dilute() |> + qp_report( + "~/my_report.html", + other = list(key = "value") # Essentially metadata + ) +} +} diff --git a/tests/testthat/test-qp_dilute.R b/tests/testthat/test-qp_dilute.R index e8c7269..5ecc627 100644 --- a/tests/testthat/test-qp_dilute.R +++ b/tests/testthat/test-qp_dilute.R @@ -13,7 +13,7 @@ test_that("length_is_recyclable works", { test_that("dilution prefers .mean_pred_conc", { data <- data.frame( .pred_conc = c(1, 2), - .mean_pred_conc = c(5, 10) + .pred_conc_mean = c(5, 10) ) out <- qp_dilute(data, target_conc = 5, target_vol = 2) expect_equal(out$sample_to_add, c(2, 1)) diff --git a/tests/testthat/test-qp_summarize.R b/tests/testthat/test-qp_summarize.R index 8dc6397..6066dbc 100644 --- a/tests/testthat/test-qp_summarize.R +++ b/tests/testthat/test-qp_summarize.R @@ -13,7 +13,7 @@ test_that("qp_summarize summary is correct", { out <- data.frame( .sample_name = c(paste0("Standard ", 1:3), 1:3), sample_type = rep(c("standard", "unknown"), each = 3), - .mean_pred_conc = c(2, 8, 14, 5, 11, 17) + .pred_conc_mean = c(2, 8, 14, 5, 11, 17) ) expect_equal(qp_summarize.data.frame(df), out) })