Quarto Report Example With Plotly and Trelliscopejs

2nd October 2024

Written by Jeremy Selva

Introduction

Here is a report showing how to create injection sequence plot and dilution plot using plotly for each Multiple Reaction Monitoring (MRM) transition in Mass Spectrometry.

An injection sequence plot. It consists of two figures. The figure on the left is a scatter plot with the y axis as peak area while the x axis as the injection sequence. Colours on the scatter plot are based on the quality control sample types. The figure in the right is a raincloud plot with the y axis as peak area while the x axis as quality control sample types. Colours on the raincloud plot are based on the quality control sample types.

Injection Sequence Plot

A dilution plot with the y axis as the peak area and the x axis as relative sample amount in percentages. Both linear and quadratic regression are used to fit the dilution points.

Dilution Plot

These multiple interactive plots will be displayed as a trellis using trelliscopejs.

The source code used to generate this report can be found in this GitHub page.

Background

Here is some background knowledge about the use of the quality control (QC) plots, for example, the injection sequence plot and the dilution plot, in targeted lipidomics analysis.

About Targeted Lipidomics

The peak area data used to generate these quality control plots comes from a targeted lipidomics experiment using Reversed-Phase Ultra High-Performance Liquid Chromatography/ Mass Spectrometry (RP-UHPLC/MS).

For those new to mass spectrometry or lipidomics, the aim of targeted lipidomics is to measure and even quantify the amount of specific lipids found in a given sample. To tell the mass spectrometer what lipids to measure, a list of multiple reaction monitoring (MRM) transitions (with their corresponding precursor and product ions) is provided. More details on what multiple reaction monitoring (MRM) is can be found in this webpage by Proteomics International Laboratories LTD.

Reason for creating quality control (QC) plots

However, variation in a transition’s peak area measured between samples can come from many unwanted sources, besides that sample itself. Such unwanted sources includes, contaminant ions or a different pipette used during lipid extraction. To check the severity of these unwanted variation, quality control (QC) samples are used.

In most practices, we want to keep transitions that give peak areas with low variation in the quality control samples. While variations can be measured using summary statistics such as the coefficient of variation, creating quality control (QC) plots can help us identify the cause of the unwanted variation. In this report, two such plots are introduced.

The injection sequence plot is a scatter plot with the peak area (or concentration) on the y-axis and the injection sequence order (or data acquisition time) on the x axis. This is to better understand the variation of peak area in the quality control (QC) sample types over time. A raincloud plot (1) is placed on the right of the scatter plot to show the distribution of each (QC) sample type as well as to highlight any potential outliers.

The dilution plot, on the other hand, checks for possible signal enhancement or suppression as a result of detector saturation or matrix effect.

R Packages Used

Code
# For session info and package reporting
library("sessioninfo") # R Session Information Posit RPSM v1.2.2
library("quarto") # R Interface to 'Quarto' Markdown Publishing System Posit RPSM v1.4.4
library("report") # Automated Reporting of Results and Statistical Models Posit RPSM v0.5.9
library("knitr") # A General-Purpose Package for Dynamic Report Generation in R CRAN v1.48
library("fontawesome") # Easily Work with 'Font Awesome' Icons Posit RPSM v0.5.2
library("verbaliseR") # Make your Text Mighty Fine CRAN v0.1

# For reading files
library("readr") # Read Rectangular Text Data CRAN v2.1.5

# For interactive tables
library("reactable") # Interactive Data Tables for R Posit RPSM v0.4.4
library("htmltools") # Tools for HTML Posit RPSM v0.5.8.1

# For data wrangling
library("dplyr") # A Grammar of Data Manipulation Posit RPSM v1.1.4
library("tidyr") # Tidy Messy Data Posit RPSM v1.3.1
library("tibble")# Simple Data Frames Posit RPSM v3.2.1
library("purrr") # Functional Programming Tools Posit RPSM v1.0.2
library("broom") # Convert Statistical Objects into Tidy Tibbles Posit RPSM v1.0.7
library("glue") # Interpreted String Literals Posit RPSM v1.7.0

# For palatte colours
library("scales") # Scale Functions for Visualization CRAN v1.3.0

# For lipid annotation
# Install from BioConductor
library("rgoslin") # Lipid Shorthand Name Parsing and Normalization [github::lifs-tools/rgoslin] v2.2.0

# For plotting
library("plotly") # Create Interactive Web Graphics via 'plotly.js' Posit RPSM v4.10.4

# For cognostics
library("assertable") # Verbose Assertions for Tabular Data (Data.frames and Data.tables) Posit RPSM v0.2.8

# For combining plotly plots
library("manipulateWidget") # Add Even More Interactivity to Interactive Charts Posit RPSM v0.11.1

# For trellis plot
library("trelliscopejs") # Create Interactive Trelliscope Displays Posit RPSM v0.2.6
Code
r_package_table <- sessioninfo::package_info()
rownames(r_package_table) <- NULL

r_package_table |>
  dplyr::mutate(
    version = ifelse(is.na(r_package_table$loadedversion), 
                     r_package_table$ondiskversion, 
                     r_package_table$loadedversion)
  ) |> 
  dplyr::filter(.data$attached == TRUE) |> 
  dplyr::select(
    dplyr::any_of(c("package", "version", 
                    "date", "source")
    )
  ) |> 
  reactable::reactable(
    columns = list(
      package = reactable::colDef(
        # Freeze first column
        sticky = "left",
        style = list(borderRight = "1px solid #eee"),
        headerStyle = list(borderRight = "1px solid #eee"))
  )
  )

R Platform Information

Code
# Taken from https://github.com/r-lib/sessioninfo/issues/75
get_quarto_version <- function() {
  if (isNamespaceLoaded("quarto")) {
    path <- quarto::quarto_path()
    ver <- system("quarto -V", intern = TRUE)
    if (is.null(path)) {
      "NA (via quarto)"
    } else {
      paste0(ver, " @ ", path, "/ (via quarto)")
    }
  } else {
    path <- Sys.which("quarto")
    if (path == "") {
      "NA"
    } else {
      ver <- system("quarto -V", intern = TRUE)
      paste0(ver, " @ ", path)
    }
  }
}


r_platform_table <- sessioninfo::platform_info()
r_platform_table[["quarto"]] <- get_quarto_version()[1]

r_platform_table <- data.frame(
    setting = names(r_platform_table),
    value = unlist(r_platform_table,
                   use.names = FALSE),
    stringsAsFactors = FALSE
  )

r_platform_table |>
  reactable::reactable(
    defaultPageSize = 5
  )

Quarto Extenstion used

This Quarto document uses the Quarto Extension schochastics/quarto-social-share. Prior to rendering this document via command quarto render index.qmd, remember to install the extension by running quarto install extension schochastics/quarto-social-share in the command line terminal. This will install the extension under the _extensions subdirectory. If you’re using version control, you will want to check in this directory.

Input Data

The data we are using are from Wolrab et. al 2022 (2).

Here are the links to the following csv file:

We read the transition’s peak area measurement using Reversed-Phase Ultra High-Performance Liquid Chromatography/ Mass Spectrometry (RP-UHPLC/MS). The measurement is done by lab 3 during Phase II of the project.

The first column is the name of the sample. Subsequent columns are the transition’s integrated peak areas.

Code
area_data <- readr::read_csv(
  file = "https://raw.github.com/JauntyJJS/Trelliscopejs_In_Quarto_Example/main/data/RP-UHPLC_MS_Area.csv",
  show_col_types = FALSE)

Here is how the data (first 30 rows) looks like

Code
area_data[1:30,] |> 
  reactable::reactable(
    defaultPageSize = 5,
    defaultColDef = reactable::colDef(
      minWidth = 350),
    columns = list(
      Sample_Name = reactable::colDef(
        # Freeze first column
        sticky = "left",
        style = list(borderRight = "1px solid #eee"),
        headerStyle = list(borderRight = "1px solid #eee"),
        minWidth = 150
      )
      ),
    bordered = TRUE,
    highlight = TRUE,
    paginationType = "jump"
    )

The transition annotation provides information about the given transitions that has been measured.

Code
transition_name_annot <- readr::read_csv(
  file = "https://raw.github.com/JauntyJJS/Trelliscopejs_In_Quarto_Example/main/data/RP-UHPLC_MS_Transition_Name_Annot.csv",
  show_col_types = FALSE)

Here are the column descriptions.

Column Name Column Description
Transition_Name Name of the transition to be mesured in the mass spectrometer.
Lipid_Name Converted transition name to suit lipid nomenclature set by Liebisch et. al. 2020 (3).
For_Rgoslin Input transition for the R package rgoslin (4), (5) to annotate.
Precursor_Ion The ion to be fragmented into smaller fragment ions.
Product_Ion Ions created from fragmentation of the precursor ion.
Retention_Time_[min] Transition name’s expectred retention time.
Delta_Retention_Time_[min] Time window to acquire the transition. Each transition is acquired in the range Retention Time +/- 0.5(Delta Retention Time).
Collision_Energy Rate of acceleration as the precursor ions enter the Q2 for fragmentation.
Cell_Accelerator_Voltage Rate of acceleration as the product ions leave the Q2.
Polarity Settings (Positive or Negative) for the mass spectrometry to detect (positive or negative) ions.
isISTD Set to TRUE if the given transition name an internal standard.

Here is how the data looks like

Code
transition_name_annot |> 
  reactable::reactable(
    defaultPageSize = 5,
    defaultColDef = reactable::colDef(minWidth = 300),
    columns = list(
      Transition_Name = reactable::colDef(
      # Freeze first column
      sticky = "left",
      style = list(borderRight = "1px solid #eee"),
      headerStyle = list(borderRight = "1px solid #eee"),
      minWidth = 250
    )),
    bordered = TRUE,
    highlight = TRUE,
    searchable = TRUE,
    paginationType = "jump"
  )

The sample annotation provides information about the given samples that has been measured.

Code
sample_annot <- readr::read_csv(
  file = "https://raw.github.com/JauntyJJS/Trelliscopejs_In_Quarto_Example/main/data/RP-UHPLC_MS_Sample_Annot.csv",
  show_col_types = FALSE)

Here are the column descriptions.

Column Name Column Description
Injection_Sequence The order in which the samples are injected to the mass spectrometer for data acquisition.
Acqusition_Time_Stamp The time the sample is being measured. It is expressed yyyy-mm-dd hh:mm:ss timezone.
Sample_Name The name of the sample.
QC_Sample_Type

Quality control category of the sample. More information can be found in Broadhurst et. al. 2018 (6).

  • SPL stands for experimental/unknown samples.

  • BQC stands for batch/pooled quality control.

  • TQC stands for technical (or pooled lipid extracts) quality control.

  • RQC stands for response (or pooled QC dilution) quality control.

  • PBLK stands for processed blanks.

  • LTR stands for long term reference.

  • NIST stands for SRM1950 NIST plasma sample from Simón-Manso et. al. 2013 (7) . It is a standard reference material provides a method to allow quality assessment across different laboratories.

Vial_Position The sample’s vial position in the autosampler.

Here is how it looks like

Code
sample_annot |> 
  reactable::reactable(
    defaultPageSize = 5,
    defaultColDef = reactable::colDef(minWidth = 250),
    columns = list(
      Injection_Sequence = reactable::colDef(
      # Freeze first column
      sticky = "left",
      style = list(borderRight = "1px solid #eee"),
      headerStyle = list(borderRight = "1px solid #eee"),
      minWidth = 200
    )),
    bordered = TRUE,
    highlight = TRUE,
    searchable = TRUE,
    paginationType = "jump"
    )

The dilution annotation provides information about the given RQC samples that has been measured.

Code
dilution_annot <- readr::read_csv(
  file = "https://raw.github.com/JauntyJJS/Trelliscopejs_In_Quarto_Example/main/data/RP-UHPLC_MS_Dilution_Annot.csv",
  show_col_types = FALSE)

Here are the column descriptions.

Column Name Column Description
Sample_Name The name of the sample.
Dilution_Batch_Name Name of the dilution batch.
Relative_Sample_Amount_[%] Relative sample amount in %. Used as the x-axis of the dilution curve.

Here is how it looks like

Code
dilution_annot |> 
  reactable::reactable(
    defaultPageSize = 5,
    defaultColDef = reactable::colDef(minWidth = 270),
    columns = list(
      Sample_Name = reactable::colDef(
      # Freeze first column
      sticky = "left",
      style = list(borderRight = "1px solid #eee"),
      headerStyle = list(borderRight = "1px solid #eee"),
      minWidth = 150
    )),
    bordered = TRUE,
    highlight = TRUE,
    searchable = TRUE,
    paginationType = "jump"
    )

Nested Data

Nested Sample Data

We first merge the peak area data with the sample annotation. Next, we created a nested sample_data for each transition name.

sample_data will be used later to do statistical analysis for each QC sample type.

Code
sample_annot_column_names <- sample_annot |> 
  colnames()

nested_sample_data <- sample_annot |> 
  dplyr::inner_join(y = area_data, by = "Sample_Name") |> 
  tidyr::pivot_longer(cols = -dplyr::any_of(sample_annot_column_names),
                      names_to = "Transition_Name", 
                      values_to = "Area") |> 
  dplyr::group_by(.data[["Transition_Name"]]) |>
  tidyr::nest() |>
  dplyr::rename(sample_data = dplyr::all_of("data")) |> 
  dplyr::ungroup()

Here is a preview of the first five rows of the nested_sample_data. For each sample_data of a given transition name, we display the first ten rows.

