├── LICENSE ├── .Rbuildignore ├── man ├── figures │ └── logo.png ├── jip_comp.Rd ├── jip_pca.Rd ├── read_all_induction.Rd ├── set_category_base.Rd ├── find_time_index.Rd ├── jip_test.Rd ├── read_induction.Rd ├── area_cal.Rd └── plot.jip.Rd ├── inst └── extdata │ ├── test │ ├── test_sample1.xlsx │ ├── INDUCTION-18508-20240308-20_38_28.xlsx │ ├── INDUCTION-18510-20240308-20_40_54.xlsx │ ├── INDUCTION-18512-20240308-20_42_37.xlsx │ ├── INDUCTION-18514-20240308-20_44_22.xlsx │ └── INDUCTION-18516-20240308-20_45_40.xlsx │ └── ojip │ ├── INDUCTION-26-20201026-16_07_50.xlsx │ ├── INDUCTION-484-20171225-13_15_58.xlsx │ ├── INDUCTION-485-20171225-14_12_14.xlsx │ ├── INDUCTION-486-20171225-14_33_46.xlsx │ ├── INDUCTION-487-20171225-14_33_46.xlsx │ ├── INDUCTION-18510-20240308-20_40_54.xlsx │ ├── INDUCTION-18512-20240308-20_42_37.xlsx │ ├── INDUCTION-18514-20240308-20_44_22.xlsx │ ├── INDUCTION-2896-20180802-09_27_02.xlsx │ └── INDUCTION-4188-20201116-10_53_39.xlsx ├── README_files └── figure-gfm │ ├── unnamed-chunk-4-1.png │ ├── unnamed-chunk-4-2.png │ ├── unnamed-chunk-5-1.png │ ├── unnamed-chunk-6-1.png │ ├── unnamed-chunk-6-2.png │ └── unnamed-chunk-6-3.png ├── .gitignore ├── jiptest.Rproj ├── tests ├── testthat.R └── testthat │ └── test_basic_function.R ├── DESCRIPTION ├── NAMESPACE ├── LICENSE.md ├── R ├── read_all_induction.R ├── set_category_base.R ├── jip_pca.R ├── utiles.R ├── jip_test.R ├── area_cal.R ├── read_induction.R ├── jip_comp.R └── jiptest-method.R ├── README.Rmd └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2025 2 | COPYRIGHT HOLDER: jiptest authors 3 | -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^jiptest\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^LICENSE\.md$ 4 | -------------------------------------------------------------------------------- /man/figures/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/jiptest/main/man/figures/logo.png -------------------------------------------------------------------------------- /inst/extdata/test/test_sample1.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/jiptest/main/inst/extdata/test/test_sample1.xlsx -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-4-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/jiptest/main/README_files/figure-gfm/unnamed-chunk-4-1.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-4-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/jiptest/main/README_files/figure-gfm/unnamed-chunk-4-2.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-5-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/jiptest/main/README_files/figure-gfm/unnamed-chunk-5-1.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-6-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/jiptest/main/README_files/figure-gfm/unnamed-chunk-6-1.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-6-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/jiptest/main/README_files/figure-gfm/unnamed-chunk-6-2.png -------------------------------------------------------------------------------- /README_files/figure-gfm/unnamed-chunk-6-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/jiptest/main/README_files/figure-gfm/unnamed-chunk-6-3.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .httr-oauth 5 | .DS_Store 6 | .quarto 7 | inst/doc 8 | 9 | /.quarto/ 10 | **/*.quarto_ipynb 11 | -------------------------------------------------------------------------------- /inst/extdata/ojip/INDUCTION-26-20201026-16_07_50.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/jiptest/main/inst/extdata/ojip/INDUCTION-26-20201026-16_07_50.xlsx -------------------------------------------------------------------------------- /inst/extdata/ojip/INDUCTION-484-20171225-13_15_58.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/jiptest/main/inst/extdata/ojip/INDUCTION-484-20171225-13_15_58.xlsx -------------------------------------------------------------------------------- /inst/extdata/ojip/INDUCTION-485-20171225-14_12_14.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/jiptest/main/inst/extdata/ojip/INDUCTION-485-20171225-14_12_14.xlsx -------------------------------------------------------------------------------- /inst/extdata/ojip/INDUCTION-486-20171225-14_33_46.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/jiptest/main/inst/extdata/ojip/INDUCTION-486-20171225-14_33_46.xlsx -------------------------------------------------------------------------------- /inst/extdata/ojip/INDUCTION-487-20171225-14_33_46.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/jiptest/main/inst/extdata/ojip/INDUCTION-487-20171225-14_33_46.xlsx -------------------------------------------------------------------------------- /inst/extdata/ojip/INDUCTION-18510-20240308-20_40_54.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/jiptest/main/inst/extdata/ojip/INDUCTION-18510-20240308-20_40_54.xlsx -------------------------------------------------------------------------------- /inst/extdata/ojip/INDUCTION-18512-20240308-20_42_37.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/jiptest/main/inst/extdata/ojip/INDUCTION-18512-20240308-20_42_37.xlsx -------------------------------------------------------------------------------- /inst/extdata/ojip/INDUCTION-18514-20240308-20_44_22.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/jiptest/main/inst/extdata/ojip/INDUCTION-18514-20240308-20_44_22.xlsx -------------------------------------------------------------------------------- /inst/extdata/ojip/INDUCTION-2896-20180802-09_27_02.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/jiptest/main/inst/extdata/ojip/INDUCTION-2896-20180802-09_27_02.xlsx -------------------------------------------------------------------------------- /inst/extdata/ojip/INDUCTION-4188-20201116-10_53_39.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/jiptest/main/inst/extdata/ojip/INDUCTION-4188-20201116-10_53_39.xlsx -------------------------------------------------------------------------------- /inst/extdata/test/INDUCTION-18508-20240308-20_38_28.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/jiptest/main/inst/extdata/test/INDUCTION-18508-20240308-20_38_28.xlsx -------------------------------------------------------------------------------- /inst/extdata/test/INDUCTION-18510-20240308-20_40_54.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/jiptest/main/inst/extdata/test/INDUCTION-18510-20240308-20_40_54.xlsx -------------------------------------------------------------------------------- /inst/extdata/test/INDUCTION-18512-20240308-20_42_37.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/jiptest/main/inst/extdata/test/INDUCTION-18512-20240308-20_42_37.xlsx -------------------------------------------------------------------------------- /inst/extdata/test/INDUCTION-18514-20240308-20_44_22.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/jiptest/main/inst/extdata/test/INDUCTION-18514-20240308-20_44_22.xlsx -------------------------------------------------------------------------------- /inst/extdata/test/INDUCTION-18516-20240308-20_45_40.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujiedong/jiptest/main/inst/extdata/test/INDUCTION-18516-20240308-20_45_40.xlsx -------------------------------------------------------------------------------- /jiptest.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | Encoding: UTF-8 9 | 10 | AutoAppendNewline: Yes 11 | StripTrailingWhitespace: Yes 12 | LineEndingConversion: Posix 13 | 14 | BuildType: Package 15 | PackageUseDevtools: Yes 16 | PackageInstallArgs: --no-multiarch --with-keep.source 17 | PackageRoxygenize: rd,collate,namespace 18 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | # This file is part of the standard setup for testthat. 2 | # It is recommended that you do not modify it. 3 | # 4 | # Where should you do additional test configuration? 5 | # Learn more about the roles of various files in: 6 | # * https://r-pkgs.org/testing-design.html#sec-tests-files-overview 7 | # * https://testthat.r-lib.org/articles/special-files.html 8 | 9 | library(testthat) 10 | library(jiptest) 11 | 12 | test_check("jiptest") 13 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: jiptest 2 | Title: What the Package Does (One Line, Title Case) 3 | Version: 0.0.0.9000 4 | Authors@R: 5 | person("First", "Last", , "first.last@example.com", role = c("aut", "cre")) 6 | Description: What the package does (one paragraph). 7 | License: MIT + file LICENSE 8 | Encoding: UTF-8 9 | Roxygen: list(markdown = TRUE) 10 | RoxygenNote: 7.3.3 11 | Imports: 12 | data.table, 13 | zoo 14 | Suggests: 15 | knitr, 16 | testthat (>= 3.0.0) 17 | Config/testthat/edition: 3 18 | -------------------------------------------------------------------------------- /man/jip_comp.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/jip_comp.R 3 | \name{jip_comp} 4 | \alias{jip_comp} 5 | \title{JIP-test Calculation} 6 | \usage{ 7 | jip_comp(sample_data, use_PAM = FALSE) 8 | } 9 | \arguments{ 10 | \item{sample_data}{Data frame from read_induction() (contains SOURCE, MILLI_SEC, DC, FLUOR)} 11 | 12 | \item{use_PAM}{Logical: TRUE = use PAM (FLUOR column), FALSE = use DC (default)} 13 | } 14 | \value{ 15 | Data frame with JIP-test parameters 16 | } 17 | \description{ 18 | JIP-test Calculation 19 | } 20 | \keyword{internal} 21 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(plot,jip) 4 | export(area_cal) 5 | export(find_time_index) 6 | export(jip_pca) 7 | export(jip_test) 8 | export(read_all_induction) 9 | export(read_induction) 10 | export(set_category_base) 11 | importFrom(data.table,rbindlist) 12 | importFrom(grDevices,adjustcolor) 13 | importFrom(grDevices,palette.colors) 14 | importFrom(graphics,abline) 15 | importFrom(graphics,axis) 16 | importFrom(graphics,grid) 17 | importFrom(graphics,legend) 18 | importFrom(graphics,par) 19 | importFrom(graphics,points) 20 | importFrom(graphics,text) 21 | importFrom(stats,na.omit) 22 | importFrom(zoo,rollmean) 23 | -------------------------------------------------------------------------------- /man/jip_pca.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/jip_pca.R 3 | \name{jip_pca} 4 | \alias{jip_pca} 5 | \title{Preprocess JIP-Test Parameters for PCA 6 | Returns SOURCE (from jip_test() output) + clean numeric parameters (no NA/Inf)} 7 | \usage{ 8 | jip_pca(jip_data) 9 | } 10 | \arguments{ 11 | \item{jip_data}{Data frame from \code{jip_test()} (contains SOURCE and numeric parameters)} 12 | } 13 | \value{ 14 | Data frame with: 15 | \itemize{ 16 | \item First column: Original SOURCE (from your data files) 17 | \item Remaining columns: Numeric JIP parameters (no NA/Inf) 18 | } 19 | } 20 | \description{ 21 | Preprocess JIP-Test Parameters for PCA 22 | Returns SOURCE (from jip_test() output) + clean numeric parameters (no NA/Inf) 23 | } 24 | -------------------------------------------------------------------------------- /man/read_all_induction.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/read_all_induction.R 3 | \name{read_all_induction} 4 | \alias{read_all_induction} 5 | \title{Batch Read LI-6800 Induction Excel Files} 6 | \usage{ 7 | read_all_induction( 8 | file_dir, 9 | pattern = "\\\\.xlsx$", 10 | recursive = FALSE, 11 | verbose = TRUE 12 | ) 13 | } 14 | \arguments{ 15 | \item{file_dir}{Character: Path to directory containing induction Excel files} 16 | 17 | \item{pattern}{Character: Regular expression to match Excel files (default: "\\.xlsx$")} 18 | 19 | \item{recursive}{Logical: Whether to read files recursively (default: FALSE)} 20 | 21 | \item{verbose}{Logical: Whether to print progress messages (default: TRUE)} 22 | } 23 | \value{ 24 | Tidy data frame with combined induction data (one row per time point per sample) 25 | } 26 | \description{ 27 | Read all LI-6800 induction Excel files in a directory into a tidy data frame. 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2025 jiptest authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /man/set_category_base.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/set_category_base.R 3 | \name{set_category_base} 4 | \alias{set_category_base} 5 | \title{Add a Categorical Column to Data Frame} 6 | \usage{ 7 | set_category_base(df, category, new_col_name = "GROUP") 8 | } 9 | \arguments{ 10 | \item{df}{A data frame containing a \code{SOURCE} column with original identifiers 11 | (e.g., filenames).} 12 | 13 | \item{category}{A character vector of new category labels. The order of 14 | \code{category} must correspond to the order of unique values in \code{df$SOURCE}.} 15 | 16 | \item{new_col_name}{A character string specifying the name of the new column 17 | to be added. Defaults to \code{"GROUP"}.} 18 | } 19 | \value{ 20 | A data frame (\code{df}) with the new categorical column appended. 21 | } 22 | \description{ 23 | This function adds a new column to a data frame, mapping values from the 24 | \code{SOURCE} column to new categories provided by the user. 25 | } 26 | \examples{ 27 | \dontrun{ 28 | # Example data frame 29 | df = data.frame( 30 | SOURCE = rep(c("file1.xlsx", "file2.xlsx", "file3.xlsx"), each = 10), 31 | Value = rnorm(30) 32 | ) 33 | 34 | # Define new categories 35 | my_categories = c("Control", "Treatment_A", "Treatment_B") 36 | 37 | # Add the new category column 38 | df_with_group = set_category_base(df, my_categories) 39 | head(df_with_group) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /man/find_time_index.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utiles.R 3 | \name{find_time_index} 4 | \alias{find_time_index} 5 | \title{Find Index of Time Point Closest to Target} 6 | \usage{ 7 | find_time_index(time_vector, target_time, time_unit = "s") 8 | } 9 | \arguments{ 10 | \item{time_vector}{A numeric vector of time values.} 11 | 12 | \item{target_time}{A numeric scalar representing the target time to find.} 13 | 14 | \item{time_unit}{A character string specifying the unit of \code{time_vector}. 15 | Possible values are "s" (seconds, default), "ms" (milliseconds), or "us" 16 | (microseconds). This determines the conversion factor applied to 17 | \code{time_vector} before comparing to \code{target_time}.} 18 | } 19 | \value{ 20 | An integer scalar representing the index of the closest time point in 21 | \code{time_vector}. If \code{time_vector} is empty, returns \code{NA_integer_}. 22 | } 23 | \description{ 24 | This function takes a vector of time values and finds the index of the element 25 | that is closest to a specified target time. 26 | } 27 | \examples{ 28 | # Time vector in seconds 29 | times_sec = c(0.001, 0.002, 0.003, 0.004) 30 | find_time_index(times_sec, target_time = 2.5, time_unit = "ms") # Should return 3 31 | 32 | # Time vector in milliseconds 33 | times_ms = c(1, 2, 3, 4) 34 | find_time_index(times_ms, target_time = 0.0025, time_unit = "s") # Should return 3 35 | } 36 | -------------------------------------------------------------------------------- /R/read_all_induction.R: -------------------------------------------------------------------------------- 1 | #' Batch Read LI-6800 Induction Excel Files 2 | #' 3 | #' Read all LI-6800 induction Excel files in a directory into a tidy data frame. 4 | #' 5 | #' @param file_dir Character: Path to directory containing induction Excel files 6 | #' @param pattern Character: Regular expression to match Excel files (default: "\\.xlsx$") 7 | #' @param recursive Logical: Whether to read files recursively (default: FALSE) 8 | #' @param verbose Logical: Whether to print progress messages (default: TRUE) 9 | #' @return Tidy data frame with combined induction data (one row per time point per sample) 10 | #' @export 11 | #' 12 | read_all_induction = function( 13 | file_dir, 14 | pattern = "\\.xlsx$", 15 | recursive = FALSE, 16 | verbose = TRUE 17 | ) { 18 | 19 | # list all data files in a folder 20 | file_paths = list.files( 21 | path = file_dir, 22 | pattern = pattern, 23 | full.names = TRUE, 24 | recursive = recursive 25 | ) 26 | 27 | if (length(file_paths) == 0) { 28 | stop(sprintf("No files matching pattern '%s' found in '%s'", pattern, file_dir)) 29 | } 30 | 31 | # read all the data in a folder 32 | all_data = lapply(file_paths, function(file) { 33 | if (verbose) { 34 | message(sprintf("Reading file: %s", basename(file))) 35 | } 36 | # use read induction 37 | df = read_induction(file) 38 | # add a SOURCE colums to distinguish the data source(default file name) 39 | df$SOURCE = tools::file_path_sans_ext(basename(file)) 40 | df 41 | }) 42 | 43 | # combine all the data 44 | combined_data = do.call(rbind, all_data) 45 | rownames(combined_data) = NULL 46 | return(combined_data) 47 | } 48 | 49 | -------------------------------------------------------------------------------- /man/jip_test.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/jip_test.R 3 | \name{jip_test} 4 | \alias{jip_test} 5 | \title{Batch Calculate JIP-test Parameters} 6 | \usage{ 7 | jip_test(df, use_PAM = FALSE, verbose = FALSE) 8 | } 9 | \arguments{ 10 | \item{df}{A data frame (typically the output of \code{read_all_induction}). It 11 | must contain the \code{SOURCE} column and all other columns required by the 12 | \code{jip_comp} function.} 13 | 14 | \item{use_PAM}{A logical value. If \code{TRUE}, the function uses the PAM 15 | fluorescence signal (\code{FLUOR} column) for calculations. If \code{FALSE} (the 16 | default), it uses the continuous DC signal (\code{DC} column).} 17 | 18 | \item{verbose}{A logical value. If \code{TRUE}, the function will print the name 19 | of each sample as it is being processed. Defaults to \code{FALSE}.} 20 | } 21 | \value{ 22 | A data frame (or \code{data.table}) containing the JIP-test parameters 23 | for each sample. Each row represents a sample. 24 | } 25 | \description{ 26 | This function applies the JIP-test analysis to a combined data frame of 27 | induction curves. It splits the data by sample (as indicated by the \code{SOURCE} 28 | column), computes JIP parameters for each sample using \code{jip_comp}, and 29 | combines the results into a single data frame. 30 | } 31 | \examples{ 32 | \dontrun{ 33 | library(jiptest) 34 | 35 | # Step 1: Read all induction files from a directory 36 | combined_data = read_all_induction("path/to/your/data_directory") 37 | 38 | # Step 2: Perform JIP-test analysis on all samples using the DC signal 39 | jip_results = jip_test(combined_data, use_PAM = FALSE) 40 | 41 | # View the results 42 | head(jip_results) 43 | } 44 | 45 | } 46 | \seealso{ 47 | \code{\link{jip_comp}}, \code{\link{read_all_induction}} 48 | } 49 | -------------------------------------------------------------------------------- /R/set_category_base.R: -------------------------------------------------------------------------------- 1 | #' Add a Categorical Column to Data Frame 2 | #' 3 | #' This function adds a new column to a data frame, mapping values from the 4 | #' `SOURCE` column to new categories provided by the user. 5 | #' 6 | #' @param df A data frame containing a `SOURCE` column with original identifiers 7 | #' (e.g., filenames). 8 | #' @param category A character vector of new category labels. The order of 9 | #' `category` must correspond to the order of unique values in `df$SOURCE`. 10 | #' @param new_col_name A character string specifying the name of the new column 11 | #' to be added. Defaults to `"GROUP"`. 12 | #' 13 | #' @return A data frame (`df`) with the new categorical column appended. 14 | #' 15 | #' @export 16 | #' 17 | #' @examples 18 | #' \dontrun{ 19 | #' # Example data frame 20 | #' df = data.frame( 21 | #' SOURCE = rep(c("file1.xlsx", "file2.xlsx", "file3.xlsx"), each = 10), 22 | #' Value = rnorm(30) 23 | #' ) 24 | #' 25 | #' # Define new categories 26 | #' my_categories = c("Control", "Treatment_A", "Treatment_B") 27 | #' 28 | #' # Add the new category column 29 | #' df_with_group = set_category_base(df, my_categories) 30 | #' head(df_with_group) 31 | #' } 32 | set_category_base = function(df, category, new_col_name = "GROUP") { 33 | # Input validation 34 | if (!"SOURCE" %in% colnames(df)) { 35 | stop("The input data frame `df` must contain a column named 'SOURCE'.") 36 | } 37 | 38 | source_unique = unique(df$SOURCE) 39 | 40 | if (length(source_unique) != length(category)) { 41 | stop(paste("The length of `category` (", length(category), 42 | ") must match the number of unique values in `df$SOURCE` (", length(source_unique), ").", sep = "")) 43 | } 44 | 45 | if (!is.character(category)) { 46 | stop("`category` must be a character vector.") 47 | } 48 | 49 | # Create the new column using match() 50 | df[[new_col_name]] = category[match(df$SOURCE, source_unique)] 51 | 52 | return(df) 53 | } 54 | 55 | -------------------------------------------------------------------------------- /R/jip_pca.R: -------------------------------------------------------------------------------- 1 | #' Preprocess JIP-Test Parameters for PCA 2 | #' Returns SOURCE (from jip_test() output) + clean numeric parameters (no NA/Inf) 3 | #' 4 | #' @param jip_data Data frame from \code{jip_test()} (contains SOURCE and numeric parameters) 5 | #' @return Data frame with: 6 | #' - First column: Original SOURCE (from your data files) 7 | #' - Remaining columns: Numeric JIP parameters (no NA/Inf) 8 | #' @export 9 | jip_pca = function(jip_data) { 10 | # Force base R data frame (avoid data.table conflicts) 11 | if (inherits(jip_data, "data.table")) { 12 | jip_data = as.data.frame(jip_data) 13 | } 14 | 15 | # 1. Validate SOURCE column exists (from jip_test() output) 16 | if (!"SOURCE" %in% colnames(jip_data)) { 17 | stop("jip_data must contain a 'SOURCE' column (from jip_test() output)") 18 | } 19 | 20 | # 2. Separate SOURCE (original file names) and numeric parameters 21 | source_col = jip_data[, "SOURCE", drop = FALSE] # Preserve original SOURCE 22 | numeric_cols = sapply(jip_data, is.numeric) # Identify numeric parameters 23 | numeric_data = jip_data[, numeric_cols, drop = FALSE] 24 | 25 | # 3. Clean numeric data (remove rows with NA/Inf) 26 | # Remove rows with NA in numeric parameters 27 | valid_rows = complete.cases(numeric_data) 28 | if (sum(!valid_rows) > 0) { 29 | message(sprintf("Removing %d rows with missing numeric values", sum(!valid_rows))) 30 | } 31 | numeric_data_clean = numeric_data[valid_rows, , drop = FALSE] 32 | source_clean = source_col[valid_rows, , drop = FALSE] 33 | 34 | # Remove columns with infinite values (if any) 35 | inf_cols = sapply(numeric_data_clean, function(col) any(is.infinite(col))) 36 | if (sum(inf_cols) > 0) { 37 | message(sprintf("Removing %d columns with infinite values", sum(inf_cols))) 38 | numeric_data_clean = numeric_data_clean[, !inf_cols, drop = FALSE] 39 | } 40 | 41 | # 4. Final validation (enough samples/variables for PCA) 42 | if (nrow(numeric_data_clean) < 2) stop("Not enough samples left (need ≥2)") 43 | if (ncol(numeric_data_clean) < 2) stop("Not enough numeric parameters (need ≥2)") 44 | 45 | # 5. Combine SOURCE + clean numeric data 46 | cbind(source_clean, numeric_data_clean) 47 | } 48 | 49 | -------------------------------------------------------------------------------- /R/utiles.R: -------------------------------------------------------------------------------- 1 | #' Find Index of Time Point Closest to Target 2 | #' 3 | #' This function takes a vector of time values and finds the index of the element 4 | #' that is closest to a specified target time. 5 | #' 6 | #' @param time_vector A numeric vector of time values. 7 | #' @param target_time A numeric scalar representing the target time to find. 8 | #' @param time_unit A character string specifying the unit of `time_vector`. 9 | #' Possible values are "s" (seconds, default), "ms" (milliseconds), or "us" 10 | #' (microseconds). This determines the conversion factor applied to 11 | #' `time_vector` before comparing to `target_time`. 12 | #' 13 | #' @return An integer scalar representing the index of the closest time point in 14 | #' `time_vector`. If `time_vector` is empty, returns `NA_integer_`. 15 | #' 16 | #' @export 17 | #' 18 | #' @examples 19 | #' # Time vector in seconds 20 | #' times_sec = c(0.001, 0.002, 0.003, 0.004) 21 | #' find_time_index(times_sec, target_time = 2.5, time_unit = "ms") # Should return 3 22 | #' 23 | #' # Time vector in milliseconds 24 | #' times_ms = c(1, 2, 3, 4) 25 | #' find_time_index(times_ms, target_time = 0.0025, time_unit = "s") # Should return 3 26 | find_time_index = function(time_vector, target_time, time_unit = "s") { 27 | # Input validation 28 | if (!is.numeric(time_vector)) { 29 | stop("`time_vector` must be a numeric vector.") 30 | } 31 | if (!is.numeric(target_time) || length(target_time) != 1) { 32 | stop("`target_time` must be a single numeric value.") 33 | } 34 | if (length(time_vector) == 0) { 35 | warning("`time_vector` is empty. Returning NA.") 36 | return(NA_integer_) 37 | } 38 | 39 | # Define conversion factor from time_vector's unit to target_time's unit 40 | conversion_factor = switch(time_unit, 41 | "s" = 1, 42 | "ms" = 1e-3, 43 | "us" = 1e-6, 44 | stop("Unsupported `time_unit`. Use 's', 'ms', or 'us'.")) 45 | 46 | # Convert time_vector to the same unit as target_time and find the closest index 47 | converted_time = time_vector * conversion_factor 48 | closest_index = which.min(abs(converted_time - target_time)) 49 | 50 | return(closest_index) 51 | } 52 | 53 | -------------------------------------------------------------------------------- /man/read_induction.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/read_induction.R 3 | \name{read_induction} 4 | \alias{read_induction} 5 | \title{Read and Preprocess LI-6800 Induction Curve Data} 6 | \usage{ 7 | read_induction(path, code_filter = 3:6) 8 | } 9 | \arguments{ 10 | \item{path}{A character string. The full path or URL to the LI-6800 INDUCTION 11 | Excel file.} 12 | 13 | \item{code_filter}{A numeric vector. The values in the \code{CODE} column that 14 | should be retained. Defaults to \code{3:6}, which are typically the codes 15 | corresponding to the light-induced fluorescence transient.} 16 | } 17 | \value{ 18 | A data frame of class \code{jip} containing the processed data. The 19 | data frame includes the following columns: 20 | \itemize{ 21 | \item \code{EVENT_ID}, \code{TRACE_NO}, \code{SECS}, \code{FLUOR}, \code{DC}, \code{PFD}, \code{REDMODAVG}, \code{CODE}: Original columns from the Excel file. 22 | \item \code{SOURCE}: The filename (without extension) from which the data was read. 23 | \item \code{NORM_FLUOR}: The \code{FLUOR} values normalized to the range 0-1 24 | \item \code{NORM_DC}: The \code{DC} values normalized to the range 0-1 25 | \item \code{MILLI_SEC}: The time in milliseconds, converted from \code{SECS}. 26 | } 27 | } 28 | \description{ 29 | This function reads an Excel file containing induction curve data from a 30 | LI-6800 instrument, filters for relevant data points, performs 31 | standardization, converts time units, and assigns a custom class \code{jip} to 32 | the resulting data frame for easy plotting and analysis. 33 | } 34 | \details{ 35 | The function executes the following steps: 36 | \enumerate{ 37 | \item \strong{Read File}: Uses \code{openxlsx2::read_xlsx} to read the Excel file. 38 | \item \strong{Add Source}: Creates a \code{SOURCE} column with the filename (without extension). 39 | \item \strong{Validate Columns}: Checks for the presence of essential columns (\code{CODE}, \code{SECS}, \code{FLUOR}, \code{DC}). 40 | \item \strong{Filter Rows}: Retains only rows where \code{CODE} is in \code{code_filter}. Issues a warning if no rows match. 41 | \item \strong{Normalize Signals}: Computes \code{NORM_FLUOR} and \code{NORM_DC} by scaling the signals to 0-1. 42 | \item \strong{Convert Time}: Converts time from seconds (\code{SECS}) to milliseconds (\code{MILLI_SEC}). 43 | \item \strong{Set Class}: Assigns the \code{jip} class to the data frame to enable specialized methods (e.g., \code{plot.jip}). 44 | } 45 | } 46 | \examples{ 47 | \dontrun{ 48 | library(jiptest) 49 | # Read a single induction curve file 50 | jip_data = read_induction("path/to/your/induction_file.xlsx") 51 | 52 | # Check the structure 53 | str(jip_data) 54 | 55 | # Since it has class 'jip', a custom plot function can be used 56 | plot(jip_data) 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /man/area_cal.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/area_cal.R 3 | \name{area_cal} 4 | \alias{area_cal} 5 | \title{Calculate the Area Under the Fluorescence Rise Curve} 6 | \usage{ 7 | area_cal(df, use_PAM = TRUE) 8 | } 9 | \arguments{ 10 | \item{df}{A data frame. It must contain the columns: \code{MILLI_SEC} (time in 11 | milliseconds), \code{FLUOR} (PAM fluorescence signal), and \code{DC} (continuous 12 | fluorescence signal).} 13 | 14 | \item{use_PAM}{A logical value. If \code{TRUE} (default), the function uses the 15 | \code{FLUOR} column for calculations. If \code{FALSE}, it uses the \code{DC} column.} 16 | } 17 | \value{ 18 | A numeric scalar representing the calculated area. Specifically, it 19 | returns the value of \code{a_total - auc}, where \code{a_total} is the area of the 20 | rectangle and \code{auc} is the area under the curve from the start to the peak. 21 | } 22 | \description{ 23 | This function computes a specific area related to the fluorescence rise curve 24 | (from the OJIP test) using a trapezoidal integration method. It is designed 25 | for high-frequency data (e.g., 250Hz) and calculates the area between the 26 | actual curve and a rectangle defined by the minimum and maximum values of the 27 | signal and time (on a log scale). 28 | } 29 | \details{ 30 | The calculation proceeds as follows: 31 | \enumerate{ 32 | \item The time column (\code{MILLI_SEC}) is converted to a natural logarithm scale 33 | (\code{logs}). 34 | \item The function identifies the index of the peak value in the selected 35 | fluorescence signal column (\code{FLUOR} or \code{DC}). 36 | \item The data frame is truncated to include only the data points from the start 37 | up to and including the peak. 38 | \item The area under the truncated curve (\code{auc}) is computed using trapezoidal 39 | integration on the log-time scale. 40 | \item The area of a rectangle (\code{a_total}) is computed, with height equal to the 41 | difference between the maximum and minimum signal values, and width equal 42 | to the difference between the maximum and minimum log-time values. 43 | \item The result is the difference between \code{a_total} and \code{auc}. 44 | } 45 | } 46 | \section{Warning}{ 47 | 48 | Ensure that your data frame \code{df} is sorted by time (\code{MILLI_SEC}) in ascending 49 | order. The function does not perform sorting and incorrect order will lead to 50 | invalid integration results. 51 | } 52 | 53 | \examples{ 54 | \dontrun{ 55 | library(jiptest) 56 | # Load your data (ensure columns are named MILLI_SEC, FLUOR, DC) 57 | data = load_your_data_function("your_data_file.xlsx") 58 | 59 | # Calculate area using the PAM signal (FLUOR column) 60 | area_pam = area_cal(data, use_PAM = TRUE) 61 | 62 | # Calculate area using the DC signal (DC column) 63 | area_dc = area_cal(data, use_PAM = FALSE) 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /R/jip_test.R: -------------------------------------------------------------------------------- 1 | #' Batch Calculate JIP-test Parameters 2 | #' 3 | #' This function applies the JIP-test analysis to a combined data frame of 4 | #' induction curves. It splits the data by sample (as indicated by the `SOURCE` 5 | #' column), computes JIP parameters for each sample using `jip_comp`, and 6 | #' combines the results into a single data frame. 7 | #' 8 | #' @param df A data frame (typically the output of `read_all_induction`). It 9 | #' must contain the `SOURCE` column and all other columns required by the 10 | #' `jip_comp` function. 11 | #' @param use_PAM A logical value. If `TRUE`, the function uses the PAM 12 | #' fluorescence signal (`FLUOR` column) for calculations. If `FALSE` (the 13 | #' default), it uses the continuous DC signal (`DC` column). 14 | #' @param verbose A logical value. If `TRUE`, the function will print the name 15 | #' of each sample as it is being processed. Defaults to `FALSE`. 16 | #' 17 | #' @return A data frame (or `data.table`) containing the JIP-test parameters 18 | #' for each sample. Each row represents a sample. 19 | #' 20 | #' @seealso \code{\link{jip_comp}}, \code{\link{read_all_induction}} 21 | #' 22 | #' @examples 23 | #' \dontrun{ 24 | #' library(jiptest) 25 | #' 26 | #' # Step 1: Read all induction files from a directory 27 | #' combined_data = read_all_induction("path/to/your/data_directory") 28 | #' 29 | #' # Step 2: Perform JIP-test analysis on all samples using the DC signal 30 | #' jip_results = jip_test(combined_data, use_PAM = FALSE) 31 | #' 32 | #' # View the results 33 | #' head(jip_results) 34 | #' } 35 | #' 36 | #' @importFrom data.table rbindlist 37 | #' @export 38 | 39 | jip_test = function(df, use_PAM = FALSE, verbose = FALSE) { 40 | 41 | # Input validation 42 | if (!is.data.frame(df)) { 43 | stop("The first argument `df` must be a data frame. Did you intend to use `read_all_induction()` first?") 44 | } 45 | 46 | if (!"SOURCE" %in% colnames(df)) { 47 | stop("The input data frame `df` must contain a 'SOURCE' column.") 48 | } 49 | 50 | # Split data by the 'SOURCE' column 51 | split_data = split(df, df$SOURCE) 52 | 53 | if (length(split_data) == 0) { 54 | warning("No data to process. The 'SOURCE' column may be empty.") 55 | return(data.frame()) 56 | } 57 | 58 | # Process each sample with jip_comp 59 | list_df = lapply(names(split_data), function(source_name) { 60 | if (verbose) { 61 | message("Processing sample: ", source_name) 62 | } 63 | # Extract data for the current sample 64 | sample_data = split_data[[source_name]] 65 | # Call jip_comp with the appropriate use_PAM value 66 | jip_comp(sample_data, use_PAM = use_PAM) 67 | }) 68 | 69 | # Combine all results into a single data frame 70 | if (length(list_df) > 0) { 71 | ojip_data = data.table::rbindlist(list_df) 72 | # Convert back to a regular data frame if preferred (optional) 73 | # ojip_data = as.data.frame(ojip_data) 74 | } else { 75 | ojip_data = data.frame() 76 | } 77 | 78 | return(ojip_data) 79 | } 80 | 81 | -------------------------------------------------------------------------------- /tests/testthat/test_basic_function.R: -------------------------------------------------------------------------------- 1 | # 加载包和测试工具 2 | library(testthat) 3 | library(jiptest) 4 | 5 | # 全局测试配置:测试目录(和你实际使用的一致) 6 | test_dir <- system.file("extdata/test/", package = "jiptest") 7 | 8 | # 前置检查:如果测试目录不存在/无有效文件,直接跳过所有测试(避免报错) 9 | all_xlsx_files <- if (dir.exists(test_dir)) { 10 | list.files(test_dir, pattern = "\\.xlsx$", full.names = TRUE) 11 | } else { 12 | character(0) 13 | } 14 | valid_files <- all_xlsx_files[!grepl("test_invalid", all_xlsx_files)] # 排除无效文件(如果有) 15 | if (length(valid_files) == 0) { 16 | skip("测试目录无有效 .xlsx 文件,跳过所有核心测试") 17 | } 18 | 19 | # ------------------------------ 20 | # 1. 测试数据读取:只验证“不报错、非空” 21 | # ------------------------------ 22 | test_that("数据读取:能批量读取文件,不报错、非空", { 23 | result <- read_all_induction(test_dir, pattern = "\\.xlsx$", verbose = FALSE) 24 | expect_no_error(result) 25 | expect_true(nrow(result) > 0) 26 | }) 27 | 28 | # ------------------------------ 29 | # 2. 测试JIP计算:只验证“不报错、计算结果非空” 30 | # ------------------------------ 31 | test_that("JIP计算:能批量计算参数,不报错、结果非空", { 32 | all_data <- read_all_induction(test_dir, pattern = "\\.xlsx$", verbose = FALSE) 33 | params_dc <- jip_test(all_data, use_PAM = FALSE, verbose = FALSE) 34 | params_pam <- jip_test(all_data, use_PAM = TRUE, verbose = FALSE) 35 | 36 | expect_no_error(params_dc) 37 | expect_no_error(params_pam) 38 | expect_true(nrow(params_dc) > 0) 39 | expect_true(nrow(params_pam) > 0) 40 | expect_true(length(setdiff(colnames(params_dc), "SOURCE")) > 0) 41 | }) 42 | 43 | # ------------------------------ 44 | # 3. 测试作图:删除 verbose = FALSE 参数(核心修改!) 45 | # ------------------------------ 46 | test_that("作图:能正常绘图,不报错", { 47 | # 第一步:读取数据(取前2个样本) 48 | all_data <- read_all_induction(test_dir, pattern = "\\.xlsx$", verbose = FALSE) 49 | sample_names <- unique(all_data$SOURCE)[1:min(2, length(unique(all_data$SOURCE)))] 50 | plot_data <- all_data[all_data$SOURCE %in% sample_names, ] 51 | 52 | # 关键修改:删掉 verbose = FALSE,plot() 函数不需要这个参数 53 | expect_no_error({ 54 | png(tempfile()) # 打开临时PNG设备 55 | plot(plot_data, use_PAM = FALSE) # 删掉 verbose = FALSE 56 | dev.off() # 关闭设备 57 | }) 58 | 59 | expect_no_error({ 60 | png(tempfile()) # 打开临时PNG设备 61 | plot(plot_data, use_PAM = TRUE) # 删掉 verbose = FALSE 62 | dev.off() # 关闭设备 63 | }) 64 | }) 65 | 66 | # ------------------------------ 67 | # 4. 测试PCA预处理:只验证“样本够就不报错、输出非空” 68 | # ------------------------------ 69 | test_that("PCA预处理:样本数足够时能正常运行,不报错", { 70 | all_data <- read_all_induction(test_dir, pattern = "\\.xlsx$", verbose = FALSE) 71 | params <- jip_test(all_data, use_PAM = FALSE, verbose = FALSE) 72 | 73 | if (nrow(params) >= 2) { 74 | pca_matrix <- jip_pca(params) 75 | expect_no_error(pca_matrix) 76 | expect_true(nrow(pca_matrix) > 0) 77 | } else { 78 | skip("样本数不足2个,跳过PCA测试") 79 | } 80 | }) 81 | 82 | # ------------------------------ 83 | # 5. 异常处理:匹配实际报错信息(文件不存在) 84 | # ------------------------------ 85 | test_that("异常处理:读取不存在的文件时报错友好", { 86 | expect_error( 87 | read_induction("不存在的文件.xlsx"), 88 | regexp = "does not exist|不存在", # 兼容中英文报错 89 | fixed = FALSE 90 | ) 91 | }) 92 | 93 | -------------------------------------------------------------------------------- /R/area_cal.R: -------------------------------------------------------------------------------- 1 | #' Calculate the Area Under the Fluorescence Rise Curve 2 | #' 3 | #' This function computes a specific area related to the fluorescence rise curve 4 | #' (from the OJIP test) using a trapezoidal integration method. It is designed 5 | #' for high-frequency data (e.g., 250Hz) and calculates the area between the 6 | #' actual curve and a rectangle defined by the minimum and maximum values of the 7 | #' signal and time (on a log scale). 8 | #' 9 | #' @param df A data frame. It must contain the columns: `MILLI_SEC` (time in 10 | #' milliseconds), `FLUOR` (PAM fluorescence signal), and `DC` (continuous 11 | #' fluorescence signal). 12 | #' @param use_PAM A logical value. If `TRUE` (default), the function uses the 13 | #' `FLUOR` column for calculations. If `FALSE`, it uses the `DC` column. 14 | #' 15 | #' @return A numeric scalar representing the calculated area. Specifically, it 16 | #' returns the value of `a_total - auc`, where `a_total` is the area of the 17 | #' rectangle and `auc` is the area under the curve from the start to the peak. 18 | #' 19 | #' @details 20 | #' The calculation proceeds as follows: 21 | #' 1. The time column (`MILLI_SEC`) is converted to a natural logarithm scale 22 | #' (`logs`). 23 | #' 2. The function identifies the index of the peak value in the selected 24 | #' fluorescence signal column (`FLUOR` or `DC`). 25 | #' 3. The data frame is truncated to include only the data points from the start 26 | #' up to and including the peak. 27 | #' 4. The area under the truncated curve (`auc`) is computed using trapezoidal 28 | #' integration on the log-time scale. 29 | #' 5. The area of a rectangle (`a_total`) is computed, with height equal to the 30 | #' difference between the maximum and minimum signal values, and width equal 31 | #' to the difference between the maximum and minimum log-time values. 32 | #' 6. The result is the difference between `a_total` and `auc`. 33 | #' 34 | #' @section Warning: 35 | #' Ensure that your data frame `df` is sorted by time (`MILLI_SEC`) in ascending 36 | #' order. The function does not perform sorting and incorrect order will lead to 37 | #' invalid integration results. 38 | #' 39 | #' @examples 40 | #' \dontrun{ 41 | #' library(jiptest) 42 | #' # Load your data (ensure columns are named MILLI_SEC, FLUOR, DC) 43 | #' data = load_your_data_function("your_data_file.xlsx") 44 | #' 45 | #' # Calculate area using the PAM signal (FLUOR column) 46 | #' area_pam = area_cal(data, use_PAM = TRUE) 47 | #' 48 | #' # Calculate area using the DC signal (DC column) 49 | #' area_dc = area_cal(data, use_PAM = FALSE) 50 | #' } 51 | #' 52 | #' @export 53 | #' @importFrom zoo rollmean 54 | 55 | area_cal = function(df, use_PAM = TRUE) { 56 | # Input validation 57 | required_cols = c("MILLI_SEC", "FLUOR", "DC") 58 | if (!all(required_cols %in% colnames(df))) { 59 | missing_cols = setdiff(required_cols, colnames(df)) 60 | stop(paste("The input data frame is missing required columns:", 61 | paste(missing_cols, collapse = ", "))) 62 | } 63 | 64 | if (!is.logical(use_PAM) || length(use_PAM) != 1) { 65 | stop("`use_PAM` must be a single logical value (TRUE or FALSE).") 66 | } 67 | 68 | # Select the appropriate signal column and calculate its peak index 69 | if (use_PAM) { 70 | signal_col = "FLUOR" 71 | } else { 72 | signal_col = "DC" 73 | } 74 | 75 | signal_data = df[[signal_col]] 76 | peak_index = which.max(signal_data) 77 | 78 | if (peak_index == 1) { 79 | warning("The peak of the ", signal_col, " signal is at the first data point. ", 80 | "The calculated area may not be meaningful.") 81 | } 82 | 83 | # Truncate data to the region from start to peak 84 | df_truncated = df[1:peak_index, , drop = FALSE] 85 | 86 | # Calculate log(time) 87 | df_truncated$logs = log(df_truncated$MILLI_SEC) 88 | 89 | # Calculate AUC using trapezoidal integration 90 | auc = with(df_truncated, sum(diff(logs) * zoo::rollmean(get(signal_col), 2))) 91 | 92 | # Calculate the total rectangular area 93 | max_signal = max(df_truncated[[signal_col]]) 94 | min_signal = min(df_truncated[[signal_col]]) 95 | max_log_time = max(df_truncated$logs) 96 | min_log_time = min(df_truncated$logs) 97 | 98 | a_total = (max_signal - min_signal) * (max_log_time - min_log_time) 99 | 100 | # Return the desired area 101 | return(a_total - auc) 102 | } 103 | 104 | -------------------------------------------------------------------------------- /man/plot.jip.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/jiptest-method.R 3 | \name{plot.jip} 4 | \alias{plot.jip} 5 | \title{Plot JIP test data measured by LI-6800} 6 | \usage{ 7 | \method{plot}{jip}( 8 | df, 9 | def_pch = 19, 10 | alpha = 0.6, 11 | leg_bty = "n", 12 | leg_cex = 0.6, 13 | leg_point_cex = 0.9, 14 | legend_pos = "topleft", 15 | xlim = NULL, 16 | ylim = c(0, 1.1), 17 | xlab = "Time (ms)", 18 | ylab = "Normalized fluorescence signal", 19 | col = NULL, 20 | log = "x", 21 | xmark = c(expression(10^{ 22 | -3 23 | }), expression(10^{ 24 | -2 25 | }), expression(10^{ 26 | 27 | -1 28 | }), expression(10^{ 29 | 0 30 | }), expression(10^{ 31 | 1 32 | }), expression(10^2), 33 | expression(10^3)), 34 | xat = c(0.001, 0.01, 0.1, 1, 10, 100, 1000), 35 | use_PAM = FALSE, 36 | add_leg = TRUE, 37 | add_grid = TRUE, 38 | grid_lty = 3, 39 | grid_col = "gray80", 40 | main = NULL, 41 | main_cex = 1.2, 42 | axis_cex = 0.9, 43 | labs_cex = 1, 44 | line_width = 0.5, 45 | show_labels = FALSE, 46 | label_cex = 0.7, 47 | label_offset = 0.02, 48 | ... 49 | ) 50 | } 51 | \arguments{ 52 | \item{df}{Data frame: files read by \code{read_induction} or \code{read_all_induction}} 53 | 54 | \item{def_pch}{Numeric/vector: Point character(s) for plotting (default: 19)} 55 | 56 | \item{alpha}{Numeric: Transparency degree of colors (0-1, default: 0.6)} 57 | 58 | \item{leg_bty}{Character: Box type for legend ("n" = no box, "o" = box, default: "n")} 59 | 60 | \item{leg_cex}{Numeric: Text size of legend (default: 0.6)} 61 | 62 | \item{leg_point_cex}{Numeric: Point size in legend (default: 0.9)} 63 | 64 | \item{legend_pos}{Character/numeric: Legend position (default: "topleft"). 65 | Can be "topleft", "topright", "bottomleft", "bottomright", "top", "bottom", 66 | "left", "right", "center" or a numeric vector of length 2 (x,y coordinates)} 67 | 68 | \item{xlim}{Numeric vector: Limits for the X axis (default: NULL, auto-calculated)} 69 | 70 | \item{ylim}{Numeric vector: Limits for the Y axis (default: c(0, 1.1) for better visibility)} 71 | 72 | \item{xlab}{Character: X axis label (default: "Time (ms)")} 73 | 74 | \item{ylab}{Character: Y axis label (default: "Normalized fluorescence signal")} 75 | 76 | \item{col}{Character/vector: Custom colors for groups (default: NULL, uses palette.colors)} 77 | 78 | \item{log}{Character: Logarithmic axis ("x" for X-axis only, default: "x"; 79 | use "" for no log axis)} 80 | 81 | \item{xmark}{Expression/numeric: X tick mark labels (default: scientific notation)} 82 | 83 | \item{xat}{Numeric: Positions for X axis tick marks (default: c(0.001, 0.01, 0.1, 1, 10, 100, 1000))} 84 | 85 | \item{use_PAM}{Logical: Whether to use PAM signal (NORM_FLUOR) instead of DC signal (NORM_DC) 86 | (default: FALSE)} 87 | 88 | \item{add_leg}{Logical: Whether to add legend (default: TRUE)} 89 | 90 | \item{add_grid}{Logical: Whether to add grid lines (default: TRUE)} 91 | 92 | \item{grid_lty}{Numeric: Grid line type (default: 3, dashed)} 93 | 94 | \item{grid_col}{Character: Grid line color (default: "gray80")} 95 | 96 | \item{main}{Character: Plot title (default: NULL)} 97 | 98 | \item{main_cex}{Numeric: Title size (default: 1.2)} 99 | 100 | \item{axis_cex}{Numeric: Axis text size (default: 0.9)} 101 | 102 | \item{labs_cex}{Numeric: Axis label size (default: 1.0)} 103 | 104 | \item{line_width}{Numeric: Line width for points (default: 0.5)} 105 | 106 | \item{show_labels}{Logical: Whether to show sample labels next to curves (default: FALSE)} 107 | 108 | \item{label_cex}{Numeric: Size of sample labels (default: 0.7)} 109 | 110 | \item{label_offset}{Numeric: Offset for sample labels (default: 0.02)} 111 | 112 | \item{...}{Additional parameters passed to \code{plot} and \code{legend}} 113 | } 114 | \description{ 115 | A enhanced function to visualize JIP test data with improved 116 | customization, better default settings, and clearer data presentation. 117 | Graphical preview of all data curves in log axis (BLUE > 1.2.2) with 118 | normalized signals. 119 | } 120 | \examples{ 121 | \dontrun{ 122 | # Basic plot 123 | plot.jip(df) 124 | 125 | # With custom colors and title 126 | plot.jip(df, col = c("red", "blue", "green"), main = "JIP Test Results") 127 | 128 | # Use PAM signal, show sample labels, change legend position 129 | plot.jip(df, use_PAM = TRUE, show_labels = TRUE, legend_pos = "bottomright") 130 | 131 | # Custom grid and axis settings 132 | plot.jip(df, grid_lty = 1, grid_col = "gray50", axis_cex = 1.0) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /R/read_induction.R: -------------------------------------------------------------------------------- 1 | #' Read and Preprocess LI-6800 Induction Curve Data 2 | #' 3 | #' This function reads an Excel file containing induction curve data from a 4 | #' LI-6800 instrument, filters for relevant data points, performs 5 | #' standardization, converts time units, and assigns a custom class `jip` to 6 | #' the resulting data frame for easy plotting and analysis. 7 | #' 8 | #' @param path A character string. The full path or URL to the LI-6800 INDUCTION 9 | #' Excel file. 10 | #' @param code_filter A numeric vector. The values in the `CODE` column that 11 | #' should be retained. Defaults to `3:6`, which are typically the codes 12 | #' corresponding to the light-induced fluorescence transient. 13 | #' 14 | #' @return A data frame of class `jip` containing the processed data. The 15 | #' data frame includes the following columns: 16 | #' \itemize{ 17 | #' \item `EVENT_ID`, `TRACE_NO`, `SECS`, `FLUOR`, `DC`, `PFD`, `REDMODAVG`, `CODE`: Original columns from the Excel file. 18 | #' \item `SOURCE`: The filename (without extension) from which the data was read. 19 | #' \item `NORM_FLUOR`: The `FLUOR` values normalized to the range 0-1 20 | #' \item `NORM_DC`: The `DC` values normalized to the range 0-1 21 | #' \item `MILLI_SEC`: The time in milliseconds, converted from `SECS`. 22 | #' } 23 | #' 24 | #' @details 25 | #' The function executes the following steps: 26 | #' 1. **Read File**: Uses `openxlsx2::read_xlsx` to read the Excel file. 27 | #' 2. **Add Source**: Creates a `SOURCE` column with the filename (without extension). 28 | #' 3. **Validate Columns**: Checks for the presence of essential columns (`CODE`, `SECS`, `FLUOR`, `DC`). 29 | #' 4. **Filter Rows**: Retains only rows where `CODE` is in `code_filter`. Issues a warning if no rows match. 30 | #' 5. **Normalize Signals**: Computes `NORM_FLUOR` and `NORM_DC` by scaling the signals to 0-1. 31 | #' 6. **Convert Time**: Converts time from seconds (`SECS`) to milliseconds (`MILLI_SEC`). 32 | #' 7. **Set Class**: Assigns the `jip` class to the data frame to enable specialized methods (e.g., `plot.jip`). 33 | #' 34 | #' @examples 35 | #' \dontrun{ 36 | #' library(jiptest) 37 | #' # Read a single induction curve file 38 | #' jip_data = read_induction("path/to/your/induction_file.xlsx") 39 | #' 40 | #' # Check the structure 41 | #' str(jip_data) 42 | #' 43 | #' # Since it has class 'jip', a custom plot function can be used 44 | #' plot(jip_data) 45 | #' } 46 | #' 47 | #' @export 48 | 49 | read_induction = function(path, code_filter = 3:6) { 50 | # Check if the file exists 51 | if (!file.exists(path)) { 52 | stop("The file '", path, "' does not exist.") 53 | } 54 | 55 | # Read the Excel file 56 | df = openxlsx2::read_xlsx(path) 57 | 58 | # Add SOURCE column (filename without extension) 59 | df$SOURCE = tools::file_path_sans_ext(basename(path)) 60 | 61 | # Define required columns 62 | required_cols = c("CODE", "SECS", "FLUOR", "DC") 63 | if (!all(required_cols %in% colnames(df))) { 64 | missing_cols = setdiff(required_cols, colnames(df)) 65 | stop("The file is missing required columns: ", paste(missing_cols, collapse = ", ")) 66 | } 67 | 68 | # Filter rows based on CODE 69 | df_filtered = df[df$CODE %in% code_filter, , drop = FALSE] 70 | 71 | if (nrow(df_filtered) == 0) { 72 | warning("No rows remaining after filtering with code_filter = c(", 73 | paste(code_filter, collapse = ", "), "). Returning empty data frame.") 74 | # Assign class and return to maintain consistency 75 | class(df_filtered) = c("jip", class(df_filtered)) 76 | return(df_filtered) 77 | } 78 | 79 | # Select and reorder columns (optional but makes output consistent) 80 | keep_columns = c('EVENT_ID', 'TRACE_NO', 'SECS', 'FLUOR', 'DC', 'PFD', 'REDMODAVG', 'CODE', 'SOURCE') 81 | # Only keep columns that exist in df_filtered 82 | keep_columns = intersect(keep_columns, colnames(df_filtered)) 83 | df_processed = df_filtered[, keep_columns] 84 | 85 | # Normalize FLUOR and DC signals, with check for division by zero 86 | range_fluor = range(df_processed$FLUOR, na.rm = TRUE) 87 | df_processed$NORM_FLUOR = if (diff(range_fluor) == 0) { 88 | warning("All FLUOR values are identical. NORM_FLUOR set to 0.") 89 | 0 90 | } else { 91 | (df_processed$FLUOR - range_fluor[1]) / diff(range_fluor) 92 | } 93 | 94 | range_dc = range(df_processed$DC, na.rm = TRUE) 95 | df_processed$NORM_DC = if (diff(range_dc) == 0) { 96 | warning("All DC values are identical. NORM_DC set to 0.") 97 | 0 98 | } else { 99 | (df_processed$DC - range_dc[1]) / diff(range_dc) 100 | } 101 | 102 | # Convert time from seconds to milliseconds 103 | df_processed$MILLI_SEC = df_processed$SECS * 1000 104 | 105 | # Assign the 'jip' class 106 | class(df_processed) = c("jip", class(df_processed)) 107 | 108 | return(df_processed) 109 | } 110 | 111 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | <!-- README.md is generated from README.Rmd. Please edit that file --> 6 | 7 | # jiptest 8 | 9 | Fast and reproducible analysis of *OJIP* induction curves measured with the **LI-6800** portable photosynthesis system. 10 | The package implements the **JIP-test** as described by: 11 | 12 | > Tsimilli-Michael, M. (2019). Revisiting JIP-test: an educative review on concepts, assumptions, approximations, definitions and terminology. *Photosynthetica* **57**(SI): 90-107. 13 | 14 | While the code is validated against the equations in the reference above, **always cross-verify results before scientific publication**. Issues, feature requests, and pull requests are warmly welcome. 15 | 16 | 17 | ## Installation 18 | 19 | Currently you can only install it from GitHub or gitee with: 20 | 21 | ```r 22 | # Install devtools if not already installed 23 | if (!requireNamespace("devtools", quietly = TRUE)) { 24 | install.packages("devtools") 25 | } 26 | 27 | # Install jiptest 28 | devtools::install_github("zhujiedong/jiptest") 29 | devtools::install_git("https://gitee.com/zhu_jie_dong/jiptest") 30 | ``` 31 | 32 | ## Quick Workflow 33 | 34 | The package streamlines OJIP analysis into 5 intuitive steps: 35 | 36 | | Step | Function | Purpose | 37 | | --------------------- | ---------------------- | ------------------- | 38 | | 1. Read single file | `read_induction()` | tidy OJIP curve | 39 | | 2. Read entire folder | `read_all_induction()` | bind many files | 40 | | 3. Compute parameters | `jip_test()` | run JIP-test | 41 | | 4. Visualise | `plot()` | OJIP curve | 42 | | 5. PCA | `jip_pca()` | ready-to-use matrix | 43 | 44 | ## Data import 45 | ### Single file 46 | 47 | mport a single OJIP Excel file (output from LI-6800) and return a tidy data frame: 48 | 49 | ```{r} 50 | library(jiptest) 51 | ojip_single = read_induction("inst/extdata/ojip/INDUCTION-4188-20201116-10_53_39.xlsx") 52 | knitr::kable(head(ojip_single[, c("SOURCE", "MILLI_SEC", "DC", "FLUOR")])) 53 | ``` 54 | 55 | ### Batch Import (Entire Folder) 56 | 57 | Efficiently import all OJIP files in a folder (supports pattern to filter files): 58 | 59 | ```{r} 60 | all_data = ("inst/extdata/ojip") 61 | ojip_all = read_all_induction(all_data, pattern = "\\.xlsx$", verbose = TRUE) 62 | 63 | # Check data structure 64 | cat("Number of samples:", length(unique(ojip_all$SOURCE)), "\n") 65 | cat("Total data rows:", nrow(ojip_all), "\n") 66 | ``` 67 | 68 | 69 | ## Run JIP-test 70 | 71 | The JIP-test calculates ~20 key chlorophyll fluorescence parameters (e.g., Fo, Fm, Fv/Fm, PI_ABS). 72 | 73 | - Default: Uses the DC (continuous) signal (higher signal-to-noise ratio). 74 | - Set use_PAM = TRUE to use the AC (PAM) signal instead. 75 | 76 | ```{r} 77 | # Calculate parameters for single sample (DC signal) 78 | params_single = jip_test(ojip_single, verbose = TRUE) 79 | 80 | # Calculate parameters for all samples (PAM signal) 81 | params_all_pam = jip_test(ojip_all, use_PAM = TRUE, verbose = TRUE) 82 | # Calculate parameters for all samples (continous signal) 83 | params_all= jip_test(ojip_all, verbose = TRUE) 84 | # View part of the parameters 85 | 86 | knitr::kable( 87 | subset( 88 | params_single, 89 | select = c(SOURCE, Fo, Fm, Fv, phi_Po, PI_ABS, PI_total) 90 | ) 91 | ) 92 | ``` 93 | 94 | For a full list of calculated parameters and their definitions, see ?jip_test. 95 | 96 | ## Visualization 97 | 98 | The `plot()` method generates publication-ready OJIP curves with flexible customization. 99 | Fluorescence data are automatically normalized to the range [0, 1] for easy comparison: 100 | 101 | $$F=\frac{Fm−Fo}{Ft−Fo}​$$ 102 | 103 | ### Single measurement 104 | 105 | Compare DC and PAM signals for a single sample: 106 | 107 | ```{r} 108 | #| layout-nrow: 1 109 | plot(ojip_single, main = "OJIP Curve (DC Signal)") 110 | plot(ojip_single, use_PAM = TRUE, main = "OJIP Curve (PAM Signal)") 111 | ``` 112 | 113 | ### Grouped Samples (Treatment Comparison) 114 | 115 | Assign samples to experimental groups and plot aggregated curves:Grouped Samples (Treatment Comparison) 116 | Assign samples to experimental groups and plot aggregated curves: 117 | ```{r} 118 | list_data_source = unique(ojip_all$SOURCE) 119 | # Define groups (match order of unique samples) 120 | groups = c("2020_species", "2018_species", "2020_species", rep("2017_species", 4), rep("2024_species", 3)) 121 | 122 | # Assign groups to data 123 | ojip_grouped = set_category_base( 124 | df = ojip_all, 125 | category = groups, 126 | new_col_name = "SOURCE" # Overwrite SOURCE with group names 127 | ) 128 | 129 | # Plot grouped curves 130 | plot(ojip_grouped, main = "OJIP Curves by Treatment Group") 131 | 132 | # USE PAM signal 133 | # plot(ojip_grouped, use_PAM = TRUE, main = "OJIP Curves by Treatment Group") 134 | ``` 135 | 136 | ### Customization Parameters 137 | 138 | All plotting functions support these flexible arguments: 139 | 140 | | Argument | Type | Meaning | 141 | | ------------- | --------- | --------------------------------------- | 142 | | `use_PAM` | logical | `FALSE` = DC (default), `TRUE` = PAM | 143 | | `legend_pos` | character | “topleft”, “bottomright”, … or `c(x,y)` | 144 | | `col` | vector | Custom colours (recycled if needed) | 145 | | `show_labels` | logical | Print sample IDs next to curves | 146 | | `alpha` | numeric | Transparency 0–1 (default 0.6) | 147 | 148 | 149 | 150 | ## Principal Component Analysis (PCA) 151 | 152 | Combine jip_test() and jip_pca() with FactoMineR/factoextra for dimensionality reduction and group comparison: 153 | 154 | ```{r} 155 | #| message: false 156 | #| warning: false 157 | 158 | library(FactoMineR) 159 | library(factoextra) 160 | library(ggplot2) # Explicitly load ggplot2 for ggtitle() 161 | 162 | # 1. Calculate JIP parameters (DC signal = continuous) 163 | all_data_continuous = jip_test(ojip_all, use_PAM = FALSE, verbose = TRUE) 164 | 165 | # 2. Preprocess with jip_pca() - SOURCE = original file names 166 | pca_df = jip_pca(all_data_continuous) 167 | 168 | # 3. Verify original SOURCE names (matches your 7 samples) 169 | unique(pca_df$SOURCE) 170 | 171 | # 4. Define treatment groups 172 | treatment = c("2020_species", "2018_species", "2020_species", rep("2017_species", 4), rep("2024_species", 3)) 173 | 174 | 175 | # 5. Assign groups (use set_category_base()) 176 | pca_df = set_category_base( 177 | df = pca_df, 178 | category = treatment, 179 | new_col_name = "SOURCE" 180 | ) 181 | 182 | # Verify groups 183 | unique(pca_df$SOURCE) 184 | 185 | # 6. Run PCA (no changes) 186 | df = pca_df[, -1] # Remove SOURCE column 187 | final_pca = PCA(df, graph = FALSE) 188 | 189 | # 7. Error-Free Plots (use ggtitle() instead of main=) 190 | # Scree Plot (fixed: no main argument) 191 | fviz_eig(final_pca, addlabels = TRUE) + 192 | ggtitle("Scree Plot (Continuous Signal)") + 193 | theme_minimal() 194 | 195 | # Variable Loadings Plot (fixed: no main argument) 196 | fviz_pca_var(final_pca) + 197 | ggtitle("Variable Loadings (Continuous Signal)") + 198 | theme_minimal() 199 | 200 | # Sample Scores Plot (fixed: no main argument) 201 | fviz_pca_ind( 202 | final_pca, 203 | repel = TRUE, 204 | col.ind = pca_df$SOURCE, 205 | palette = c("#E74C3C", "#3498DB", "#2ECC71", "#CDAD00"), # 2017=Red, 2018=Blue, 2020=Green 206 | legend.title = element_text("Treatment Group") # Proper legend title formatting 207 | ) + 208 | ggtitle("Sample Scores (Continuous Signal)") + 209 | theme_minimal() 210 | ``` 211 | 212 | Full help: `?jiptest` 213 | 214 | ## Citation 215 | If you use jiptest in a publication please cite: 216 | 217 | @misc{jiptest2025, 218 | author = {Zhu, Jiedong}, 219 | title = {jiptest: JIP-test analysis for LI-6800 OJIP curves}, 220 | year = {2025}, 221 | url = {https://github.com/zhujiedong/jiptest} 222 | } 223 | 224 | Also cite the foundational JIP-test reference: 225 | 226 | @article{Strasser2000, 227 | title={The fluorescence transient as a tool to characterize and screen photosynthetic samples}, 228 | author={ Strasser, R. J. and Srivastava, A. and Tsimilli-Michael, M. }, 229 | journal={Probing Photosynthesis Mechanisms Regulation & Adaptation}, 230 | year={2000}, 231 | } 232 | 233 | ## License 234 | 235 | MIT © Jiedong Zhu 236 | 237 | -------------------------------------------------------------------------------- /R/jip_comp.R: -------------------------------------------------------------------------------- 1 | #' JIP-test Parameter Calculation Core Function 2 | #' 3 | #' This internal function computes the core JIP-test parameters from LI-6800 induction data, 4 | #' following the methodology described in: 5 | #' Tsimilli-Michael M. Revisiting JIP-test: An educative review on concepts, assumptions, 6 | #' approximations, definitions and terminology[J]. Photosynthetica, 2019, 57(SI): 90-107. 7 | #' 8 | #' @param sample_data Data frame from read_induction() containing at least columns: 9 | #' SOURCE, MILLI_SEC, DC, FLUOR 10 | #' @param use_PAM Logical indicating whether to use PAM fluorescence (FLUOR column) 11 | #' or continuous fluorescence (DC column, default) 12 | #' 13 | #' @return Wide-format data frame with one row per sample and one column per JIP-test parameter, 14 | #' including: 15 | #' - Basic fluorescence values (F20us, F50us, F100us, F300us, FJ, FI, FP, Fo, Fm, Fv) 16 | #' - Derived parameters (VJ, VI, MO, Sm, Ss, tFm, Area, ECo_RC, N, RC_ABS, gamma_RC) 17 | #' - Quantum yields (phi_Po, phi_Eo, phi_Ro) 18 | #' - Energy fluxes (ABS_RC, TRo_RC, ETo_RC, REo_RC) 19 | #' - Performance indices (PI_ABS, PI_total) 20 | #' - Probabilities (Psi_Eo, delta_Ro) 21 | #' 22 | #' @keywords internal 23 | jip_comp <- function(sample_data, use_PAM = FALSE) { 24 | # Validate input columns 25 | required_cols <- c("SOURCE", "MILLI_SEC", "DC", "FLUOR") 26 | if (!all(required_cols %in% colnames(sample_data))) { 27 | stop(paste("sample_data must contain columns:", paste(required_cols, collapse = ", "))) 28 | } 29 | 30 | # Extract source identifier (one sample per call) 31 | source_name <- unique(sample_data$SOURCE) 32 | if (length(source_name) != 1) { 33 | warning("sample_data contains multiple SOURCE values - using first one") 34 | source_name <- source_name[1] 35 | } 36 | 37 | # Select fluorescence signal and time data 38 | if (use_PAM) { 39 | Ft <- sample_data$FLUOR 40 | } else { 41 | Ft <- sample_data$DC 42 | } 43 | time_ms <- sample_data$MILLI_SEC 44 | 45 | # Sort data by time to ensure chronological order 46 | order_idx <- order(time_ms) 47 | time_ms_sorted <- time_ms[order_idx] 48 | Ft_sorted <- Ft[order_idx] 49 | 50 | # Helper function to find closest time index (ms) 51 | get_closest_idx <- function(target_ms) { 52 | which.min(abs(time_ms_sorted - target_ms)) 53 | } 54 | 55 | # -------------------------- 56 | # Step 1: Extract key time points (convert us to ms for consistency) 57 | # -------------------------- 58 | F20us <- Ft_sorted[get_closest_idx(0.02)] # 20 microseconds = 0.02 ms 59 | F50us <- Ft_sorted[get_closest_idx(0.05)] # 50 microseconds = 0.05 ms 60 | F100us <- Ft_sorted[get_closest_idx(0.1)] # 100 microseconds = 0.1 ms 61 | F300us <- Ft_sorted[get_closest_idx(0.3)] # 300 microseconds = 0.3 ms 62 | FJ <- Ft_sorted[get_closest_idx(2)] # J-step (2 ms) 63 | FI <- Ft_sorted[get_closest_idx(30)] # I-step (30 ms) 64 | FP <- max(Ft_sorted) # Maximum fluorescence (Fm) 65 | tFm <- time_ms_sorted[which.max(Ft_sorted)] # Time of maximum fluorescence (ms) 66 | 67 | # Calculate area under OJIP curve (trapezoidal rule, from 0 to tFm) 68 | curve_range <- which(time_ms_sorted <= tFm) 69 | if (length(curve_range) >= 2) { 70 | time_segment <- time_ms_sorted[curve_range] 71 | signal_segment <- Ft_sorted[curve_range] 72 | Area <- sum(diff(time_segment) * (signal_segment[-1] + signal_segment[-length(signal_segment)]) / 2) 73 | } else { 74 | Area <- NA 75 | warning(paste("Insufficient data points for area calculation in", source_name)) 76 | } 77 | 78 | # -------------------------- 79 | # Step 2: Basic parameters 80 | # -------------------------- 81 | Fo <- F20us # Minimum fluorescence (20us) 82 | Fm <- FP # Maximum fluorescence 83 | Fv <- Fm - Fo # Variable fluorescence 84 | if (Fv < 0) warning(paste("Negative Fv detected in", source_name, "- check data quality")) 85 | 86 | # -------------------------- 87 | # Step 3: Normalized parameters 88 | # -------------------------- 89 | VJ <- if (Fv != 0) (FJ - Fo) / Fv else NA # J-step normalized fluorescence (V_J) 90 | VI <- if (Fv != 0) (FI - Fo) / Fv else NA # I-step normalized fluorescence (V_I) 91 | 92 | # -------------------------- 93 | # Step 4: Kinetic parameters 94 | # -------------------------- 95 | MO <- if (Fv != 0) 4 * (F300us - F50us) / Fv else NA # Initial slope (M_O) 96 | Sm <- if (Fv != 0) Area / Fv else NA # Normalized area (S_m) 97 | Ss <- if (!is.na(MO) && MO != 0) VJ / MO else NA # O-J phase normalized area (S_s) 98 | 99 | # -------------------------- 100 | # Step 5: Quantum yields & efficiencies 101 | # -------------------------- 102 | phi_Po <- if (Fm != 0) 1 - (Fo / Fm) else NA # Maximum quantum yield (φ_Po) 103 | phi_Eo <- if (!is.na(phi_Po) && !is.na(VJ)) phi_Po * (1 - VJ) else NA # Electron transport yield (φ_Eo) 104 | phi_Ro <- if (!is.na(phi_Po) && !is.na(VI)) phi_Po * (1 - VI) else NA # Reduction yield (φ_Ro) 105 | 106 | Psi_Eo <- if (!is.na(VJ)) 1 - VJ else NA # Electron transport probability (ψ_Eo) 107 | delta_Ro <- if (!is.na(VJ) && !is.na(VI) && (1 - VJ) != 0) (1 - VI) / (1 - VJ) else NA # End electron transport probability (δ_Ro) 108 | 109 | # -------------------------- 110 | # Step 6: Energy fluxes per reaction center (RC) 111 | # -------------------------- 112 | ABS_RC <- if (!is.na(MO) && !is.na(VJ) && !is.na(phi_Po) && VJ != 0 && phi_Po != 0) { 113 | MO * (1 / VJ) * (1 / phi_Po) 114 | } else { 115 | NA 116 | } # Absorption flux (ABS/RC) 117 | 118 | TRo_RC <- if (!is.na(MO) && VJ != 0) MO * (1 / VJ) else NA # Trapped energy flux (TR_o/RC) 119 | ETo_RC <- if (!is.na(TRo_RC) && !is.na(Psi_Eo)) TRo_RC * Psi_Eo else NA # Electron transport flux (ET_o/RC) 120 | REo_RC <- if (!is.na(ETo_RC) && !is.na(delta_Ro)) ETo_RC * delta_Ro else NA # Reduction flux (RE_o/RC) 121 | 122 | # -------------------------- 123 | # Step 7: Additional biophysical parameters 124 | # -------------------------- 125 | ECo_RC <- Sm # Electron transport chain capacity (ECo/RC) 126 | N <- if (!is.na(Sm) && !is.na(MO) && !is.na(VJ) && VJ != 0) Sm * (MO / VJ) else NA # Turnover number (N) 127 | RC_ABS <- if (!is.na(ABS_RC) && ABS_RC != 0) 1 / ABS_RC else NA # RC/ABS ratio (reaction centers per absorption) 128 | gamma_RC <- if (!is.na(RC_ABS)) RC_ABS / (RC_ABS + 1) else NA # RC concentration ratio (γ_RC) 129 | 130 | # -------------------------- 131 | # Step 8: Performance indices 132 | # -------------------------- 133 | PI_ABS <- if (!is.na(gamma_RC) && !is.na(phi_Po) && !is.na(Psi_Eo) && 134 | (1 - gamma_RC) != 0 && (1 - phi_Po) != 0 && (1 - Psi_Eo) != 0) { 135 | (gamma_RC / (1 - gamma_RC)) * (phi_Po / (1 - phi_Po)) * (Psi_Eo / (1 - Psi_Eo)) 136 | } else { 137 | NA 138 | } # Performance index (PI_ABS) 139 | 140 | PI_total <- if (!is.na(PI_ABS) && !is.na(delta_Ro) && (1 - delta_Ro) != 0) { 141 | PI_ABS * (delta_Ro / (1 - delta_Ro)) 142 | } else { 143 | NA 144 | } # Extended performance index (PI_total) 145 | 146 | # -------------------------- 147 | # Step 9: Organize output (WIDE FORMAT - 1 row per sample, 1 column per parameter) 148 | # -------------------------- 149 | output_df <- data.frame( 150 | SOURCE = source_name, 151 | # Basic fluorescence 152 | F20us = round(F20us, 6), 153 | F50us = round(F50us, 6), 154 | F100us = round(F100us, 6), 155 | F300us = round(F300us, 6), 156 | FJ = round(FJ, 6), 157 | FI = round(FI, 6), 158 | FP = round(FP, 6), 159 | # Derived basic parameters 160 | Fo = round(Fo, 6), 161 | Fm = round(Fm, 6), 162 | Fv = round(Fv, 6), 163 | tFm = round(tFm, 6), 164 | Area = round(Area, 6), 165 | # Normalized parameters 166 | VJ = round(VJ, 6), 167 | VI = round(VI, 6), 168 | # Kinetic parameters 169 | MO = round(MO, 6), 170 | Sm = round(Sm, 6), 171 | Ss = round(Ss, 6), 172 | # Quantum yields 173 | phi_Po = round(phi_Po, 6), 174 | phi_Eo = round(phi_Eo, 6), 175 | phi_Ro = round(phi_Ro, 6), 176 | # Probabilities 177 | Psi_Eo = round(Psi_Eo, 6), 178 | delta_Ro = round(delta_Ro, 6), 179 | # Energy fluxes 180 | ABS_RC = round(ABS_RC, 6), 181 | TRo_RC = round(TRo_RC, 6), 182 | ETo_RC = round(ETo_RC, 6), 183 | REo_RC = round(REo_RC, 6), 184 | # Additional parameters 185 | ECo_RC = round(ECo_RC, 6), 186 | N = round(N, 6), 187 | RC_ABS = round(RC_ABS, 6), 188 | gamma_RC = round(gamma_RC, 6), 189 | # Performance indices 190 | PI_ABS = round(PI_ABS, 6), 191 | PI_total = round(PI_total, 6), 192 | stringsAsFactors = FALSE 193 | ) 194 | 195 | return(output_df) 196 | } 197 | -------------------------------------------------------------------------------- /R/jiptest-method.R: -------------------------------------------------------------------------------- 1 | #' @title Plot JIP test data measured by LI-6800 2 | #' 3 | #' @description A enhanced function to visualize JIP test data with improved 4 | #' customization, better default settings, and clearer data presentation. 5 | #' Graphical preview of all data curves in log axis (BLUE > 1.2.2) with 6 | #' normalized signals. 7 | #' 8 | #' @param df Data frame: files read by \code{read_induction} or \code{read_all_induction} 9 | #' @param def_pch Numeric/vector: Point character(s) for plotting (default: 19) 10 | #' @param alpha Numeric: Transparency degree of colors (0-1, default: 0.6) 11 | #' @param legend_pos Character/numeric: Legend position (default: "topleft"). 12 | #' Can be "topleft", "topright", "bottomleft", "bottomright", "top", "bottom", 13 | #' "left", "right", "center" or a numeric vector of length 2 (x,y coordinates) 14 | #' @param leg_cex Numeric: Text size of legend (default: 0.6) 15 | #' @param leg_point_cex Numeric: Point size in legend (default: 0.9) 16 | #' @param leg_bty Character: Box type for legend ("n" = no box, "o" = box, default: "n") 17 | #' @param xlim Numeric vector: Limits for the X axis (default: NULL, auto-calculated) 18 | #' @param ylim Numeric vector: Limits for the Y axis (default: c(0, 1.1) for better visibility) 19 | #' @param xlab Character: X axis label (default: "Time (ms)") 20 | #' @param ylab Character: Y axis label (default: "Normalized fluorescence signal") 21 | #' @param col Character/vector: Custom colors for groups (default: NULL, uses palette.colors) 22 | #' @param log Character: Logarithmic axis ("x" for X-axis only, default: "x"; 23 | #' use "" for no log axis) 24 | #' @param xmark Expression/numeric: X tick mark labels (default: scientific notation) 25 | #' @param xat Numeric: Positions for X axis tick marks (default: c(0.001, 0.01, 0.1, 1, 10, 100, 1000)) 26 | #' @param use_PAM Logical: Whether to use PAM signal (NORM_FLUOR) instead of DC signal (NORM_DC) 27 | #' (default: FALSE) 28 | #' @param add_leg Logical: Whether to add legend (default: TRUE) 29 | #' @param add_grid Logical: Whether to add grid lines (default: TRUE) 30 | #' @param grid_lty Numeric: Grid line type (default: 3, dashed) 31 | #' @param grid_col Character: Grid line color (default: "gray80") 32 | #' @param main Character: Plot title (default: NULL) 33 | #' @param main_cex Numeric: Title size (default: 1.2) 34 | #' @param axis_cex Numeric: Axis text size (default: 0.9) 35 | #' @param labs_cex Numeric: Axis label size (default: 1.0) 36 | #' @param line_width Numeric: Line width for points (default: 0.5) 37 | #' @param show_labels Logical: Whether to show sample labels next to curves (default: FALSE) 38 | #' @param label_cex Numeric: Size of sample labels (default: 0.7) 39 | #' @param label_offset Numeric: Offset for sample labels (default: 0.02) 40 | #' @param ... Additional parameters passed to \code{plot} and \code{legend} 41 | #' 42 | #' @importFrom graphics points abline legend text axis grid par 43 | #' @importFrom grDevices palette.colors adjustcolor 44 | #' @importFrom stats na.omit 45 | #' @export 46 | #' 47 | #' @examples 48 | #' \dontrun{ 49 | #' # Basic plot 50 | #' plot.jip(df) 51 | #' 52 | #' # With custom colors and title 53 | #' plot.jip(df, col = c("red", "blue", "green"), main = "JIP Test Results") 54 | #' 55 | #' # Use PAM signal, show sample labels, change legend position 56 | #' plot.jip(df, use_PAM = TRUE, show_labels = TRUE, legend_pos = "bottomright") 57 | #' 58 | #' # Custom grid and axis settings 59 | #' plot.jip(df, grid_lty = 1, grid_col = "gray50", axis_cex = 1.0) 60 | #' } 61 | 62 | plot.jip = function(df, 63 | def_pch = 19, 64 | alpha = 0.6, 65 | leg_bty = "n", 66 | leg_cex = 0.6, 67 | leg_point_cex = 0.9, 68 | legend_pos = "topleft", 69 | xlim = NULL, 70 | ylim = c(0, 1.1), # Extended for better visibility 71 | xlab = "Time (ms)", 72 | ylab = "Normalized fluorescence signal", 73 | col = NULL, 74 | log = "x", 75 | xmark = c(expression(10^{-3}), expression(10^{-2}), expression(10^{-1}), 76 | expression(10^{0}), expression(10^{1}), expression(10^2), expression(10^3)), 77 | xat = c(0.001, 0.01, 0.1, 1, 10, 100, 1000), 78 | use_PAM = FALSE, 79 | add_leg = TRUE, 80 | add_grid = TRUE, 81 | grid_lty = 3, 82 | grid_col = "gray80", 83 | main = NULL, 84 | main_cex = 1.2, 85 | axis_cex = 0.9, 86 | labs_cex = 1.0, 87 | line_width = 0.5, 88 | show_labels = FALSE, 89 | label_cex = 0.7, 90 | label_offset = 0.02, 91 | ...) { 92 | 93 | # -------------------------- 94 | # Input Validation 95 | # -------------------------- 96 | if (!is.data.frame(df)) { 97 | stop("df must be a data frame from read_induction or read_all_induction") 98 | } 99 | 100 | # Check required columns 101 | required_cols = c("SOURCE", "MILLI_SEC") 102 | signal_col = if (use_PAM) "NORM_FLUOR" else "NORM_DC" 103 | required_cols = c(required_cols, signal_col) 104 | 105 | missing_cols = setdiff(required_cols, colnames(df)) 106 | if (length(missing_cols) > 0) { 107 | stop(paste("Data frame missing required columns:", paste(missing_cols, collapse = ", "))) 108 | } 109 | 110 | # Remove NA values 111 | df = na.omit(df[, required_cols]) 112 | 113 | if (nrow(df) == 0) { 114 | stop("No valid data after removing NA values") 115 | } 116 | 117 | # -------------------------- 118 | # Prepare Group Information 119 | # -------------------------- 120 | df$SOURCE = as.factor(df$SOURCE) 121 | groups = levels(df$SOURCE) 122 | n_groups = length(groups) 123 | 124 | # -------------------------- 125 | # Color Preparation 126 | # -------------------------- 127 | if (is.null(col)) { 128 | # Use distinct palette, handle more groups by recycling 129 | col = palette.colors(n = n_groups, palette = "Set1", alpha = alpha, recycle = TRUE) 130 | } else { 131 | # Ensure color vector matches number of groups 132 | if (length(col) != n_groups) { 133 | warning(paste("Color vector length (", length(col), ") does not match number of groups (", 134 | n_groups, "). Recycling colors.", sep = "")) 135 | col = rep(col, length.out = n_groups) 136 | } 137 | # Apply alpha if not already applied 138 | col = adjustcolor(col, alpha.f = alpha) 139 | } 140 | 141 | # -------------------------- 142 | # Point Character Preparation 143 | # -------------------------- 144 | if (length(def_pch) != 1 && length(def_pch) != n_groups) { 145 | warning(paste("pch vector length (", length(def_pch), ") does not match number of groups (", 146 | n_groups, "). Using default pch = 19.", sep = "")) 147 | def_pch = 19 148 | } 149 | pch_vec = if (length(def_pch) == 1) rep(def_pch, n_groups) else def_pch 150 | 151 | # -------------------------- 152 | # Axis Limit Preparation 153 | # -------------------------- 154 | # X axis limits (ensure log-compatible) 155 | if (is.null(xlim)) { 156 | x_min = min(df$MILLI_SEC, na.rm = TRUE) 157 | x_max = max(df$MILLI_SEC, na.rm = TRUE) 158 | # Add small buffer for log scale 159 | xlim = c(x_min * 0.9, x_max * 1.1) 160 | } else { 161 | if (any(xlim <= 0) && log == "x") { 162 | warning("xlim contains non-positive values with log='x'. Adjusting to positive values.") 163 | xlim[xlim <= 0] = min(df$MILLI_SEC[df$MILLI_SEC > 0], na.rm = TRUE) * 0.9 164 | } 165 | } 166 | 167 | # Y axis limits (auto-adjust if needed) 168 | if (is.null(ylim)) { 169 | y_min = min(df[[signal_col]], na.rm = TRUE) * 0.9 170 | y_max = max(df[[signal_col]], na.rm = TRUE) * 1.1 171 | ylim = c(max(y_min, 0), y_max) # Ensure y >= 0 172 | } 173 | 174 | # -------------------------- 175 | # Plot Setup 176 | # -------------------------- 177 | # Save original par settings 178 | old_par = par(no.readonly = TRUE) 179 | on.exit(par(old_par)) # Restore par settings on exit 180 | 181 | # Set plot margins and text sizes 182 | par( 183 | mar = c(4.5, 4.5, 3, 1.5) + 0.1, # Adjust margins for better labels 184 | cex.main = main_cex, 185 | cex.axis = axis_cex, 186 | cex.lab = labs_cex 187 | ) 188 | 189 | # -------------------------- 190 | # Create Base Plot 191 | # -------------------------- 192 | plot( 193 | x = df$MILLI_SEC, 194 | y = df[[signal_col]], 195 | log = log, 196 | xlim = xlim, 197 | ylim = ylim, 198 | xlab = xlab, 199 | ylab = ylab, 200 | col = col[df$SOURCE], 201 | pch = pch_vec[df$SOURCE], 202 | lwd = line_width, 203 | xaxt = "n", 204 | main = main, 205 | type = "n", # Initialize empty plot first for better control 206 | ... 207 | ) 208 | 209 | # Add grid (improved visibility) 210 | if (add_grid) { 211 | grid( 212 | nx = NA, # Auto x grid 213 | ny = NULL, # Manual y grid for better spacing 214 | lty = grid_lty, 215 | col = grid_col, 216 | lwd = 0.8 217 | ) 218 | # Add vertical grid lines at xat positions 219 | abline(v = xat, lty = grid_lty, col = grid_col, lwd = 0.8) 220 | } 221 | 222 | # Add data points (plot group by group for better control) 223 | for (i in seq_along(groups)) { 224 | group_data = df[df$SOURCE == groups[i], ] 225 | points( 226 | x = group_data$MILLI_SEC, 227 | y = group_data[[signal_col]], 228 | col = col[i], 229 | pch = pch_vec[i], 230 | lwd = line_width, 231 | ... 232 | ) 233 | } 234 | 235 | # -------------------------- 236 | # Custom X Axis 237 | # -------------------------- 238 | axis( 239 | side = 1, 240 | at = xat, 241 | labels = xmark, 242 | las = 1, # Horizontal axis labels 243 | tcl = -0.5, # Tick length 244 | ... 245 | ) 246 | 247 | # -------------------------- 248 | # Add Legend 249 | # -------------------------- 250 | if (add_leg && n_groups > 0) { 251 | # Handle custom legend positions 252 | if (is.character(legend_pos)) { 253 | legend_pos = match.arg( 254 | legend_pos, 255 | choices = c("topleft", "topright", "bottomleft", "bottomright", 256 | "top", "bottom", "left", "right", "center") 257 | ) 258 | } 259 | 260 | legend( 261 | x = legend_pos, 262 | legend = groups, 263 | col = col, 264 | pch = pch_vec, 265 | cex = leg_cex, 266 | pt.cex = leg_point_cex, 267 | pt.lwd = line_width, 268 | bty = leg_bty, 269 | bg = if (leg_bty == "o") adjustcolor("white", alpha.f = 0.8) else NA, 270 | box.lwd = if (leg_bty == "o") 1 else 0, 271 | inset = 0.02, # Small inset to prevent legend from being cut off 272 | ... 273 | ) 274 | } 275 | 276 | # -------------------------- 277 | # Add Sample Labels (Optional) 278 | # -------------------------- 279 | if (show_labels) { 280 | for (i in seq_along(groups)) { 281 | group_data = df[df$SOURCE == groups[i], ] 282 | # Place label at the last data point (or maximum time point) 283 | label_point = group_data[which.max(group_data$MILLI_SEC), ] 284 | text( 285 | x = label_point$MILLI_SEC, 286 | y = label_point[[signal_col]] + label_offset, 287 | labels = groups[i], 288 | cex = label_cex, 289 | col = col[i], 290 | adj = 0, # Left-align text 291 | ... 292 | ) 293 | } 294 | } 295 | 296 | # -------------------------- 297 | # Add Reference Lines (Optional) 298 | # -------------------------- 299 | # Add F0 reference line (dashed gray) 300 | f0_values = tapply(df[[signal_col]], df$SOURCE, function(x) x[which.min(df$MILLI_SEC)]) 301 | if (length(unique(f0_values)) > 0) { 302 | abline(h = mean(f0_values), col = adjustcolor("gray50", alpha.f = 0.5), lty = 2, lwd = 0.8) 303 | text(x = max(xlim), y = mean(f0_values), labels = "F0", 304 | col = adjustcolor("gray50", alpha.f = 0.7), cex = 0.7, adj = 1) 305 | } 306 | 307 | invisible(df) # Return data frame invisibly for chaining 308 | } 309 | 310 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | \ 3 | 4 | # jiptest 5 | 6 | Fast and reproducible analysis of *OJIP* induction curves measured with 7 | the **LI-6800** portable photosynthesis system. 8 | The package implements the **JIP-test** as described by: 9 | 10 | \> Tsimilli-Michael, M. (2019). Revisiting JIP-test: an educative review 11 | on concepts, assumptions, approximations, definitions and terminology. 12 | *Photosynthetica* **57**(SI): 90-107. 13 | 14 | While the code is validated against the equations in the reference 15 | above, **always cross-verify results before scientific publication**. 16 | Issues, feature requests, and pull requests are warmly welcome. 17 | 18 | ## Installation 19 | 20 | Currently you can only install it from GitHub or gitee with: 21 | 22 | ``` r 23 | # Install devtools if not already installed 24 | if (!requireNamespace("devtools", quietly = TRUE)) { 25 | install.packages("devtools") 26 | } 27 | 28 | # Install jiptest 29 | devtools::install_github("zhujiedong/jiptest") 30 | devtools::install_git("https://gitee.com/zhu_jie_dong/jiptest") 31 | ``` 32 | 33 | ## Quick Workflow 34 | 35 | The package streamlines OJIP analysis into 5 intuitive steps: 36 | 37 | | Step | Function | Purpose | 38 | |------------------------|------------------------|---------------------| 39 | | 1\. Read single file | `read_induction()` | tidy OJIP curve | 40 | | 2\. Read entire folder | `read_all_induction()` | bind many files | 41 | | 3\. Compute parameters | `jip_test()` | run JIP-test | 42 | | 4\. Visualise | `plot()` | OJIP curve | 43 | | 5\. PCA | `jip_pca()` | ready-to-use matrix | 44 | 45 | ## Data import 46 | 47 | ### Single file 48 | 49 | mport a single OJIP Excel file (output from LI-6800) and return a tidy 50 | data frame: 51 | 52 | ``` r 53 | library(jiptest) 54 | ojip_single = read_induction("inst/extdata/ojip/INDUCTION-4188-20201116-10_53_39.xlsx") 55 | knitr::kable(head(ojip_single[, c("SOURCE", "MILLI_SEC", "DC", "FLUOR")])) 56 | ``` 57 | 58 | | | SOURCE | MILLI_SEC | DC | FLUOR | 59 | |:----|:---------------------------------|----------:|-------:|------:| 60 | | 7 | INDUCTION-4188-20201116-10_53_39 | 0.0020266 | 96040 | 621 | 61 | | 8 | INDUCTION-4188-20201116-10_53_39 | 0.0060797 | 98161 | 621 | 62 | | 9 | INDUCTION-4188-20201116-10_53_39 | 0.0101328 | 100331 | 620 | 63 | | 10 | INDUCTION-4188-20201116-10_53_39 | 0.0141859 | 102577 | 649 | 64 | | 11 | INDUCTION-4188-20201116-10_53_39 | 0.0180006 | 104849 | 629 | 65 | | 12 | INDUCTION-4188-20201116-10_53_39 | 0.0220537 | 107214 | 674 | 66 | 67 | ### Batch Import (Entire Folder) 68 | 69 | Efficiently import all OJIP files in a folder (supports pattern to 70 | filter files): 71 | 72 | ``` r 73 | all_data = ("inst/extdata/ojip") 74 | ojip_all = read_all_induction(all_data, pattern = "\\.xlsx$", verbose = TRUE) 75 | ``` 76 | 77 | ## Reading file: INDUCTION-18510-20240308-20_40_54.xlsx 78 | 79 | ## Reading file: INDUCTION-18512-20240308-20_42_37.xlsx 80 | 81 | ## Reading file: INDUCTION-18514-20240308-20_44_22.xlsx 82 | 83 | ## Reading file: INDUCTION-26-20201026-16_07_50.xlsx 84 | 85 | ## Reading file: INDUCTION-2896-20180802-09_27_02.xlsx 86 | 87 | ## Reading file: INDUCTION-4188-20201116-10_53_39.xlsx 88 | 89 | ## Reading file: INDUCTION-484-20171225-13_15_58.xlsx 90 | 91 | ## Reading file: INDUCTION-485-20171225-14_12_14.xlsx 92 | 93 | ## Reading file: INDUCTION-486-20171225-14_33_46.xlsx 94 | 95 | ## Reading file: INDUCTION-487-20171225-14_33_46.xlsx 96 | 97 | ``` r 98 | # Check data structure 99 | cat("Number of samples:", length(unique(ojip_all$SOURCE)), "\n") 100 | ``` 101 | 102 | ## Number of samples: 10 103 | 104 | ``` r 105 | cat("Total data rows:", nrow(ojip_all), "\n") 106 | ``` 107 | 108 | ## Total data rows: 7212 109 | 110 | ## Run JIP-test 111 | 112 | The JIP-test calculates ~20 key chlorophyll fluorescence parameters 113 | (e.g., Fo, Fm, Fv/Fm, PI_ABS). 114 | 115 | - Default: Uses the DC (continuous) signal (higher signal-to-noise 116 | ratio). 117 | - Set use_PAM = TRUE to use the AC (PAM) signal instead. 118 | 119 | ``` r 120 | # Calculate parameters for single sample (DC signal) 121 | params_single = jip_test(ojip_single, verbose = TRUE) 122 | ``` 123 | 124 | ## Processing sample: INDUCTION-4188-20201116-10_53_39 125 | 126 | ``` r 127 | # Calculate parameters for all samples (PAM signal) 128 | params_all_pam = jip_test(ojip_all, use_PAM = TRUE, verbose = TRUE) 129 | ``` 130 | 131 | ## Processing sample: INDUCTION-18510-20240308-20_40_54 132 | 133 | ## Processing sample: INDUCTION-18512-20240308-20_42_37 134 | 135 | ## Processing sample: INDUCTION-18514-20240308-20_44_22 136 | 137 | ## Processing sample: INDUCTION-26-20201026-16_07_50 138 | 139 | ## Processing sample: INDUCTION-2896-20180802-09_27_02 140 | 141 | ## Processing sample: INDUCTION-4188-20201116-10_53_39 142 | 143 | ## Processing sample: INDUCTION-484-20171225-13_15_58 144 | 145 | ## Processing sample: INDUCTION-485-20171225-14_12_14 146 | 147 | ## Processing sample: INDUCTION-486-20171225-14_33_46 148 | 149 | ## Processing sample: INDUCTION-487-20171225-14_33_46 150 | 151 | ## Warning in jip_comp(sample_data, use_PAM = use_PAM): Insufficient data points 152 | ## for area calculation in INDUCTION-487-20171225-14_33_46 153 | 154 | ``` r 155 | # Calculate parameters for all samples (continous signal) 156 | params_all= jip_test(ojip_all, verbose = TRUE) 157 | ``` 158 | 159 | ## Processing sample: INDUCTION-18510-20240308-20_40_54 160 | 161 | ## Processing sample: INDUCTION-18512-20240308-20_42_37 162 | 163 | ## Processing sample: INDUCTION-18514-20240308-20_44_22 164 | 165 | ## Processing sample: INDUCTION-26-20201026-16_07_50 166 | 167 | ## Processing sample: INDUCTION-2896-20180802-09_27_02 168 | 169 | ## Processing sample: INDUCTION-4188-20201116-10_53_39 170 | 171 | ## Processing sample: INDUCTION-484-20171225-13_15_58 172 | 173 | ## Processing sample: INDUCTION-485-20171225-14_12_14 174 | 175 | ## Processing sample: INDUCTION-486-20171225-14_33_46 176 | 177 | ## Processing sample: INDUCTION-487-20171225-14_33_46 178 | 179 | ``` r 180 | # View part of the parameters 181 | 182 | knitr::kable( 183 | subset( 184 | params_single, 185 | select = c(SOURCE, Fo, Fm, Fv, phi_Po, PI_ABS, PI_total) 186 | ) 187 | ) 188 | ``` 189 | 190 | | SOURCE | Fo | Fm | Fv | phi_Po | PI_ABS | PI_total | 191 | |:---|---:|---:|---:|---:|---:|---:| 192 | | INDUCTION-4188-20201116-10_53_39 | 104849 | 402308 | 297459 | 0.739381 | 0.584275 | 0.379135 | 193 | 194 | For a full list of calculated parameters and their definitions, see 195 | ?jip_test. 196 | 197 | ## Visualization 198 | 199 | The `plot()` method generates publication-ready OJIP curves with 200 | flexible customization. Fluorescence data are automatically normalized 201 | to the range \[0, 1\] for easy comparison: 202 | 203 | $$F=\frac{Fm−Fo}{Ft−Fo}​$$ 204 | 205 | ### Single measurement 206 | 207 | Compare DC and PAM signals for a single sample: 208 | 209 | ``` r 210 | plot(ojip_single, main = "OJIP Curve (DC Signal)") 211 | ``` 212 | 213 | ![](README_files/figure-gfm/unnamed-chunk-4-1.png) 214 | 215 | ``` r 216 | plot(ojip_single, use_PAM = TRUE, main = "OJIP Curve (PAM Signal)") 217 | ``` 218 | 219 | ![](README_files/figure-gfm/unnamed-chunk-4-2.png) 220 | 221 | ### Grouped Samples (Treatment Comparison) 222 | 223 | Assign samples to experimental groups and plot aggregated curves:Grouped 224 | Samples (Treatment Comparison) Assign samples to experimental groups and 225 | plot aggregated curves: 226 | 227 | ``` r 228 | list_data_source = unique(ojip_all$SOURCE) 229 | # Define groups (match order of unique samples) 230 | groups = c("2020_species", "2018_species", "2020_species", rep("2017_species", 4), rep("2024_species", 3)) 231 | 232 | # Assign groups to data 233 | ojip_grouped = set_category_base( 234 | df = ojip_all, 235 | category = groups, 236 | new_col_name = "SOURCE" # Overwrite SOURCE with group names 237 | ) 238 | 239 | # Plot grouped curves 240 | plot(ojip_grouped, main = "OJIP Curves by Treatment Group") 241 | ``` 242 | 243 | ![](README_files/figure-gfm/unnamed-chunk-5-1.png) 244 | 245 | ``` r 246 | # USE PAM signal 247 | # plot(ojip_grouped, use_PAM = TRUE, main = "OJIP Curves by Treatment Group") 248 | ``` 249 | 250 | ### Customization Parameters 251 | 252 | All plotting functions support these flexible arguments: 253 | 254 | | Argument | Type | Meaning | 255 | |---------------|-----------|-----------------------------------------| 256 | | `use_PAM` | logical | `FALSE` = DC (default), `TRUE` = PAM | 257 | | `legend_pos` | character | “topleft”, “bottomright”, … or `c(x,y)` | 258 | | `col` | vector | Custom colours (recycled if needed) | 259 | | `show_labels` | logical | Print sample IDs next to curves | 260 | | `alpha` | numeric | Transparency 0–1 (default 0.6) | 261 | 262 | ## Principal Component Analysis (PCA) 263 | 264 | Combine jip_test() and jip_pca() with FactoMineR/factoextra for 265 | dimensionality reduction and group comparison: 266 | 267 | ``` r 268 | library(FactoMineR) 269 | library(factoextra) 270 | library(ggplot2) # Explicitly load ggplot2 for ggtitle() 271 | 272 | # 1. Calculate JIP parameters (DC signal = continuous) 273 | all_data_continuous = jip_test(ojip_all, use_PAM = FALSE, verbose = TRUE) 274 | 275 | # 2. Preprocess with jip_pca() - SOURCE = original file names 276 | pca_df = jip_pca(all_data_continuous) 277 | 278 | # 3. Verify original SOURCE names (matches your 7 samples) 279 | unique(pca_df$SOURCE) 280 | ``` 281 | 282 | ## [1] "INDUCTION-18510-20240308-20_40_54" "INDUCTION-18512-20240308-20_42_37" 283 | ## [3] "INDUCTION-18514-20240308-20_44_22" "INDUCTION-26-20201026-16_07_50" 284 | ## [5] "INDUCTION-2896-20180802-09_27_02" "INDUCTION-4188-20201116-10_53_39" 285 | ## [7] "INDUCTION-484-20171225-13_15_58" "INDUCTION-485-20171225-14_12_14" 286 | ## [9] "INDUCTION-486-20171225-14_33_46" "INDUCTION-487-20171225-14_33_46" 287 | 288 | ``` r 289 | # 4. Define treatment groups 290 | treatment = c("2020_species", "2018_species", "2020_species", rep("2017_species", 4), rep("2024_species", 3)) 291 | 292 | 293 | # 5. Assign groups (use set_category_base()) 294 | pca_df = set_category_base( 295 | df = pca_df, 296 | category = treatment, 297 | new_col_name = "SOURCE" 298 | ) 299 | 300 | # Verify groups 301 | unique(pca_df$SOURCE) 302 | ``` 303 | 304 | ## [1] "2020_species" "2018_species" "2017_species" "2024_species" 305 | 306 | ``` r 307 | # 6. Run PCA (no changes) 308 | df = pca_df[, -1] # Remove SOURCE column 309 | final_pca = PCA(df, graph = FALSE) 310 | 311 | # 7. Error-Free Plots (use ggtitle() instead of main=) 312 | # Scree Plot (fixed: no main argument) 313 | fviz_eig(final_pca, addlabels = TRUE) + 314 | ggtitle("Scree Plot (Continuous Signal)") + 315 | theme_minimal() 316 | ``` 317 | 318 | ![](README_files/figure-gfm/unnamed-chunk-6-1.png) 319 | 320 | ``` r 321 | # Variable Loadings Plot (fixed: no main argument) 322 | fviz_pca_var(final_pca) + 323 | ggtitle("Variable Loadings (Continuous Signal)") + 324 | theme_minimal() 325 | ``` 326 | 327 | ![](README_files/figure-gfm/unnamed-chunk-6-2.png) 328 | 329 | ``` r 330 | # Sample Scores Plot (fixed: no main argument) 331 | fviz_pca_ind( 332 | final_pca, 333 | repel = TRUE, 334 | col.ind = pca_df$SOURCE, 335 | palette = c("#E74C3C", "#3498DB", "#2ECC71", "#CDAD00"), # 2017=Red, 2018=Blue, 2020=Green 336 | legend.title = element_text("Treatment Group") # Proper legend title formatting 337 | ) + 338 | ggtitle("Sample Scores (Continuous Signal)") + 339 | theme_minimal() 340 | ``` 341 | 342 | ![](README_files/figure-gfm/unnamed-chunk-6-3.png) 343 | 344 | Full help: `?jiptest` 345 | 346 | ## Citation 347 | 348 | If you use jiptest in a publication please cite: 349 | 350 | @misc{jiptest2025, author = {Zhu, Jiedong}, title = {jiptest: JIP-test 351 | analysis for LI-6800 OJIP curves}, year = {2025}, url = 352 | {} } 353 | 354 | Also cite the foundational JIP-test reference: 355 | 356 | @article{Strasser2000, title={The fluorescence transient as a tool to 357 | characterize and screen photosynthetic samples}, author={ Strasser, R. 358 | J. and Srivastava, A. and Tsimilli-Michael, M. }, journal={Probing 359 | Photosynthesis Mechanisms Regulation & Adaptation}, year={2000}, } 360 | 361 | ## License 362 | 363 | MIT © Jiedong Zhu 364 | --------------------------------------------------------------------------------