├── 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 | 
214 |
215 | ``` r
216 | plot(ojip_single, use_PAM = TRUE, main = "OJIP Curve (PAM Signal)")
217 | ```
218 |
219 | 
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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------