Code
nested_sample_data[1:5,] |>
  reactable::reactable(
    defaultColDef = reactable::colDef(minWidth = 250),
    columns = list(
      sample_data = reactable::colDef(
        details = function(index) {
          filtered_data <- nested_sample_data[["sample_data"]][index][[1]]
          htmltools::div(style = "padding: 1rem",
                         reactable::reactable(
                           data = filtered_data[1:10,],
                           defaultPageSize = 5,
                           defaultColDef = reactable::colDef(minWidth = 250),
                           columns = list(Injection_Sequence = reactable::colDef(minWidth = 150),
                                          Sample_Name = reactable::colDef(minWidth = 150)),
                           outlined = TRUE,
                           highlight = TRUE)
                       )
      }
    )
  )

)

Nested Dilution Data

We first merge the peak area data with the dilution and sample annotation. Next, we created a nested dilution_data for each transition name.

dilution_data will be used later to do dilution curve related analysis for each transition.

Code
sample_annot_column_names <- sample_annot |> 
  colnames()

dilution_annot_column_names <- dilution_annot |> 
  colnames()

nested_dilution_data <- dilution_annot |> 
  dplyr::inner_join(y = sample_annot, by = "Sample_Name") |>
  dplyr::inner_join(y = area_data, by = "Sample_Name") |> 
  tidyr::pivot_longer(cols = -dplyr::any_of(c(sample_annot_column_names,
                                              dilution_annot_column_names)),
                      names_to = "Transition_Name", 
                      values_to = "Area") |> 
  dplyr::group_by(.data[["Transition_Name"]]) |>
  tidyr::nest() |>
  dplyr::rename(dilution_data = dplyr::all_of("data")) |> 
  dplyr::ungroup()

Here is a preview of the first five rows of the nested_dilution_data. For each dilution_data of a given transition name, we display the first five rows.

Code
nested_dilution_data[1:5,] |>
  reactable::reactable(
    defaultColDef = reactable::colDef(minWidth = 250),
    columns = list(
      dilution_data = reactable::colDef(
        details = function(index) {
          filtered_data <- nested_dilution_data[["dilution_data"]][index][[1]]
          htmltools::div(style = "padding: 1rem",
                         reactable::reactable(
                           data = filtered_data[1:5,],
                           defaultPageSize = 5,
                           defaultColDef = reactable::colDef(minWidth = 250),
                           columns = list(Sample_Name = reactable::colDef(minWidth = 150)),
                           outlined = TRUE,
                           highlight = TRUE)
                       )
      }
    )
  )

)

Join them together

We join the two nested data sample_data and dilution_data together

Code
nested_data <- nested_sample_data |> 
  dplyr::inner_join(y = nested_dilution_data, by = "Transition_Name")

Here is a preview of the first five rows of the nested_data.

Code
nested_data[1:5,] |>
  reactable::reactable(
    defaultColDef = reactable::colDef(minWidth = 250),
    columns = list(
      sample_data = reactable::colDef(
        details = function(index) {
          filtered_data <- nested_data[["sample_data"]][index][[1]]
          htmltools::div(style = "padding: 1rem",
                         reactable::reactable(
                           data = filtered_data[1:10,],
                           defaultPageSize = 5,
                           defaultColDef = reactable::colDef(minWidth = 250),
                           columns = list(Injection_Sequence = reactable::colDef(minWidth = 150)),
                           outlined = TRUE,
                           highlight = TRUE)
                       )}),
      dilution_data = reactable::colDef(
        details = function(index) {
          filtered_data <- nested_data[["dilution_data"]][index][[1]]
          htmltools::div(style = "padding: 1rem",
                         reactable::reactable(
                           data = filtered_data[1:5,],
                           defaultPageSize = 5,
                           defaultColDef = reactable::colDef(minWidth = 250),
                           columns = list(Sample_Name = reactable::colDef(minWidth = 150)),
                           outlined = TRUE,
                           highlight = TRUE)
                       )})
      )
    )

Annotate Transition Names

We now add the annotations for each transitions from transition_name_annot and rgoslin.

Add transition_name_annot annotations

We first join the nested_data with our transition annotations.

Code
annotated_data <- nested_data |> 
  dplyr::inner_join(y = transition_name_annot, by = "Transition_Name")

Here is a preview of the first five rows of the annotated_data.

Code
annotated_data[1:5,] |>
  reactable::reactable(
    defaultColDef = reactable::colDef(minWidth = 250),
    columns = list(
      sample_data = reactable::colDef(
        details = function(index) {
          filtered_data <- annotated_data[["sample_data"]][index][[1]]
          htmltools::div(style = "padding: 1rem",
                         reactable::reactable(
                           data = filtered_data[1:10,],
                           defaultPageSize = 5,
                           defaultColDef = reactable::colDef(minWidth = 250),
                           columns = list(Injection_Sequence = reactable::colDef(minWidth = 150)),
                           outlined = TRUE,
                           highlight = TRUE)
                       )}),
      dilution_data = reactable::colDef(
        details = function(index) {
          filtered_data <- annotated_data[["dilution_data"]][index][[1]]
          htmltools::div(style = "padding: 1rem",
                         reactable::reactable(
                           data = filtered_data[1:5,],
                           defaultPageSize = 5,
                           defaultColDef = reactable::colDef(minWidth = 250),
                           columns = list(Sample_Name = reactable::colDef(minWidth = 150)),
                           outlined = TRUE,
                           highlight = TRUE)
                       )})
      )
    )

Add rgoslin annotations

Small example

The R package rgoslin is useful in getting additional information of lipid names such as the lipid classes. A column called For_Rgoslin was created to convert each transition name that can be parsed by rgoslin. Unfortunately only Coenzyme Q10 is unable to be parsed by rgoslin. We thus add the relevant information manually.

Below is an code example (not part of the pipeline) on how rgoslin can be used

Code
rgoslin_example <- annotated_data |> 
  dplyr::select(dplyr::all_of("For_Rgoslin")) |> 
  dplyr::mutate(GoslinOutput = purrr::map(.data[["For_Rgoslin"]],
                                          rgoslin::parseLipidNames)
                ) |> 
  tidyr::unnest(dplyr::all_of("GoslinOutput")) |> 
  dplyr::rows_update(
    tibble::tibble(
      `For_Rgoslin` = "Coenzyme Q10",
      Lipid.Maps.Category = "PR",
      Lipid.Maps.Main.Class = "Quinones and hydroquinones",
      Species.Name = "Coenzyme Q10",
      Molecular.Species.Name = "Coenzyme Q10",
      Functional.Class.Abbr = "[Coenzyme]",
      Functional.Class.Synonyms = "[Coenzyme, Ubiquinone]"
    ),
    by = "For_Rgoslin"
)

Here is a preview of rgoslin_example.

Code
rgoslin_example |>
  reactable::reactable(
    defaultPageSize = 5,
    defaultColDef = reactable::colDef(minWidth = 250),
    columns = list(
      For_Rgoslin = reactable::colDef(
      # Freeze first column
      sticky = "left",
      style = list(borderRight = "1px solid #eee"),
      headerStyle = list(borderRight = "1px solid #eee"),
      minWidth = 150
    )),
    bordered = TRUE,
    highlight = TRUE,
    searchable = TRUE,
    paginationType = "jump"
  )

Pipeline example

For the workflow, we do not need all the columns that rgoslin provide.

The following columns are kept instead.

Column Name Column Description
Lipid.Maps.Category LIPID MAPS’ Structure Database (8) Lipid Category which the transition name belongs to.
Lipid.Maps.Main.Class LIPID MAPS’ Structure Database (8) Lipid Main Class which the transition name belongs to.
Species.Name Represented by the sum composition, i.e., sum of carbon atoms, double bond equivalent and number of additional oxygen atoms, e.g. FA 18:1;O.
Molecular.Species.Name Represented by lipid species with identified fatty acyl/alkyl residues, e.g. TG 16:0_18:1_18:1.
Functional.Class.Abbr Abbreviation used to describe the lipid’s functional class based on the grammar used.
Functional.Class.Synonyms Alternative abbreviations used to describe the lipid’s functional class.
Code
annotated_data <- nested_data |> 
  dplyr::inner_join(y = transition_name_annot, by = "Transition_Name")

annotated_data_cols <- colnames(annotated_data)

annotated_data <- annotated_data |> 
  dplyr::mutate(GoslinOutput = purrr::map(.data[["For_Rgoslin"]],
                                          rgoslin::parseLipidNames)
                ) |>
  tidyr::unnest(.data$GoslinOutput) |> 
  dplyr::select(dplyr::any_of(
    c(annotated_data_cols,
      "Lipid.Maps.Category", "Lipid.Maps.Main.Class",
      "Species.Name", "Molecular.Species.Name",
      "Functional.Class.Abbr", "Functional.Class.Synonyms")
    )
  ) |>
  dplyr::rows_update(
    tibble::tibble(
      `For_Rgoslin` = "Coenzyme Q10",
      Lipid.Maps.Category = "PR",
      Lipid.Maps.Main.Class = "Quinones and hydroquinones",
      Species.Name = "Coenzyme Q10",
      Molecular.Species.Name = "Coenzyme Q10",
      Functional.Class.Abbr = "[Coenzyme]",
      Functional.Class.Synonyms = "[Coenzyme, Ubiquinone]"
    ),
    by = "For_Rgoslin"
    
)

Here is a preview of the first five rows of the annotated_data.

Code
annotated_data[1:5,] |>
  reactable::reactable(
    defaultColDef = reactable::colDef(minWidth = 250),
    columns = list(
      Transition_Name = reactable::colDef(
      # Freeze first column
      sticky = "left",
      style = list(borderRight = "1px solid #eee"),
      headerStyle = list(borderRight = "1px solid #eee"),
      minWidth = 170),
      sample_data = reactable::colDef(
        details = function(index) {
          filtered_data <- annotated_data[["sample_data"]][index][[1]]
          htmltools::div(style = "padding: 1rem",
                         reactable::reactable(
                           data = filtered_data[1:10,],
                           defaultPageSize = 5,
                           defaultColDef = reactable::colDef(minWidth = 250),
                           columns = list(Injection_Sequence = reactable::colDef(minWidth = 150)),
                           outlined = TRUE,
                           highlight = TRUE)
                       )}),
      dilution_data = reactable::colDef(
        details = function(index) {
          filtered_data <- annotated_data[["dilution_data"]][index][[1]]
          htmltools::div(style = "padding: 1rem",
                         reactable::reactable(
                           data = filtered_data[1:5,],
                           defaultPageSize = 5,
                           defaultColDef = reactable::colDef(minWidth = 250),
                           columns = list(Sample_Name = reactable::colDef(minWidth = 150)),
                           outlined = TRUE,
                           highlight = TRUE)
                       )})
      )
    )

Calculate Transition Statistics

We now proceed to calculate some statistical summary for each transition using the nested dilution_data and sample_data.

Calculate RQC/Dilution Statistics

The following statistics are calculated from dilution_data.

Column Name Column Description
r_corr Pearson Correlation \(R\) value.
r2_linear Linear Regression \(R^2\) Value.
r2_linear_<=0.8 Set to TRUE if r2_linear is less than 0.8.

The relevant functions to do these calculations are as follows

Code
#' @title Create Linear Model
#' @description A wrapper to create a linear model from dilution data
#' @param dilution_data A data frame or tibble containing dilution data
#' @param conc_var Column name in `dilution_data` to indicate concentration
#' @param signal_var Column name in `dilution_data` to indicate signal
#' @return A linear model object from `stats:lm()` with formula
#' `signal_var ~ conc_var` from data `diltuion_data`
#' @examples
#' dilution_percent <- c(10, 20, 40, 60, 80, 100)
#' area <- c(22561, 31178, 39981, 48390, 52171, 53410)
#' dilution_data <- data.frame(Dilution_Percent = dilution_percent, Area = area)
#' linear_model <- create_linear_model(dilution_data,
#'                                     "Dilution_Percent",
#'                                     "Area")
#' linear_model
#' @rdname create_linear_model
#' @export
create_linear_model <- function(dilution_data, conc_var, signal_var) {

  conc_var <- paste0("`",conc_var,"`")
  signal_var <- paste0("`",signal_var,"`")

  # Create the formula
  linear_formula <- stats::as.formula(paste(signal_var, "~",
                                            paste(conc_var, collapse = " + ")
  )
  )

  # Create the linear model on dilution data
  linear_model <- stats::lm(linear_formula, data = dilution_data)

  return(linear_model)

}

#' @title Create Quadratic Model
#' @description A wrapper to create a quadratic model for dilution data
#' @param dilution_data A data frame or tibble containing dilution data
#' @param conc_var Column name in `dilution_data` to indicate concentration
#' @param signal_var Column name in `dilution_data` to indicate signal
#' @return A linear model object from `stats:lm()` with formula
#' `signal_var ~ conc_var + I(conc_var * conc_var)`
#' from data `diltuion_data`
#' @examples
#' dilution_percent <- c(10, 20, 40, 60, 80, 100)
#' area <- c(22561, 31178, 39981, 48390, 52171, 53410)
#' dilution_data <- data.frame(Dilution_Percent = dilution_percent, Area = area)
#' quad_model <- create_quad_model(dilution_data,
#'                                 "Dilution_Percent",
#'                                 "Area")
#' quad_model
#' @rdname create_quad_model
#' @export
create_quad_model <- function(dilution_data, conc_var, signal_var) {

  conc_var <- paste0("`",conc_var,"`")
  signal_var <- paste0("`",signal_var,"`")

  # Create the formula
  quad_formula <- stats::as.formula(paste(signal_var, "~",
                                          paste(conc_var, "+",
                                                paste0("I(", conc_var, " * ",
                                                       conc_var, ")")
                                          )
  )
  )

  # Create the quadratic model on dilution data
  quad_model <- stats::lm(quad_formula, data = dilution_data)

  return(quad_model)

}

#' @title Calculate Linear Model's Goodness Of Fit
#' @description Calculate the Goodness of Fit of the Dilution Linear Model
#' @param dilution_data A data frame or tibble containing dilution data
#' @param conc_var Column name in `dilution_data` to indicate concentration
#' @param signal_var Column name in `dilution_data` to indicate signal
#' @return A tibble containing the Goodness of Fit measures of the linear model
#' The Goodness of Fit measures are the Pearson correlation coefficient (R) and
#' the R^2
#' @details The function will return a tibble with NA values
#' if the number of dilution points is less than or equal to three
#' @examples
#' dilution_percent <- c(10, 20, 40, 60, 80, 100)
#' area <- c(22561, 31178, 39981, 48390, 52171, 53410)
#' dilution_data <- data.frame(Dilution_Percent = dilution_percent, Area = area)
#' dil_linear_gof <- calculate_gof_linear(dilution_data,
#'                                        "Dilution_Percent", "Area")
#' dil_linear_gof
#' @rdname calculate_gof_linear
#' @export
calculate_gof_linear <- function(dilution_data, conc_var, signal_var) {

  dil_linear_gof <- tibble::tibble(r_corr = NA,
                                   r2_linear = NA)

  if (is.null(nrow(dilution_data))) {
    return(dil_linear_gof)
  }

  # Drop rows whose value of signal_var is NA
  dilution_data <- dilution_data |>
    tidyr::drop_na(.data[[signal_var]])

  # Return NA for too little points
  # Horizontal, Vertical line or single point
  if (nrow(dilution_data) <= 2) {
    return(dil_linear_gof)
  }
  if (stats::sd(dilution_data[[conc_var]]) == 0) {
    return(dil_linear_gof)
  }
  if (stats::sd(dilution_data[[signal_var]]) == 0) {
    return(dil_linear_gof)
  }

  # Get the correlation results
  cor_result <- broom::tidy(stats::cor.test(dilution_data[[signal_var]],
                                            dilution_data[[conc_var]],
                                            method = "pearson"))
  r_corr <- round(cor_result$estimate, digits = 6)

  # Create the linear model on dilution data
  linear_model <- create_linear_model(dilution_data, conc_var, signal_var)

  # Get GOF for each model
  linear_gof <- broom::glance(linear_model)

  # Get R2
  r2_linear <- round(linear_gof$r.squared, digits = 6)

  dil_linear_gof <- tibble::tibble(r_corr = r_corr,
                                   r2_linear = r2_linear)

  return(dil_linear_gof)

}

#' @title Summarise Dilution Curve Data
#' @description Get the summary statistics of the dilution data
#' for one group or batch
#' @param dilution_data A data frame or tibble containing dilution data
#' @param conc_var Column name in `dilution_data` to indicate concentration (x-axis)
#' @param signal_var Column name in `dilution_data` to indicate signal (y-axis)
#' @return A tibble containing the Goodness of Fit measures of the linear model
#' The Goodness of Fit measures are the Pearson correlation coefficient (R) and R^2
get_dilution_summary <- function(dilution_data, 
                                 conc_var, 
                                 signal_var) {
  
  dil_linear_gof <- calculate_gof_linear(dilution_data,
                                         conc_var, signal_var)
  
  dilution_summary <- dil_linear_gof
  
  return(dilution_summary)
  
}

Here are the codes for our workflow.

Code
summary_data <- annotated_data |> 
  dplyr::mutate(
    dilution_summary = purrr::map(.x = .data[["dilution_data"]], 
                                .f = get_dilution_summary,
                                conc_var = "Relative_Sample_Amount_[%]",
                                signal_var = "Area")
  ) |>
  tidyr::unnest(dplyr::all_of("dilution_summary")) |> 
  dplyr::mutate(
    `r2_linear_<=0.8` = dplyr::case_when(
      .data[["r2_linear"]] <= 0.8 ~ "TRUE",
      .data[["r2_linear"]] > 0.8 ~ "FALSE",
      TRUE ~ "TRUE"
    )
  ) 

Here is a preview of the first five rows of the summary_data.

Code
summary_data[1:5,] |>
  reactable::reactable(
    defaultColDef = reactable::colDef(minWidth = 250),
    columns = list(
      Transition_Name = reactable::colDef(
      # Freeze first column
      sticky = "left",
      style = list(borderRight = "1px solid #eee"),
      headerStyle = list(borderRight = "1px solid #eee"),
      minWidth = 170),
      sample_data = reactable::colDef(
        details = function(index) {
          filtered_data <- annotated_data[["sample_data"]][index][[1]]
          htmltools::div(style = "padding: 1rem",
                         reactable::reactable(
                           data = filtered_data[1:10,],
                           defaultPageSize = 5,
                           defaultColDef = reactable::colDef(minWidth = 250),
                           columns = list(Sample_Name = reactable::colDef(minWidth = 150)),
                           outlined = TRUE,
                           highlight = TRUE)
                       )}),
      dilution_data = reactable::colDef(
        details = function(index) {
          filtered_data <- annotated_data[["dilution_data"]][index][[1]]
          htmltools::div(style = "padding: 1rem",
                         reactable::reactable(
                           data = filtered_data[1:5,],
                           defaultPageSize = 5,
                           defaultColDef = reactable::colDef(minWidth = 250),
                           columns = list(Sample_Name = reactable::colDef(minWidth = 150)),
                           outlined = TRUE,
                           highlight = TRUE)
                       )})
      )
    )

Calculate QC Samples Statistics

The following statistics are calculated from sample_data.

Column Name Column Description
bqc_mean Mean signal from batch/pooled QC samples defined by Broadhurst et. al. 2018 (6).
bqc_median Median signal from batch/pooled QC samples defined by Broadhurst et. al. 2018 (6).
bqc_sd Standard deviation of signal from batch/pooled QC samples defined by Broadhurst et. al. 2018 (6).
bqc_mad Median absolute deviation of signal from batch/pooled QC samples defined by Broadhurst et. al. 2018 (6).
bqc_cv_% Coefficient of variation of signal in % from batch/pooled QC samples defined by Broadhurst et. al. 2018 (6).
bqc_cv_robust_% Robust coefficient of variation of signal in % from batch/pooled QC samples defined by Broadhurst et. al. 2018 (6).
tqc_mean Mean signal from techincal QC (or pooled lipid extracts) samples defined by Broadhurst et. al. 2018 (6).
tqc_median Median signal from techincal QC (or pooled lipid extracts) samples defined by Broadhurst et. al. 2018 (6).
tqc_sd Standard deviation of signal from techincal QC (or pooled lipid extracts) samples defined by Broadhurst et. al. 2018 (6).
tqc_mad Median absolute deviation of signal techincal QC (or pooled lipid extracts) samples defined by Broadhurst et. al. 2018 (6).
tqc_cv_% Coefficient of variation of signal in % from techincal QC (or pooled lipid extracts) samples defined by Broadhurst et. al. 2018 (6).
tqc_cv_robust_% Robust coefficient of variation in % of signal from techincal QC (or pooled lipid extracts) samples defined by Broadhurst et. al. 2018 (6).
spl_mean Mean signal from experimental (unknown) samples defined by Broadhurst et. al. 2018 (6).
spl_median Median signal from experimental (unknown) defined by Broadhurst et. al. 2018 (6).
spl_sd Standard deviation of signal from experimental (unknown) defined by Broadhurst et. al. 2018 (6).
spl_mad Median absolute deviation of signal from experimental (unknown) defined by Broadhurst et. al. 2018 (6).
spl_cv_% Coefficient of variation in % of signal from experimental (unknown) defined by Broadhurst et. al. 2018 (6).
spl_cv_robust_% Robust coefficient of variation in % of signal from experimental (unknown) defined by Broadhurst et. al. 2018 (6).
pblk_median Median signal from processed blank samples defined by Broadhurst et. al. 2018 (6).
pblk_to_tqc_ratio_% Ratio of median signal from processed blank sample to median signal from techincal QC (or pooled lipid extracts) samples.
bqc_cv_>20% Set to TRUE if bqc_cv is greater than 20%.
tqc_cv_>20% Set to TRUE if tqc_cv is greater than 20%.
pblk_to_tqc_ratio_>10% Set to TRUE if pblk_to_tqc_ratio_% is greater than 10%.

The relevant functions to do these calculations are as follows.

Code
#' Function used to calculate statistics summary (RSD,Signal to Blank Ratio) of a given input numeric vector
#'
#' @param lipid_data_vector Input lipid data numeric vector
#' @param prepend_string Input characters to prepend to the result table column name
#'
#' @return Output statistical summary table (RSD,Signal to Blank Ratio)
#' @export
get_sample_description <- function(lipid_data_vector, prepend_string) {

  description_table <- tibble::tibble(
    mean = lipid_data_vector |> mean(na.rm = TRUE),
    median = lipid_data_vector |> stats::median(na.rm = TRUE),
    sd = lipid_data_vector |> stats::sd(na.rm = TRUE),
    mad = lipid_data_vector |> stats::mad(na.rm = TRUE,constant = 1),
  ) |>
    dplyr::mutate(
      `cv_%` = .data$sd/.data$mean * 100,
      `cv_robust_%` = (1.4826 * .data$mad)/.data$median * 100
  )
  
  description_table <- description_table |>
      dplyr::rename_all(~paste0(prepend_string,"_",.))

  return(description_table)
}

#' Function used to calculate QC Sample Types statistics summary (RSD,Signal to Blank Ratio) of a given column name from the input data set
#'
#' @param lipid_Data Input lipid data that must contain the columns from `qc_sample_type_var` and `signal_var`
#' @param qc_sample_type_var Input column name indicating QC sample_type
#' @param signal_var Input column name to extract from the input lipid data and calculate qc sample type statistics
#' 
#' @return Output QC Sample Types statistical summary table (RSD,Signal to Blank Ratio)
#' @export
get_qc_sample_summary <- function(lipid_data, 
                                  qc_sample_type_var, 
                                  signal_var) {
  
  spl_summary <- lipid_data |> 
    dplyr::filter(.data[[qc_sample_type_var]] == "SPL") |> 
    dplyr::pull(.data[[signal_var]]) |> 
    get_sample_description(prepend_string = "spl")
  
  bqc_summary <- lipid_data |> 
    dplyr::filter(.data[[qc_sample_type_var]] == "BQC") |> 
    dplyr::pull(.data[[signal_var]]) |> 
    get_sample_description(prepend_string = "bqc")
  
  tqc_summary <- lipid_data |> 
    dplyr::filter(.data[[qc_sample_type_var]] == "TQC") |> 
    dplyr::pull(.data[[signal_var]]) |> 
    get_sample_description(prepend_string = "tqc")
  
  tqc_median <- lipid_data |> 
    dplyr::filter(.data[[qc_sample_type_var]] %in% c("TQC")) |> 
    dplyr::pull(!!signal_var) |> 
    stats::median(na.rm = TRUE)
  
  pblk_median <- lipid_data |> 
    dplyr::filter(.data[[qc_sample_type_var]] == "PBLK") |> 
    dplyr::pull(!!signal_var) |> 
    stats::median(na.rm = TRUE)
  
  `pblk_to_tqc_ratio_%` <- pblk_median/tqc_median * 100
  
  pblk_summary <- tibble::tibble(
    pblk_median = pblk_median,
    `pblk_to_tqc_ratio_%` = `pblk_to_tqc_ratio_%`
  )

  qc_stat_table <- dplyr::bind_cols(bqc_summary, 
                                    tqc_summary,
                                    spl_summary,
                                    pblk_summary)

  return(qc_stat_table)

}

Here are the codes for our workflow.

Code
summary_data <- summary_data |> 
  dplyr::mutate(
    sample_summary = purrr::map(.x = .data[["sample_data"]], 
                                .f = get_qc_sample_summary,
                                qc_sample_type_var = "QC_Sample_Type",
                                signal_var = "Area")
  ) |> 
  tidyr::unnest(dplyr::all_of("sample_summary")) |>
  dplyr::mutate(
    `bqc_cv_>20%` = dplyr::case_when(
      .data[["bqc_cv_%"]] > 20 ~ "TRUE",
      .data[["bqc_cv_%"]] <= 20 ~ "FALSE",
      TRUE ~ "TRUE"
    ),
    `tqc_cv_>20%` = dplyr::case_when(
      .data[["tqc_cv_%"]] > 20 ~ "TRUE",
      .data[["tqc_cv_%"]] <= 20 ~ "FALSE",
      TRUE ~ "TRUE"
    ),
    `pblk_to_tqc_ratio_>10%` = dplyr::case_when(
      .data[["pblk_to_tqc_ratio_%"]] > 10 ~ "TRUE",
      .data[["pblk_to_tqc_ratio_%"]] <= 20 ~ "FALSE",
      TRUE ~ "TRUE"
    )
  )

Here is a preview of the first five rows of the summary_data.

Code
summary_data[1:5,] |>
  reactable::reactable(
    defaultColDef = reactable::colDef(minWidth = 250),
    columns = list(
      Transition_Name = reactable::colDef(
      # Freeze first column
      sticky = "left",
      style = list(borderRight = "1px solid #eee"),
      headerStyle = list(borderRight = "1px solid #eee"),
      minWidth = 170),
      sample_data = reactable::colDef(
        details = function(index) {
          filtered_data <- annotated_data[["sample_data"]][index][[1]]
          htmltools::div(style = "padding: 1rem",
                         reactable::reactable(
                           data = filtered_data[1:10,],
                           defaultPageSize = 5,
                           defaultColDef = reactable::colDef(minWidth = 250),
                           columns = list(Sample_Name = reactable::colDef(minWidth = 150)),
                           outlined = TRUE,
                           highlight = TRUE)
                       )}),
      dilution_data = reactable::colDef(
        details = function(index) {
          filtered_data <- annotated_data[["dilution_data"]][index][[1]]
          htmltools::div(style = "padding: 1rem",
                         reactable::reactable(
                           data = filtered_data[1:5,],
                           defaultPageSize = 5,
                           defaultColDef = reactable::colDef(minWidth = 250),
                           columns = list(Sample_Name = reactable::colDef(minWidth = 150)),
                           outlined = TRUE,
                           highlight = TRUE)
                       )})
      )
    )

Plotting Palette

In this section, we create the palette for our dilution plots as well as our injection sequence plots. The Okato-Ito palette (9) is used.

The relevant utility functions to do this are as follows.

Code
#' @title Create Cyclic Character Sequence
#' @description Create cyclic character sequence
#' @param group_name A character vector as input
#' @param output_length The length of the output sequence
#' @return A cyclic character sequence with length `output_length`
#' @details Taken from
#' https://community.rstudio.com/t/fill-in-a-sequence-of-letters-based-on-a-given-order/88823/3
#' @examples
#' group_name <- c("red", "green", "blue")
#' create_char_seq(group_name, output_length = 2)
#' create_char_seq(group_name, output_length = 5)
#'
#' @rdname create_char_seq
#' @export
create_char_seq <- function(group_name, output_length) {

  # whole integer division
  i <- output_length %/% length(group_name)

  # remainder
  r <- output_length %% length(group_name)

  # Set the number of cycles needed
  if (r > 0) {
    t <- i + 1
  } else {
    t <- i
  }

  # Create the set and cut by the output_length
  set <- rep(group_name, t)
  output <- set[1:output_length]

  return(output)

}

We create a colour for each dilution batch name. The palette for the dilution plot are created as such.

Code
# Get the dilution batch name from dilution_annot
dilution_batch_name <- dilution_annot |> 
  dplyr::pull(.data[["Dilution_Batch_Name"]]) |> 
  unique() |> 
  as.character()

# Create palette for each dilution batch for plotting
dilution_pal <- c("#377eb8") |>  
  create_char_seq(output_length = length(dilution_batch_name)) |> 
  stats::setNames(dilution_batch_name)

scales::show_col(dilution_pal)

We create a colour for each QC sample type. The palette for the injection sequence plot are created as such.

Code
qc_sample_type <- c("SPL","TQC", "BQC","NIST", 
                    "LTR", "PBLK","RQC")

# Create palette for each qc sample type for plotting
qc_sample_type_pal <- c("#F0E442", "#0072B2", "#D55E00","#009E73",
                        "#56B4E9", "#CC79A7", "#E69F00") |>  
  create_char_seq(output_length = length(qc_sample_type)) |> 
  stats::setNames(qc_sample_type)

scales::show_col(qc_sample_type_pal)

Plot Dilution Curves

With the palette created, we will create a dilution plot for each transition.

The relevant functions to do this plot are as follows.

Code
#' @title Plot Dilution Curve Using `plotly`
#' @description Plot Dilution Data using `plotly`
#' @param dilution_data A data frame or tibble containing dilution data
#' @param dilution_title Title to use for each dilution plot
#' @param dilution_pal Input palette for each dilution batch group in `dil_batch_var`.
#' It is a named char vector where each value is a colour and
#' name is a dilution batch group given in `dil_batch_var`
#' @param sample_name_var Column name in `dilution_data`
#' to indicate the sample name
#' @param dil_batch_var Column name in `dilution_data`
#' to indicate the group name of each dilution batch,
#' used to colour the points in the dilution plot
#' @param conc_var Column name in `dilution_data` to indicate concentration
#' @param conc_var_units Unit of measure for `conc_var` in the dilution plot
#' @param conc_var_interval Distance between two tick labels
#' @param signal_var Column name in `dilution_data` to indicate signal
#' @param x_axis_title The x axis title for the graph. If there is no input,
#' column name corresponding to `conc_var` will be used instead.
#' @param y_axis_title The y axis title for the graph. If there is no input,
#' column name corresponding to `signal_var` will be used instead.
#' @param y_axis_rangemode Wrapper of `plotly` `rangemode`. If "normal" the
#' y range should be computed in relation to the extrema of the input data.
#' If "tozero", the range extends to 0. If "nonnegative",
#' the range is non-negative, regardless of the input data.
#' Default: "normal"
#' @param lin_reg_name The name of the linear regression line used to fit
#' the dilution data. The name will be displayed in the legend.
#' Default: "lin reg"
#' @param quad_reg_name The name of the quadratic regression line used to
#' fit the dilution data. The name will be displayed in the legend.
#' Default: "quad reg"
#' @param show_legend Decide if we want to create legends for the plot.
#' If we are creating a sub plot with shared legends, ensure that only
#' the first plot has show_legend set to TRUE, the rest must be set to FALSE.
#' Default: TRUE
#' @param show_y_axis_title Decide if we want to show the y-axis title.
#' Default: TRUE
#' @param annotation_var Column name in `dilution_data` to show in hover point
#' @return Output `plotly` dilution plot data of one dilution batch per transition
#' @rdname plot_dil_curve_plotly
#' @export
plot_dil_curve_plotly <- function(dilution_data,
                                  dilution_title,
                                  dilution_pal,
                                  sample_name_var = "Sample_Name",
                                  dil_batch_var = "Dilution_Batch_Name",
                                  conc_var = "Dilution_Percent",
                                  conc_var_units = "%",
                                  conc_var_interval = 50,
                                  signal_var = "Area",
                                  x_axis_title = "",
                                  y_axis_title = "",
                                  y_axis_rangemode = "normal",
                                  lin_reg_name = "lin reg",
                                  quad_reg_name = "quad reg",
                                  show_legend = TRUE,
                                  show_y_axis_title = TRUE,
                                  annotation_var = c()) {
  
  # Drop rows whose value of signal_var is NA
  dilution_data <- dilution_data |>
    tidyr::drop_na(.data[[signal_var]])
  
  # For the hover text
  text_input <- glue::glue(
    "<b>{dilution_data[[sample_name_var]]}</b>\\
     <br>{conc_var}: {dilution_data[[conc_var]]}\\
     <br>{signal_var}: {format(dilution_data[[signal_var]], big.mark = ",", nsmall = 1)}"
  )

  for(things in annotation_var) {
    text_input <- glue::glue("{text_input}\\
                              <br>{things}: {dilution_data[[things]]}")
  }
  
  # Convert the column that holds the dilution_batch_var
  # to factors
  dilution_data[[dil_batch_var]] <- dilution_data[[dil_batch_var]] |>
    # To handle the case that batches are named "1", "2", "3"
    as.character() |>
    # Factor batch based on the order of the palette colours
    # So that the order will match
    factor(levels = names(dilution_pal))
  
  # Create the dots in the dilution plot
  p <- plotly::plot_ly() |>
    plotly::add_trace(data = dilution_data,
                      x = dilution_data[[conc_var]],
                      y = dilution_data[[signal_var]],
                      type = "scatter", mode = "markers",
                      marker = list(size = 10, opacity = 1,
                                    line = list(color = "black", width = 1.5)),
                      name = dilution_data[[dil_batch_var]],
                      color = dilution_data[[dil_batch_var]],
                      # See https://github.com/plotly/plotly.R/issues/1985
                      colors = unname(dilution_pal),
                      hoverinfo = "text",
                      text = dilution_data[[sample_name_var]],
                      hovertemplate = text_input,
                      legendgroup = "group1",
                      showlegend = show_legend,
                      inherit = FALSE)
  
  if (nrow(dilution_data) > 3) {

     # When we need to plot a horizontal line
     if (stats::sd(dilution_data[[signal_var]]) == 0 ) {

      min_x <- min(dilution_data[[conc_var]], na.rm = TRUE)
      max_x <- max(dilution_data[[conc_var]], na.rm = TRUE)
      cont_y <- unique(dilution_data[[signal_var]])

      p <- p |>
        plotly::add_segments(x = min_x, xend = max_x,
                             y = cont_y, yend = cont_y,
                             name = lin_reg_name,
                             line = list(color = "black", width = 1),
                             legendgroup = "group2",
                             showlegend = show_legend,
                             inherit = FALSE)

     } else if (stats::sd(dilution_data[[conc_var]]) == 0) {
      # When we need to plot a vertical line
      min_y <- min(dilution_data[[signal_var]], na.rm = TRUE)
      max_y <- max(dilution_data[[signal_var]], na.rm = TRUE)
      cont_x <- unique(dilution_data[[conc_var]])

      p <- p |>
        plotly::add_segments(x = cont_x, xend = cont_x,
                             y = min_y, yend = max_y,
                             name = lin_reg_name,
                             line = list(color = "black", width = 1),
                             legendgroup = "group2",
                             showlegend = show_legend,
                             inherit = FALSE)

     } else {
      # Plot the curves

      # Model the data
      linear_model <- create_linear_model(dilution_data, conc_var, signal_var)
      quad_model <- create_quad_model(dilution_data, conc_var, signal_var)

      dilution <- seq(min(dilution_data[[conc_var]]),
                      max(dilution_data[[conc_var]]),
                      length.out = 15)

      # See https://github.com/plotly/plotly.R/issues/1985

      linear_prediction <- linear_model |>
        stats::predict(tibble::tibble(!!conc_var := dilution)) |>
        unname()

      quad_prediction <- quad_model |>
        stats::predict(tibble::tibble(!!conc_var := dilution)) |>
        unname()

      # Create the linear and quadratic curve in the dilution plot
      p <- p |>
        plotly::add_trace(data = dilution_data, x = dilution,
                          y = linear_prediction,
                          type = "scatter", mode = "lines", name = lin_reg_name,
                          line = list(color = "black", width = 1),
                          legendgroup = "group2",
                          showlegend = show_legend,
                          inherit = FALSE) |>
        plotly::add_trace(data = dilution_data, x = dilution,
                          y = quad_prediction,
                          type = "scatter", mode = "lines", name = quad_reg_name,
                          line = list(color = "red", width = 1, opacity = 0.25),
                          legendgroup = "group3",
                          showlegend = show_legend,
                          inherit = FALSE)
     }
  }
  
  # Create x axis title
  # If conc_var_units is empty, do not add brackets
  if (x_axis_title != "") {
    x_title <- x_axis_title
  } else {
    x_title <- conc_var
  }

  if (conc_var_units != "") {
    x_title <- paste0(x_title, " (",  conc_var_units, ")")
  }

  # Create y axis title
  if (y_axis_title != "") {
    y_title <- y_axis_title
  } else {
    y_title <- signal_var
  }

  # Create the layout to be the same as ggplot2
  p <- p |>
    plotly::layout(title = list(text = dilution_title,
                                x = 0.1) ,
                   xaxis = list(title = x_title,
                                titlefont = list(size = 10),
                                gridcolor = "rgb(255,255,255)",
                                showgrid = TRUE,
                                showline = FALSE,
                                showticklabels = TRUE,
                                tickcolor = "rgb(127,127,127)",
                                ticks = "outside",
                                zeroline = FALSE,
                                tickfont = list(size = 10),
                                tick0 = 0,
                                dtick = conc_var_interval,
                                showspikes = TRUE,
                                spikemode = "toaxis+marker",
                                spikesnap = "data"),
                   yaxis = list(title = NA,
                                autorange = TRUE,
                                fixedrange = FALSE,
                                rangemode = y_axis_rangemode,
                                titlefont = list(size = 10),
                                gridcolor = "rgb(255,255,255)",
                                showgrid = TRUE,
                                showline = FALSE,
                                showticklabels = TRUE,
                                tickcolor = "rgb(127,127,127)",
                                ticks = "outside",
                                zeroline = FALSE,
                                tickfont = list(size = 10),
                                exponentformat = "e",
                                showspikes = TRUE,
                                spikemode = "toaxis+marker",
                                spikesnap = "data"),
                   hovermode = "x",
                   legend = list(orientation = "v",
                                 font = list(size = 10)),
                   paper_bgcolor = "rgb(255,255,255)",
                   plot_bgcolor = "rgb(229,229,229)"
                   #showlegend = show_legend
    )

  # Add the y axis title
  if (isTRUE(show_y_axis_title)) {

    p <- p |>
      plotly::add_annotations(
        x = 0,
        y = 1,
        xref = "paper",
        yref = "paper",
        xanchor = "left",
        yanchor = "bottom",
        text = y_title,
        showarrow = FALSE
      )
  }

  return(p)
  
}

Here is the code that plots the dilution curve for each transition. A new column dilution_panel is created to contain such a plot. Both nested data dilution_data and sample_data are removed in the process.

Code
dilution_plot_table <- summary_data |> 
  dplyr::mutate(
    dilution_panel = trelliscopejs::pmap_plot(
      list(dilution_data = .data[["dilution_data"]]),
      plot_dil_curve_plotly,
      dilution_title = "",
      dilution_pal = dilution_pal,
      sample_name_var = "Sample_Name",
      dil_batch_var = "Dilution_Batch_Name",
      conc_var = "Relative_Sample_Amount_[%]",
      conc_var_units = "%",
      conc_var_interval = 25,
      signal_var = "Area",
      x_axis_title = "Relative Sample Amount",
      y_axis_title = "Area",
      y_axis_rangemode = "normal",
      lin_reg_name = "lin reg",
      quad_reg_name = "quad reg",
      show_legend = TRUE,
      show_y_axis_title = TRUE,
      annotation_var = c("Dilution_Batch_Name",
                         "Injection_Sequence",
                         "Acqusition_Time_Stamp",
                         "Vial_Position")
    )
  ) |> 
  dplyr::select(-dplyr::any_of(c("dilution_data", 
                                 "sample_data")))

Here is a preview of the dilution plot of first row of the dilution_plot_table.

Code
manipulateWidget::combineWidgets(ncol = 2,
                                 nrow = 2,
                                 dilution_plot_table$dilution_panel[[1]],
                                 dilution_plot_table$dilution_panel[[2]],
                                 dilution_plot_table$dilution_panel[[3]],
                                 dilution_plot_table$dilution_panel[[4]])

Plot Injection Sequence

The injection sequence plot consist of a scatter plot and a raincloud plot.

Scatter Plot

The relevant function to do the scatter plot is as follows.

Code
#' @title Plot Scatter Plot Using `plotly`
#' @description Plot Scatter Plot using `plotly`
#' @param scatterplot_data A data frame or tibble containing data
#' to be plotted in the scatter plot
#' @param scatterplot_pal Input palette for category group
#' It is a named char vector where each value is a colour and
#' name is a category group.
#' @param x_axis_var Column name in `scatterplot_data` to use
#' as x axis of scatter plot. Default: 'Injection_Sequence'
#' @param y_axis_var Column name in `scatterplot_data` to use
#' as y axis of scatter plot. Default: 'Area'
#' @param sample_name_var Column name in `scatterplot_data` to use
#' in the hover text for each point in the scatter plot.
#' Default: 'Sample_Name'
#' @param legend_category_var Column name in `scatterplot_data` to use
#' for legend and colour, Default: 'QC_Sample_Type'
#' @param legend_title Input character to use as legend title. 
#' If there is no input, input character from `legend_category_var` 
#' will be used instead. Default: ''
#' @param annotation_var Column name in `scatterplot_data` to show in hover point
#' Default: c()
#' @param lod_val Input numeric to indicate the limit of detection value in 
#' the scatter plot as a red horizontal line. It is not plotted if no input
#' is provided. Default: NA
#' @param show_y_axis_title Decide if we want to show the y-axis title.
#' Default: FALSE
#' @param show_legend Decide if we want to create legends for the plot.
#' If we are creating a sub plot with shared legends, ensure that only
#' the first plot has show_legend set to TRUE, the rest must be set to FALSE.
#' Default: TRUE
#' @return Output `plotly` scatter plot
#' @rdname plot_scatterplot_plotly
#' @export
plot_scatterplot_plotly <- function(scatterplot_data,
                                    scatterplot_pal,
                                    x_axis_var = "Injection_Sequence",
                                    y_axis_var = "Area",
                                    sample_name_var = "Sample_Name",
                                    legend_category_var = "QC_Sample_Type",
                                    legend_title = "",
                                    annotation_var = c(),
                                    lod_val = NA,
                                    show_y_axis_title = FALSE,
                                    show_legend = TRUE) {
  
  # Drop rows whose value of signal_var is NA
  scatterplot_data <- scatterplot_data |> 
    tidyr::drop_na(.data[[y_axis_var]])
  
  # For the hover text
  text_input <- glue::glue(
    "<b>{scatterplot_data[[sample_name_var]]}</b>\\
     <br>{x_axis_var}: {scatterplot_data[[x_axis_var]]}\\
     <br>{y_axis_var}: {format(scatterplot_data[[y_axis_var]], big.mark = ",", nsmall = 1)}"
  )
  
  for (things in annotation_var) {
    text_input <- glue::glue("{text_input}\\
                              <br>{things}: {scatterplot_data[[things]]}")
  }
  
  # Convert the column that holds the legend_category_var
  # to factors
  scatterplot_data[[legend_category_var]] <- scatterplot_data[[legend_category_var]] |>
    # To handle the case that batches are named "1", "2", "3"
    as.character() |>
    # Factor batch based on the order of the palette colours
    # So that the order will match
    factor(levels = names(scatterplot_pal))
  
  scatter <- plotly::plot_ly() |>
    plotly::add_trace(data = scatterplot_data,
                      x = scatterplot_data[[x_axis_var]], 
                      y = scatterplot_data[[y_axis_var]],
                      type = "scattergl", 
                      mode = "markers",
                      visible = TRUE,
                      marker = list(
                        size = 8 ,
                        opacity = 1,
                        line = list(color = "black", width = 1.5)
                      ),
                      name = scatterplot_data[[legend_category_var]],
                      color = scatterplot_data[[legend_category_var]],
                      colors = unname(scatterplot_pal),
                      legendgroup = scatterplot_data[[legend_category_var]],
                      showlegend = show_legend,
                      hoverinfo = "text",
                      text = text_input,
                      inherit = FALSE) |>
    # Create the layout to be the same as ggplot2
    plotly::layout(xaxis = list(title = x_axis_var, 
                                titlefont = list(size = 10),
                                tickfont = list(size = 10), 
                                tickangle = 0,
                                gridcolor = "rgb(255,255,255)",
                                showgrid = TRUE,
                                showticklabels = TRUE,
                                tickcolor = "rgb(127,127,127)",
                                ticks = 'outside',
                                zeroline = FALSE,
                                showspikes = TRUE, 
                                spikemode = "toaxis+marker",
                                spikesnap = "data",
                                rangeslider = list(thickness = 0.05),
                                autorange = TRUE, 
                                fixedrange = FALSE),
                   yaxis = list(title = "",
                                titlefont = list(size = 10),
                                tickfont = list(size = 10),
                                gridcolor = "rgb(255,255,255)",
                                showgrid = TRUE,
                                showline = FALSE,
                                showticklabels = TRUE,
                                tickcolor = "rgb(127,127,127)",
                                ticks = "outside",
                                zeroline = FALSE,
                                showspikes = TRUE,
                                spikemode = "toaxis+marker",
                                spikesnap = "data",
                                autorange = TRUE,
                                fixedrange = FALSE),
                   hovermode = "closest",
                   legend = list(title = list(text = legend_title),
                                 orientation = 'v', 
                                 font = list(size = 10)),
                   paper_bgcolor = "rgb(255,255,255)",
                   plot_bgcolor = "rgb(229,229,229)"
                   )
  
  # Add the LOD line
  if (!isTRUE(is.na(lod_val))) {
    
    scatter <- scatter |> 
      plotly::add_trace(data = scatterplot_data,
                        x = scatterplot_data[[x_axis_var]],
                        y = lod_val, yend = lod_val,
                        type = "scattergl" ,
                        mode = "lines",
                        name = "LOD Line",
                        line = list(color = "red",
                                    width = 2,
                                    dash = "solid"),
                        inherit = FALSE)
  }
    
  # Add the y axis title
  if (isTRUE(show_y_axis_title)) {
    scatter <- scatter |>
      plotly::add_annotations(
        x = 0,
        y = 1,
        xref = "paper",
        yref = "paper",
        yanchor = "bottom",
        text = y_axis_var,
        showarrow = FALSE
      )
  }
  
  return(scatter)
  
}

The first step is to create a scatter plot (Area vs Injection Sequence) for each transition. A new column scatterplot_panel is created to contain such a plot.

Code
scatterplot_table <- summary_data |>  
  dplyr::mutate(
    scatterplot_panel = trelliscopejs::pmap_plot(
      list(scatterplot_data = .data[["sample_data"]]),
      plot_scatterplot_plotly,
      scatterplot_pal = qc_sample_type_pal,
      x_axis_var = "Injection_Sequence",
      y_axis_var = "Area",
      sample_name_var = "Sample_Name",
      legend_category_var = "QC_Sample_Type",
      legend_title = "<b>QC Sample Type</b>",
      annotation_var = c("Sample_Name",
                         "Injection_Sequence",
                         "Acqusition_Time_Stamp",
                         "Vial_Position",
                         "QC_Sample_Type"),
      lod_val = 1500,
      show_y_axis_title = TRUE,
      show_legend = TRUE
      )
  ) 

Here is a preview of the scatter plot of first row of the scatterplot_table.

Code
scatterplot_table$scatterplot_panel[[1]]

Raincloud Plot

The relevant utility function is as follows.

Code
remove_rqc <- function(input_data) {
  
  input_data <- input_data |>
      dplyr::filter(.data[["QC_Sample_Type"]] != "RQC")
  
  return(input_data)
  
}

The relevant function to do the raincloud plot is as follows.

Code
#' @title Plot Raincloud Plot Using `plotly`
#' @description Plot Raincloud Plot using `plotly`
#' @param raincloud_data A data frame or tibble containing data to be plotted
#' into a raincloud plot
#' @param raincloud_pal Input palette for category group
#' It is a named char vector where each value is a colour and
#' name is a category group
#' @param x_axis_var Column name in `raincloud_data` to use
#' as x axis of raincloud plot. Default: 'QC_Sample_Type'
#' @param y_axis_var Column name in `raincloud_data` to use
#' as y axis of raincloud plot. Default: 'Area'
#' @param sample_name_var Column name in `raincloud _data` to use
#' in the hover text for each point in the raincloud  plot.
#' Default: 'Sample_Name'
#' @param legend_category_var Column name in `raincloud _data` to use
#' for legend and colour, Default: 'QC_Sample_Type'
#' @param legend_title Input character to use as legend title. 
#' If there is no input, input character from `legend_category_var` 
#' will be used instead. Default: ''
#' @param annotation_var Column name in `raincloud_data` to show in hover point
#' Default: c()
#' @param show_y_axis_title Decide if we want to show the y-axis title.
#' Default: FALSE
#' @param show_legend Decide if we want to create legends for the plot.
#' If we are creating a sub plot with shared legends, ensure that only
#' the first plot has show_legend set to TRUE, the rest must be set to FALSE.
#' Default: TRUE
#' @return Output `plotly` raincloud plot
#' @rdname plot_raincloud_plotly
#' @export
plot_raincloud_plotly <- function(raincloud_data,
                                  raincloud_pal,
                                  x_axis_var = "QC_Sample_Type",
                                  y_axis_var = "Area",
                                  sample_name_var = "Sample_Name",
                                  legend_category_var = "QC_Sample_Type",
                                  legend_title = "",
                                  annotation_var = c(),
                                  show_y_axis_title = FALSE,
                                  show_legend = TRUE) {
  
  
  # Drop rows whose value of signal_var is NA
  raincloud_data <- raincloud_data |>
    tidyr::drop_na(.data[[y_axis_var]])
  
  # For the hover text
  text_input <- glue::glue(
    "<b>{raincloud_data[[sample_name_var]]}</b>\\
     <br>{x_axis_var}: {raincloud_data[[x_axis_var]]}\\
     <br>{y_axis_var}: {format(raincloud_data[[y_axis_var]], big.mark = ",", nsmall = 1)}"
  )
  
  for (things in annotation_var) {
    text_input <- glue::glue("{text_input}\\
                              <br>{things}: {raincloud_data[[things]]}")
  }
  
  # Convert the column that holds the legend_category_var
  # to factors
  raincloud_data[[legend_category_var]] <- raincloud_data[[legend_category_var]] |>
    # To handle the case that batches are named "1", "2", "3"
    as.character() |>
    # Factor batch based on the order of the palette colours
    # So that the order will match
    factor(levels = names(raincloud_pal))
  
  
  violin <- plotly::plot_ly() |>
    plotly::add_trace(data = raincloud_data,
                      x = raincloud_data[[x_axis_var]],
                      y = raincloud_data[[y_axis_var]],
                      type = "violin",
                      opacity = 1,
                      #type = 'box',
                      color = raincloud_data[[legend_category_var]],
                      colors = unname(raincloud_pal),
                      legendgroup = raincloud_data[[legend_category_var]],
                      showlegend = show_legend,
                      text = text_input,
                      orientation = "v",
                      box = list(
                        visible = TRUE,
                        width = 1,
                        line = list(color = "black"),
                        fillcolor = 'white'
                      ),
                      side = "positive",
                      #boxpoints = "all",
                      points = "suspectedoutliers",
                      jitter = 0,
                      #boxmean = "sd",
                      meanline = list(visible = TRUE,
                                      color = "red"),
                      marker = list(
                        size = 7 ,
                        opacity = 1,
                        #outliercolor = 'rgba(133,231,1,1)',
                        line = list(color = "black", width = 1.5)
                      ),
                      pointpos = -1,
                      inherit = FALSE) |> 
    plotly::layout(xaxis = list(title = x_axis_var,
                                titlefont = list(size = 10),
                                tickfont = list(size = 10), 
                                tickangle = 0,
                                gridcolor = "rgb(255,255,255)",
                                type = 'category',
                                showgrid = TRUE,
                                showline = FALSE,
                                showticklabels = TRUE,
                                tickcolor = "rgb(127,127,127)",
                                ticks = 'outside',
                                zeroline = FALSE,
                                tickfont = list(size = 10),
                                autorange = TRUE, 
                                fixedrange = FALSE),
                   yaxis = list(title = "",
                                titlefont = list(size = 10),
                                tickfont = list(size = 10),
                                gridcolor = "rgb(255,255,255)",
                                showgrid = TRUE,
                                showline = FALSE,
                                showticklabels = TRUE,
                                tickcolor = "rgb(127,127,127)",
                                ticks = "outside",
                                zeroline = FALSE,
                                tickfont = list(size = 10),
                                autorange = TRUE, 
                                fixedrange = FALSE),
                   hovermode = "closest",
                   legend = list(title = list(text = legend_title),
                                 orientation = "v", 
                                 font = list(size = 10)),
                   paper_bgcolor = "rgb(255,255,255)",
                   plot_bgcolor = "rgb(229,229,229)"
                   )
  
  if (isTRUE(show_y_axis_title)) {
    violin <- violin |>
      plotly::add_annotations(
        x = 0,
        y = 1,
        xref = "paper",
        yref = "paper",
        yanchor = "bottom",
        text = y_axis_var,
        showarrow = FALSE
      )
  }
  
  return(violin)
}

The second step is to create a raincloud plot (QC Sample Type (without RQC) vs Injection Sequence) for each transition. A new column called no_rqc_data is created from sample_data to filter out the RQC samples. The filtered samples are then used to create the plot. A new column raincloud_panel is created to contain such a plot.

Code
raincloud_table <- scatterplot_table |>
  dplyr::mutate(
    no_rqc_data = purrr::map(.x = .data[["sample_data"]],
                             .f = remove_rqc)
    ) |>  
  dplyr::mutate(
    raincloud_panel = trelliscopejs::pmap_plot(
      list(raincloud_data = .data$no_rqc_data),
      plot_raincloud_plotly,
      raincloud_pal = qc_sample_type_pal,
      x_axis_var = "QC_Sample_Type",
      y_axis_var = "Area",
      sample_name_var = "Sample_Name",
      legend_category_var = "QC_Sample_Type",
      legend_title = "<b>QC Sample Type</b>",
      annotation_var = c("Sample_Name",
                         "Injection_Sequence",
                         "Acqusition_Time_Stamp",
                         "Vial_Position"),
      show_y_axis_title = FALSE,
      show_legend = FALSE
    )
  )

Here is a preview of the raincloud plot of first row of the raincloud_table.

Code
raincloud_table$raincloud_panel[[1]]

Injection Sequence Plot

The relevant utility functions are as follows.

Code
arrange_plots <- function(scatterplot_panel, raincloud_panel) {
  
  combined_plots <- plotly::subplot(scatterplot_panel,
                                    raincloud_panel,
                                    widths = c(0.6, 0.4),
                                    margin = 0.01,
                                    shareX = TRUE,
                                    shareY = TRUE
  )
  return(combined_plots)
  
}

The last step is to combine the two plots together. dilution_data, sample_data, no_rqc_data, scatterplot_panel and raincloud_panel are removed in the process.

Code
injection_sequence_table <- raincloud_table |> 
  dplyr::mutate(
    panel = trelliscopejs::pmap_plot(
      .l = list(
        scatterplot_panel = .data$scatterplot_panel,
        raincloud_panel = .data$raincloud_panel
      ),
      .f = arrange_plots
  )) |>  
  dplyr::select(
    -dplyr::any_of(
      c("dilution_data","sample_data","no_rqc_data",
        "scatterplot_panel", "raincloud_panel")
      )
    )

Here is a preview of the injection sequence plot of first row of the injection_sequence_table.

Code
injection_sequence_table$panel[[1]]

Cognostics

To output these interactive plots as a trellis plot in html, we need to convert our current tibble and dilution_plot_table and injection_sequence_table into a cognostics dataframe.

The cognostics dataframe consist of three components

  1. A group of columns known as conditioning variables. They will form the unique id of the trellis plot. As such, each row of these columns must be unique. In our example, the column used is Transition_Name
  2. One column that holds the images to display each plot in the trellis. This column is known as the panel variable. We just created this column using trelliscopejs::pmap_plot earlier.
  3. The other columns will be grouped as general cognostics columns.

Cognostics Conversion Functions

The relevant functions to convert all columns, except the panel variable, to cognostics objects are as follows.

Code
#' @title Validate Cognostics Data
#' @description Validate Cognostics Data
#' @param cog_df A data frame or tibble that contains cognostics information
#' @param needed_column A vector consisting of needed column names that
#' must be found in `cog_df`,
#' Default:
#' `c("col_name_vec", "desc_vec", "type_vec")`
#' @return An error if the things in `needed_column`
#' is not found in the Cognostics Data
#' @examples
#' # Create Cognostics Dataframe
#' col_name_vec <- c("Transition_Name", "Dilution_Batch_Name")
#'
#' desc_vec <- c("Transition_Name", "Dilution_Batch_Name")
#'
#' type_vec <- c("factor","factor")
#'
#' cog_df <- data.frame(col_name_vec = col_name_vec,
#'                      desc_vec = desc_vec,
#'                      type_vec = type_vec)
#'
#' validate_cog_df(cog_df)
#'
#' @rdname validate_cog_df
#' @export
validate_cog_df <- function(cog_df,
                            needed_column = c("col_name_vec",
                                              "desc_vec",
                                              "type_vec")) {


  # Check if things in needed_column are in cog_df
  assertable::assert_colnames(cog_df, needed_column,
                              only_colnames = FALSE, quiet = TRUE)


}

#' @title Update Cognostics Manually
#' @description Update cognostics on `summary_table` based on
#' the cognostics parameters given by `cog_df`. We assume
#' `cog_df` is created manually
#' @param summary_table The summary data frame or tibble generated
#' by function [summarise_dilution_data()]
#' @param cog_df  A data frame or tibble output
#' from the function [create_default_cog_df()] or created manually
#' @param col_name_vec Column name in `cog_df` to indicate the columns
#' in `summary_table` that needs to be converted to a cognostics,
#' Default: 'col_name_vec'
#' @param desc_vec Column name in `cog_df` to indicate the description
#' for each cognostics as define in `trelliscopejs::cog`,
#' Default: 'desc_vec'
#' @param type_vec Column name in `cog_df` to indicate the type
#' of each cognostics as define in `trelliscopejs::cog`,
#' Default: 'type_vec'
#' @return `summary_table` with some columns converted
#' to type cog as defined in `trelliscopejs::cog`
#' @details
#' More details in `trelliscopejs::cog` can be found in
#' <https://rdrr.io/cran/trelliscopejs/man/cog.html>
#'
#' @rdname update_cog_manual
#' @export
update_cog_manual <- function(summary_table, cog_df,
                              col_name_vec = "col_name_vec",
                              desc_vec = "desc_vec",
                              type_vec = "type_vec") {


  if (is.null(cog_df)) {
    return(summary_table)
  }

  #Check if cog_df is valid with the relevant columns
  validate_cog_df(cog_df,
                  needed_column = c(col_name_vec,
                                    desc_vec,
                                    type_vec)
  )

  # See this webpage to learn how to mutate specific columns.
  # https://stackoverflow.com/questions/52998471/dynamically-determine-if-a-dataframe-column-exists-and-mutate-if-it-does

  for (colname in colnames(summary_table)) {
    row_index <- which(cog_df$col_name_vec == colname)

    if (length(row_index) == 1) {
      summary_table <- summary_table |>
        dplyr::mutate(
          dplyr::across(
            .cols = dplyr::one_of(cog_df[[col_name_vec]][row_index]),
            .fns  = \(x) trelliscopejs::cog(
              val = x, 
              desc = cog_df[[desc_vec]][row_index], 
              type = cog_df[[type_vec]][row_index]
            )
          )
        )
    }
  }
  return(summary_table)
}

#' Update Cognostics Automatically
#' @description Update cognostics on the dilution summary based on
#' the cognostics data frame given by the function `create_default_cog_df`.
#' @param summary_table The summary table generated
#' by function [summarise_dilution_data()]
#' @return `summary_table` with some columns converted
#' to type cog as defined in `trelliscopejs::cog`
#' @details
#' More details in `trelliscopejs::cog` can be found in
#' <https://rdrr.io/cran/trelliscopejs/man/cog.html>
#' @rdname update_cog_auto
#' @export
update_cog_auto <- function(summary_table) {

  # Create the default cognostics table
  cog_df <- create_default_cog_df()

  # Update the dilution summary columns and
  # convert the relevant to cognostics
  summary_table <- summary_table |>
    update_cog_manual(cog_df = cog_df)

  return(summary_table)

}

#' @title Convert To Cognostics
#' @description Convert columns in `summary_table` to `trelliscopejs` cognostics
#' for the `Trelliscope` display
#' @param summary_table Input summary table as a
#' data frame or tibble
#' @param cog_df A data frame or tibble that contains cognostics information
#' If no input is given the cognostics information generated by function
#' [create_default_cog_df()] will be used.
#' Default: NULL
#' @param grouping_variable A character vector of
#' column names in `summary_table`to indicate how each dilution curve
#' should be grouped by. It is also going to be used as a conditional
#' cognostics in the `trelliscopejs` report,
#' Default: c("Transition_Name")
#' @param panel_variable A column name in `summary_table` to be converted
#' into a panel for the `Trelliscope` display
#' Default: NULL
#' @param col_name_vec Column name in `cog_df` to indicate the columns
#' in `summary_table` that needs to be converted to a cognostics,
#' Default: 'col_name_vec'
#' @param desc_vec Column name in `cog_df` to indicate the description
#' for each cognostics as define in `trelliscopejs::cog`,
#' Default: 'desc_vec'
#' @param type_vec Column name in `cog_df` to indicate the type
#' of each cognostics as define in `trelliscopejs::cog`,
#' Default: 'type_vec'
#' @return The dilution summary table with `grouping variable` columns
#' converted to conditional cognostics,
#' other columns in `summary_table` converted to cognostics
#' to be used in the in the `trelliscopejs` report.
#'
#' @rdname convert_to_cog
#' @export
convert_to_cog <- function(summary_table, cog_df = NULL,
                           grouping_variable = c("Transition_Name"),
                           panel_variable = NULL,
                           col_name_vec = "col_name_vec",
                           desc_vec = "desc_vec",
                           type_vec = "type_vec") {


  # Check if things in needed_column are in summary_table
  assertable::assert_colnames(summary_table, grouping_variable,
                              only_colnames = FALSE, quiet = TRUE)

  # Check if panel_variable is also a grouping variable
  if (isTRUE(panel_variable %in% grouping_variable)) {
    stop(paste("panel_variable", panel_variable,
               "cannot be a grouping_variable")
         )
  }

  # Convert logical columns to characters
  summary_table <- summary_table |>
    dplyr::mutate_if(is.logical,
                     ~as.character(.x))

  # Separate the panel variables if it is in summary_table
  panel_df <- NULL
  if (!is.null(panel_variable)) {
    panel_df <- summary_table |>
      dplyr::select(dplyr::any_of(c(grouping_variable,
                                    panel_variable)))

    summary_table <- summary_table |>
      dplyr::select(-dplyr::any_of(c(panel_variable)))
  }

  # Get cognostics for trellis report
  # First convert the columns based on default cognostics
  # from create_default_cog_df()
  # Next convert the columns based on user's input cognostics
  # Lastly, convert the rest of the columns based on class
  # Grouping variables must be the conditional columns
  summary_table <- summary_table |>
    update_cog_auto() |>
    update_cog_manual(cog_df = cog_df,
                      col_name_vec = col_name_vec,
                      desc_vec = desc_vec,
                      type_vec = type_vec) |>
    trelliscopejs::as_cognostics(cond_cols = grouping_variable,
                                 needs_cond = TRUE, needs_key = FALSE)

  #Convert panel variables if any to trelliscope_panels
  if (!is.null(panel_df) && ncol(panel_df) != length(grouping_variable)) {

    # Ensure that the grouping variable is converted to
    # conditional columns
    # Ensure that the panel variable is converted to
    # trelliscope_panel
    panel_df <- panel_df |>
      dplyr::select(dplyr::all_of(c(grouping_variable))) |>
      trelliscopejs::as_cognostics(cond_cols = grouping_variable,
                                   needs_cond = TRUE,
                                   needs_key = FALSE) |>
      dplyr::bind_cols(panel_df |>
                         dplyr::select(dplyr::any_of(c(panel_variable)))
      ) |>
      dplyr::mutate(
        dplyr::across(
          .cols = dplyr::all_of(panel_variable),
          .fns  = \(x) structure(x, class = c("trelliscope_panels","list"))
        )
      )

    # Panel_df to do a left join with dilution summary
    # Move panel_variable to the end
    summary_table <- panel_df |>
      dplyr::left_join(summary_table, by = grouping_variable) |>
      dplyr::relocate(dplyr::any_of(c(panel_variable)),
                      .after = dplyr::last_col())

  }

  return(summary_table)

}

Cognostics Setup

The function trelliscopejs::cog is used to convert a column into cognostic object. The following meta information must be provided in order for the conversion to be successful.

Given a column name, we need to provide

  • Description of the column name (meta information)
  • The type of cognostics to used. For our example only “numeric” and “factor” are used. Other factor types can be found in the documentation of trelliscopejs::cog

Here are the relevant functions to create our meta information tibble.

Code
#' @title Create Transition Name Annotation
#' Cognostics Data Frame
#' @description Create transition name annotation
#' cognostics data frame to be used to
#' convert columns to class cognostics
#' @return A dataframe of default cognostics information
#' @details Internal function to create a dataframe of default cognostics
#' information to be used by the `trelliscopejs::cog`.
#' @examples
#' transition_name_annot_cog_df <- create_transition_name_annot_cog_df()
#' transition_name_annot_cog_df
#'
#' @rdname create_transition_name_annot_cog_df
#' @export
create_transition_name_annot_cog_df <- function() {
  
  # More details in `trelliscopejs::cog` can be found in
  # <https://rdrr.io/cran/trelliscopejs/man/cog.html>

  col_name_vec <- c("Transition_Name",
                    "Lipid_Name",
                    "For_Rgoslin",
                    "Precursor_Ion",
                    "Product_Ion",
                    "Retention_Time_[min]",
                    "Delta_Retention_Time_[min]",
                    "Collision_Energy",
                    "Cell_Accelerator_Voltage",
                    "Polarity",
                    "isISTD")

  desc_vec <- c(
    "Name of the transition to be mesured in the mass spectrometer.",
    "Converted transition name to suit lipid nomenclature set by Liebisch et. al. 2020.",
    "Input transition for the R package rgoslin to annotate.",
    "The ion to be fragmented into smaller fragment ions.",
    "Ions created from fragmentation of the precursor ion.",
    "Transition name's expectred retention time.",
    "Time window to acquire the transition. Each transition is acquired in the range Retention Time +/- 0.5(Delta Retention Time).",
    "Rate of acceleration as the precursor ions enter the Q2 for fragmentation.",
    "Rate of acceleration as the product ions leave the Q2.",
    "Settings (Positive or Negative) for the mass spectrometry to detect (positive or negative) ions.",
    "Set to TRUE if the given transition name an internal standard.")

  type_vec <- c("factor",
                "factor",
                "factor",
                "numeric",
                "numeric",
                "numeric",
                "numeric",
                "numeric",
                "numeric",
                "factor",
                "factor")

  transition_annot_name_cog_df <- data.frame(
    col_name_vec = col_name_vec,
    desc_vec = desc_vec,
    type_vec = type_vec)
  
  return(transition_annot_name_cog_df)
  
}

#' @title Create Rgoslin Annotation
#' Cognostics Data Frame
#' @description Create rgoslin annotation
#' cognostics data frame to be used to
#' convert columns to class cognostics
#' @return A dataframe of default cognostics information
#' @details Internal function to create a dataframe of default cognostics
#' information to be used by the `trelliscopejs::cog`.
#' @examples
#' rgoslin_cog_df <- create_rgoslin_cog_df()
#' rgoslin_cog_df
#'
#' @rdname create_rgoslin_cog_df
#' @export
create_rgoslin_cog_df <- function() {
  
  # More details in `trelliscopejs::cog` can be found in
  # <https://rdrr.io/cran/trelliscopejs/man/cog.html> 
  
  col_name_vec <- c(
    "Lipid.Maps.Category",
    "Lipid.Maps.Main.Class",
    "Species.Name",
    "Molecular.Species.Name",
    "Functional.Class.Abbr",
    "Functional.Class.Synonyms"
  )

  desc_vec <- c(
    "Lipid MAPS' Lipid Category which the transition name belongs to.",
    "Lipid MAPS' Lipid Main Class which the transition name belongs to.",
    "Represented by the sum composition, i.e., sum of carbon atoms, double bond equivalent and number of additional oxygen atoms, e.g. FA 18:1;O.",
    "Represented by lipid species with identified fatty acyl/alkyl residues, e.g. TG 16:0_18:1_18:1.",
    "Abbreviation used to describe the lipid's functional class based on the grammar used.",
    "Alternative abbreviations used to describe the lipid's functional class."
  )

  type_vec <- c("factor",
                "factor",
                "factor",
                "factor",
                "factor",
                "factor")
  
  rgoslin_cog_df <- data.frame(col_name_vec = col_name_vec,
                               desc_vec = desc_vec,
                               type_vec = type_vec)
  
  return(rgoslin_cog_df)
}

#' @title Create Dilution Stats
#' Cognostics Data Frame
#' @description Create dilution statistics
#' cognostics data frame to be used to
#' convert columns to class cognostics
#' @return A dataframe of default cognostics information
#' @details Internal function to create a dataframe of default cognostics
#' information to be used by the `trelliscopejs::cog`.
#' @examples
#' dilution_stats_cog_df <- create_dilution_stats_cog_df()
#' dilution_stats_cog_df
#'
#' @rdname create_dilution_stats_cog_df
#' @export
create_dilution_stats_cog_df <- function() {

  col_name_vec <- c("r_corr",
                    "r2_linear",
                    "r2_linear_<=0.8")

  desc_vec <- c("Pearson Correlation R value.",
                "Linear Regression R^2 Value.",
                "Set to TRUE if r2_linear is less than 0.8.")

  type_vec <- c("numeric", "numeric",
                "factor")

  dilution_stats_cog_df <- data.frame(col_name_vec = col_name_vec,
                                      desc_vec = desc_vec,
                                      type_vec = type_vec)
  return(dilution_stats_cog_df)
}

#' @title Create QC Sample Type Stats
#' Cognostics Data Frame
#' @description Create QC sample type statistics
#' cognostics data frame to be used to
#' convert columns to class cognostics
#' @return A dataframe of default cognostics information
#' @details Internal function to create a dataframe of default cognostics
#' information to be used by the `trelliscopejs::cog`.
#' @examples
#' qc_sample_type_stats_cog_df <- create_qc_sample_type_stats_cog_df()
#' qc_sample_type_stats_cog_df
#'
#' @rdname create_qc_sample_type_stats_cog_df
#' @export
create_qc_sample_type_stats_cog_df <- function() {
  
  col_name_vec <- c(
   "bqc_mean",
   "bqc_median",
   "bqc_sd",
   "bqc_mad",
   "bqc_cv_%",
   "bqc_cv_robust_%",
   "tqc_mean",
   "tqc_median",
   "tqc_sd",
   "tqc_mad",
   "tqc_cv_%",
   "tqc_cv_robust_%",
   "spl_mean",
   "spl_median",
   "spl_sd",
   "spl_mad",
   "spl_cv_%",
   "spl_cv_robust_%",
   "pblk_median",
   "pblk_to_tqc_ratio_%",
   "bqc_cv_>20%",
   "tqc_cv_>20%",
   "pblk_to_tqc_ratio_>10%"

  )

  desc_vec <- c(
    "Mean signal from batch / pooled QC samples defined by Broadhurst et. al. 2018.",
    "Median signal from batch / pooled QC samples defined by Broadhurst et. al. 2018.",
    "Standard deviation of signal from batch / pooled QC samples defined by Broadhurst et. al. 2018.",
    "Median absolute deviation of signal from batch / pooled QC samples defined by Broadhurst et. al. 2018.",
    "Coefficient of variation of signal in % from batch / pooled QC samples defined by Broadhurst et. al. 2018.",
    "Robust coefficient of variation of signal in % from batch / pooled QC samples defined by Broadhurst et. al. 2018.",
    "Mean signal from techincal QC (or pooled lipid extracts) samples defined by Broadhurst et. al. 2018.",
    "Median signal from techincal QC (or pooled lipid extracts) samples defined by Broadhurst et. al. 2018.",
    "Standard deviation of signal from techincal QC (or pooled lipid extracts) samples defined by Broadhurst et. al. 2018.",
    "Median absolute deviation of signal techincal QC (or pooled lipid extracts) samples defined by Broadhurst et. al. 2018.",
    "Coefficient of variation of signal in % from techincal QC (or pooled lipid extracts) samples defined by Broadhurst et. al. 2018.",
    "Robust coefficient of variation in % of signal from techincal QC (or pooled lipid extracts) samples defined by Broadhurst et. al. 2018.",
    "Mean signal from experimental (unknown) samples defined by Broadhurst et. al. 2018.",
    "Median signal from experimental (unknown) defined by Broadhurst et. al. 2018.",
    "Standard deviation of signal from experimental (unknown) defined by Broadhurst et. al. 2018.",
    "Median absolute deviation of signal from experimental (unknown) defined by Broadhurst et. al. 2018.",
    "Coefficient of variation in % of signal from experimental (unknown) defined by Broadhurst et. al. 2018.",
    "Robust coefficient of variation in % of signal from experimental (unknown) defined by Broadhurst et. al. 2018.",
    "Median signal from processed blank samples defined by Broadhurst et. al. 2018.",
    "Ratio of median signal from processed blank sample to median signal from techincal QC (or pooled lipid extracts) samples defined by Broadhurst et. al. 2018.",
    "Set to TRUE if bqc_cv is greater than 20%.",
    "Set to TRUE if tqc_cv is greater than 20%.",
    "Set to TRUE if pblk_to_tqc_ratio_ % is greater than 10%."
    
  )

  type_vec <- c(
    "numeric",
    "numeric",
    "numeric",
    "numeric",
    "numeric",
    "numeric",
    "numeric",
    "numeric",
    "numeric",
    "numeric",
    "numeric",
    "numeric",
    "numeric",
    "numeric",
    "numeric",
    "numeric",
    "numeric",
    "numeric",
    "numeric",
    "numeric",
    "factor",
    "factor",
    "factor"
  )

  qc_sample_type_stats_cog_df <- data.frame(col_name_vec = col_name_vec,
                                            desc_vec = desc_vec,
                                            type_vec = type_vec)
  return(qc_sample_type_stats_cog_df)
  
}

#' @title Create Default Cognostics Data Frame
#' @description Create default cognostics data frame to be used to
#' convert columns in `dilution_summary` to class cognostics
#' @return A dataframe of default cognostics information
#' @details Internal function to create a dataframe of default cognostics
#' information to be used by the `trelliscopejs::cog`.
#' @examples
#' cog_df <- create_default_cog_df()
#' cog_df
#'
#' @rdname create_default_cog_df
#' @export
create_default_cog_df <- function() {

  # More details in `trelliscopejs::cog` can be found in
  # <https://rdrr.io/cran/trelliscopejs/man/cog.html>

  col_name_vec <- c("Transition_Name")

  desc_vec <- c("Transition_Name")

  type_vec <- c("factor")

  cog_df <- data.frame(col_name_vec = col_name_vec,
                       desc_vec = desc_vec,
                       type_vec = type_vec)
  
  transition_name_annot_cog_df <- create_transition_name_annot_cog_df()
  rgoslin_cog_df <- create_rgoslin_cog_df()
  dilution_stats_cog_df <- create_dilution_stats_cog_df()
  qc_sample_type_stats_cog_df <- create_qc_sample_type_stats_cog_df()

  cog_df <- dplyr::bind_rows(transition_name_annot_cog_df,
                             rgoslin_cog_df,
                             dilution_stats_cog_df,
                             qc_sample_type_stats_cog_df,
                             cog_df)

  return(cog_df)
}

Here is a preview of the meta information tibble

Code
create_default_cog_df() |> 
  reactable::reactable(
    defaultPageSize = 5,
    bordered = TRUE,
    highlight = TRUE,
    searchable = TRUE,
    paginationType = "jump"
  )

Cognostics Creation

We use the function convert_to_cog which uses the default cog_df created by create_default_cog_df.

Here are the attributes of some columns in dilution_plot_table before conversion to cognostics.

Code
attributes(dilution_plot_table$Transition_Name)
> NULL
Code
attributes(dilution_plot_table$Lipid_Name)
> NULL
Code
attributes(dilution_plot_table$Precursor_Ion)
> NULL
Code
attributes(dilution_plot_table$dilution_panel)
> $class
> [1] "trelliscope_panels" "list"

We convert each column in dilution_plot_table to relevant cognostics using the function convert_to_cog. With Transition_Name as a conditioning variable, dilution_panel as a panel variable.

Code
dilution_trellis <- dilution_plot_table |> 
    convert_to_cog(grouping_variable = c("Transition_Name"),
                   panel_variable = "dilution_panel")

Here are the attributes of some columns in dilution_plot_table after conversion to cognostics. Observe that the attributes of the columns are different.

For the column labelled as conditioning (grouping) variable column labelled we have.

Code
attributes(dilution_trellis$Transition_Name)
> $cog_attrs
> $cog_attrs$desc
> [1] "conditioning variable"
> 
> $cog_attrs$type
> [1] "factor"
> 
> $cog_attrs$group
> [1] "condVar"
> 
> $cog_attrs$defLabel
> [1] TRUE
> 
> $cog_attrs$defActive
> [1] TRUE
> 
> $cog_attrs$filterable
> [1] TRUE
> 
> $cog_attrs$log
> [1] NA
> 
> 
> $class
> [1] "cog"       "character"

For the column labelled as a panel variable, we have

Code
attributes(dilution_trellis$dilution_panel)
> $class
> [1] "trelliscope_panels" "list"

For the rest of the column converted to a common cognostics, we have

Code
attributes(dilution_trellis$Lipid_Name)
> $cog_attrs
> $cog_attrs$desc
> [1] "Converted transition name to suit lipid nomenclature set by Liebisch et. al. 2020."
> 
> $cog_attrs$type
> [1] "factor"
> 
> $cog_attrs$group
> [1] "common"
> 
> $cog_attrs$defLabel
> [1] FALSE
> 
> $cog_attrs$defActive
> [1] TRUE
> 
> $cog_attrs$filterable
> [1] TRUE
> 
> $cog_attrs$log
> [1] NA
> 
> 
> $class
> [1] "cog"       "character"
Code
attributes(dilution_trellis$Precursor_Ion)
> $cog_attrs
> $cog_attrs$desc
> [1] "The ion to be fragmented into smaller fragment ions."
> 
> $cog_attrs$type
> [1] "numeric"
> 
> $cog_attrs$group
> [1] "common"
> 
> $cog_attrs$defLabel
> [1] FALSE
> 
> $cog_attrs$defActive
> [1] TRUE
> 
> $cog_attrs$filterable
> [1] TRUE
> 
> $cog_attrs$log
> [1] NA
> 
> 
> $class
> [1] "cog"     "numeric"

Here are the attributes of some columns in injection_sequence_table before conversion to cognostics.

Code
attributes(injection_sequence_table$Transition_Name)
> NULL
Code
attributes(injection_sequence_table$Lipid_Name)
> NULL
Code
attributes(injection_sequence_table$Precursor_Ion)
> NULL
Code
attributes(injection_sequence_table$panel)
> $class
> [1] "trelliscope_panels" "list"

We convert each column in injection_sequence_table to relevant cognostics using the function convert_to_cog. With Transition_Name as a conditioning variable, panel as a panel variable.

Code
injection_sequence_trellis <- injection_sequence_table |> 
    convert_to_cog(grouping_variable = c("Transition_Name"),
                   panel_variable = "panel")

Here are the attributes of some columns in injection_sequence_table after conversion to cognostics. Observe that the attributes of the columns are different.

For the column labelled as conditioning (grouping) variable column labelled we have.

Code
attributes(injection_sequence_trellis$Transition_Name)
> $cog_attrs
> $cog_attrs$desc
> [1] "conditioning variable"
> 
> $cog_attrs$type
> [1] "factor"
> 
> $cog_attrs$group
> [1] "condVar"
> 
> $cog_attrs$defLabel
> [1] TRUE
> 
> $cog_attrs$defActive
> [1] TRUE
> 
> $cog_attrs$filterable
> [1] TRUE
> 
> $cog_attrs$log
> [1] NA
> 
> 
> $class
> [1] "cog"       "character"

For the column labelled as a panel variable, we have

Code
attributes(injection_sequence_trellis$panel)
> $class
> [1] "trelliscope_panels" "list"

For the rest of the column converted to a common cognostics, we have

Code
attributes(injection_sequence_trellis$Lipid_Name)
> $cog_attrs
> $cog_attrs$desc
> [1] "Converted transition name to suit lipid nomenclature set by Liebisch et. al. 2020."
> 
> $cog_attrs$type
> [1] "factor"
> 
> $cog_attrs$group
> [1] "common"
> 
> $cog_attrs$defLabel
> [1] FALSE
> 
> $cog_attrs$defActive
> [1] TRUE
> 
> $cog_attrs$filterable
> [1] TRUE
> 
> $cog_attrs$log
> [1] NA
> 
> 
> $class
> [1] "cog"       "character"
Code
attributes(injection_sequence_trellis$Precursor_Ion)
> $cog_attrs
> $cog_attrs$desc
> [1] "The ion to be fragmented into smaller fragment ions."
> 
> $cog_attrs$type
> [1] "numeric"
> 
> $cog_attrs$group
> [1] "common"
> 
> $cog_attrs$defLabel
> [1] FALSE
> 
> $cog_attrs$defActive
> [1] TRUE
> 
> $cog_attrs$filterable
> [1] TRUE
> 
> $cog_attrs$log
> [1] NA
> 
> 
> $class
> [1] "cog"     "numeric"

Create Trellis Plot

Dilution Curves

Here is a trellis plot for the dilution curves.

Code
trelliscope_name <- "Dilution Plot"

# Conditional cognostics must always be displayed as a label
trellis_labels <- c("Transition_Name") |> 
  unique()

# If we only have one label to display
# Set it as NULL as it will automatically
# appeared in state
if (length(trellis_labels) == 1) {
    trellis_labels <- NULL
}

dilution_trellis |> 
  trelliscopejs::trelliscope(
    name = trelliscope_name,
    panel_col = "dilution_panel",
    path = "Dilution_Plot_Folder",
    state = list(
      labels = trellis_labels,
      sort = list(trelliscopejs::sort_spec("Transition_Name", dir = "asc"))),
    nrow = 2,
    ncol = 2,
    height = 520,
    width = 1200,
    self_contained = FALSE,
    thumb = FALSE,
    auto_cog = FALSE
  )

Injection Sequence Plots

Here is a trellis plot for the injection sequence plots.

Code
trelliscope_name <- "Injection Sequence Plot"

# Conditional cognostics must always be displayed as a label
trellis_labels <- c("Transition_Name") |> 
  unique()

# If we only have one label to display
# Set it as NULL as it will automatically
# appeared in state
if (length(trellis_labels) == 1) {
    trellis_labels <- NULL
}

injection_sequence_trellis  |> 
  trelliscopejs::trelliscope(
    name = trelliscope_name,
    panel_col = "panel",
    path = "Injection_Sequence_Plot_Folder",
    state = list(
      labels = trellis_labels,
      sort = list(trelliscopejs::sort_spec("Transition_Name", dir = "asc"))),
    nrow = 1,
    ncol = 1,
    height = 520,
    width = 1200,
    self_contained = FALSE,
    thumb = FALSE,
    auto_cog = FALSE
  )

Combined Plots

It is also possible to combined the two trellis plots together as they have the same conditioning cognostics variable. Simply set the path to the same folder name. In the example below, path = "docs" was used.

Unfortunately, the trellis plot does not seems to function properly in the Quarto document. As such, the code below is called interactively via the console.

Code
trelliscope_name <- "Dilution Plot"

# Conditional cognostics must always be displayed as a label
trellis_labels <- c("Transition_Name") |> 
  unique()

# If we only have one label to display
# Set it as NULL as it will automatically
# appeared in state
if (length(trellis_labels) == 1) {
    trellis_labels <- NULL
}

dilution_trellis |> 
  trelliscopejs::trelliscope(
    name = trelliscope_name,
    panel_col = "dilution_panel",
    path = "docs",
    state = list(
      labels = trellis_labels,
      sort = list(trelliscopejs::sort_spec("Transition_Name", dir = "asc"))),
    nrow = 2,
    ncol = 2,
    height = 520,
    width = 1100,
    self_contained = FALSE,
    thumb = FALSE,
    auto_cog = FALSE
  )

trelliscope_name <- "Injection Sequence Plot"

injection_sequence_trellis  |> 
  trelliscopejs::trelliscope(
    name = trelliscope_name,
    panel_col = "panel",
    path = "docs",
    state = list(
      labels = trellis_labels,
      sort = list(trelliscopejs::sort_spec("Transition_Name", dir = "asc"))),
    nrow = 1,
    ncol = 1,
    height = 520,
    width = 1100,
    self_contained = FALSE,
    thumb = FALSE,
    auto_cog = FALSE
  )

A docs folder will be created. It is currently placed in this GitHub repository.

Below is just an embedded webpage of the trellis plot. It can be accessed via this GitHub Page link.

Package References

Code
  • Allaire J, Dervieux C (2024). quarto: R Interface to ‘Quarto’ Markdown Publishing System. R package version 1.4.4, https://quarto-dev.github.io/quarto-r/, https://github.com/quarto-dev/quarto-r.
  • Bachelier V, ZAWAM J, Guillem F (2021). manipulateWidget: Add Even More Interactivity to Interactive Charts. R package version 0.11.1, https://github.com/rte-antares-rpackage/manipulateWidget.
  • Cheng J, Sievert C, Schloerke B, Chang W, Xie Y, Allen J (2024). htmltools: Tools for HTML. R package version 0.5.8.1, https://rstudio.github.io/htmltools/, https://github.com/rstudio/htmltools.
  • Hafen R, Schloerke B (2021). trelliscopejs: Create Interactive Trelliscope Displays. R package version 0.2.6, https://github.com/hafen/trelliscopejs.
  • Hester J, Bryan J (2024). glue: Interpreted String Literals. R package version 1.7.0, https://github.com/tidyverse/glue, https://glue.tidyverse.org/.
  • Iannone R (2023). fontawesome: Easily Work with ‘Font Awesome’ Icons. R package version 0.5.2, https://rstudio.github.io/fontawesome/, https://github.com/rstudio/fontawesome.
  • Kopczynski D, Hoffmann N, Peng B, Ahrends R (2020). “Goslin: A Grammar of Succinct Lipid Nomenclature.” Analytical Chemistry, 92(16), 10957-10960. https://pubs.acs.org/doi/10.1021/acs.analchem.0c01690.
  • Lin G (2023). reactable: Interactive Data Tables for R. R package version 0.4.4, https://github.com/glin/reactable, https://glin.github.io/reactable/.
  • Makowski D, Lüdecke D, Patil I, Thériault R, Ben-Shachar M, Wiernik B (2023). “Automated Results Reporting as a Practical Tool to Improve Reproducibility and Methodological Best Practices Adoption.” CRAN. https://easystats.github.io/report/.
  • Müller K, Wickham H (2023). tibble: Simple Data Frames. R package version 3.2.1, https://github.com/tidyverse/tibble, https://tibble.tidyverse.org/.
  • Nguyen G (2021). assertable: Verbose Assertions for Tabular Data (Data.frames and Data.tables). R package version 0.2.8.
  • R Core Team (2024). R: A Language and Environment for Statistical Computing. R Foundation for Statistical Computing, Vienna, Austria. https://www.R-project.org/.
  • Robinson D, Hayes A, Couch S (2024). broom: Convert Statistical Objects into Tidy Tibbles. R package version 1.0.7, https://github.com/tidymodels/broom, https://broom.tidymodels.org/.
  • Sievert C (2020). Interactive Web-Based Data Visualization with R, plotly, and shiny. Chapman and Hall/CRC. ISBN 9781138331457, https://plotly-r.com.
  • Thompson C (2022). verbaliseR: Make your Text Mighty Fine. R package version 0.1, https://CRAN.R-project.org/package=verbaliseR.
  • Wickham H (2016). ggplot2: Elegant Graphics for Data Analysis. Springer-Verlag New York. ISBN 978-3-319-24277-4, https://ggplot2.tidyverse.org.
  • Wickham H, Chang W, Flight R, Müller K, Hester J (2021). sessioninfo: R Session Information. R package version 1.2.2, https://r-lib.github.io/sessioninfo/, https://github.com/r-lib/sessioninfo#readme.
  • Wickham H, François R, Henry L, Müller K, Vaughan D (2023). dplyr: A Grammar of Data Manipulation. R package version 1.1.4, https://github.com/tidyverse/dplyr, https://dplyr.tidyverse.org.
  • Wickham H, Henry L (2023). purrr: Functional Programming Tools. R package version 1.0.2, https://github.com/tidyverse/purrr, https://purrr.tidyverse.org/.
  • Wickham H, Hester J, Bryan J (2024). readr: Read Rectangular Text Data. R package version 2.1.5, https://CRAN.R-project.org/package=readr.
  • Wickham H, Pedersen T, Seidel D (2023). scales: Scale Functions for Visualization. R package version 1.3.0, https://CRAN.R-project.org/package=scales.
  • Wickham H, Vaughan D, Girlich M (2024). tidyr: Tidy Messy Data. R package version 1.3.1, https://github.com/tidyverse/tidyr, https://tidyr.tidyverse.org.
  • Xie Y (2024). knitr: A General-Purpose Package for Dynamic Report Generation in R. R package version 1.48, https://yihui.org/knitr/. Xie Y (2015). Dynamic Documents with R and knitr, 2nd edition. Chapman and Hall/CRC, Boca Raton, Florida. ISBN 978-1498716963, https://yihui.org/knitr/. Xie Y (2014). “knitr: A Comprehensive Tool for Reproducible Research in R.” In Stodden V, Leisch F, Peng RD (eds.), Implementing Reproducible Computational Research. Chapman and Hall/CRC. ISBN 978-1466561595.

References

1.
Allen M, Poggiali D, Whitaker K, Marshall T, Langen J van, Kievit R. Raincloud plots: A multi-platform tool for robust data visualization [version 2; peer review: 2 approved]. Wellcome Open Research [Internet]. 2021;4(63). Available from: https://wellcomeopenresearch.org/articles/4-63
2.
Wolrab D, Jirásko R, Cífková E, Höring M, Mei D, Chocholoušková M, et al. Lipidomic profiling of human serum enables detection of pancreatic cancer. Nature Communications [Internet]. 2022 Jan 10;13(1):124. Available from: https://doi.org/10.1038/s41467-021-27765-9
3.
Liebisch F Gerhard, Spener F. Update on LIPID MAPS classification, nomenclature, and shorthand notation for MS-derived lipid structures. Journal of Lipid Research [Internet]. 2020 Dec 1;61(12):1539–55. Available from: https://doi.org/10.1194/jlr.S120001025
4.
Kopczynski D, Hoffmann N, Peng B, Ahrends R. Goslin: A grammar of succinct lipid nomenclature. Analytical Chemistry [Internet]. 2020;92(16):10957–60. Available from: https://doi.org/10.1021/acs.analchem.0c01690
5.
Kopczynski D, Hoffmann N, Peng B, Liebisch G, Spener F, Ahrends R. Goslin 2.0 implements the recent lipid shorthand nomenclature for MS-derived lipid structures. Analytical Chemistry [Internet]. 2022;94(16):6097–101. Available from: https://doi.org/10.1021/acs.analchem.1c05430
6.
Broadhurst D, Goodacre R, Reinke SN, Kuligowski J, Wilson ID, Lewis MR, et al. Guidelines and considerations for the use of system suitability and quality control samples in mass spectrometry assays applied in untargeted clinical metabolomic studies. Metabolomics [Internet]. 2018 May 18;14(6):72. Available from: https://doi.org/10.1007/s11306-018-1367-3
7.
Simón-Manso Y, Lowenthal MS, Kilpatrick LE, Sampson ML, Telu KH, Rudnick PA, et al. Metabolite profiling of a NIST standard reference material for human plasma (SRM 1950): GC-MS, LC-MS, NMR, and clinical laboratory analyses, libraries, and web-based resources. Analytical Chemistry [Internet]. 2013 Dec 17;85(24):11725–31. Available from: https://doi.org/10.1021/ac402503m
8.
Sud M, Fahy E, Cotter D, Brown A, Dennis EA, Glass CK, et al. LMSD: LIPID MAPS structure database. Nucleic Acids Research [Internet]. 2006 Nov;35(suppl_1):D527–32. Available from: https://doi.org/10.1093/nar/gkl838
9.
Okabe M, Ito K. Color universal design (CUD)- how to make figures and presentations that are friendly to colorblind people - [Internet]. 2002 [cited 2022 Aug 1]. Available from: https://jfly.uni-koeln.de/color/index.html

Citation

BibTeX citation:
@online{selva2022,
  author = {Selva, Jeremy},
  title = {Quarto {Report} {Example} {With} {Plotly} and
    {Trelliscopejs}},
  date = {2022-08-08},
  url = {https://jauntyjjs.github.io/Trelliscopejs_In_Quarto_Example/},
  langid = {en},
  abstract = {A report showing how to create injection sequence plot and
    dilution plot using R package plotly for each Multiple Reaction
    Monitoring (MRM) transition and display them as an interactive
    trellis plot using R package trelliscopejs.}
}
For attribution, please cite this work as:
Selva J. Quarto Report Example With Plotly and Trelliscopejs [Internet]. 2022. Available from: https://jauntyjjs.github.io/Trelliscopejs_In_Quarto_Example/