From ff2adc607982dadee64c7c5b63d9a81a485ead98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:32:59 +0000 Subject: [PATCH 1/6] Add Analyte Comparison subsection to slide types tree (#1174) Add 'Analyte Comparison' as a new subsection under Summary Slides in .build_slide_types(). This makes it available in the slide customisation modal when exporting results. Co-authored-by: Ona --- inst/shiny/modules/tab_nca/zip.R | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/inst/shiny/modules/tab_nca/zip.R b/inst/shiny/modules/tab_nca/zip.R index e70b08f5d..bf74fcbd2 100644 --- a/inst/shiny/modules/tab_nca/zip.R +++ b/inst/shiny/modules/tab_nca/zip.R @@ -56,10 +56,11 @@ zip_ui <- function(id) { id = "summary", label = "Summary Slides", sections = list( - list(id = "meanplot", label = "Mean Plots"), - list(id = "linplot", label = "Spaghetti / Group Plot"), - list(id = "boxplot", label = "Box Plot"), - list(id = "statistics", label = "Summary Statistics") + list(id = "meanplot", label = "Mean Plots"), + list(id = "analyte_comparison", label = "Analyte Comparison"), + list(id = "linplot", label = "Spaghetti / Group Plot"), + list(id = "boxplot", label = "Box Plot"), + list(id = "statistics", label = "Summary Statistics") ) ) ) From 383496a1c749587d4c0a1b803c15b7eb75efdad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:33:31 +0000 Subject: [PATCH 2/6] Generate analyte comparison mean plots in get_dose_esc_results (#1174) Add exploration_meanplot call with facet_by = c('DOSETRT', PARAM) to produce analyte comparison plots for each dose group. The plot is stored as 'analyte_comparison' in the group output list. Co-authored-by: Ona --- inst/shiny/functions/zip-utils.R | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/inst/shiny/functions/zip-utils.R b/inst/shiny/functions/zip-utils.R index e6ce38f9a..5e4f1ad08 100644 --- a/inst/shiny/functions/zip-utils.R +++ b/inst/shiny/functions/zip-utils.R @@ -183,6 +183,19 @@ get_dose_esc_results <- function( sd_max = TRUE ) + # Analyte comparison: mean plot faceted by DOSETRT × PARAM + analyte_comparison_i <- exploration_meanplot( + pknca_data = o_nca_i$data, + color_by = group_by_vars, + facet_by = c("DOSETRT", analyte_col), + filtering_list = list( + PCSPEC = unique(d_conc_i[[pcspec_col]]), + ATPTREF = unique(d_conc_i[[profile_col]]) + ), + ylog_scale = TRUE, + sd_max = TRUE + ) + stats_i <- calculate_summary_stats( data = merge(o_res_i, d_conc_i[, c(group_vars(o_nca), facet_vars)]), input_groups = facet_vars @@ -262,6 +275,7 @@ get_dose_esc_results <- function( output_list[[paste0("Group_", i)]] <- list( linplot = linplot_i, meanplot = meanplot_i, + analyte_comparison = analyte_comparison_i, statistics = stats_i, boxplot = boxplots_i, info = info_i, From 172eb79d2c5098ec246a78a49a0136efd76e86ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:34:03 +0000 Subject: [PATCH 3/6] Add analyte comparison slides to PPTX export (#1174) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Insert analyte comparison plot slide (faceted by DOSETRT × PARAM) into the group summary section, between the main mean plot and the spaghetti plot. Update has_summary check and slide count accordingly. Co-authored-by: Ona --- R/officer-utils.R | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/R/officer-utils.R b/R/officer-utils.R index 5d361e31d..e014925ac 100644 --- a/R/officer-utils.R +++ b/R/officer-utils.R @@ -185,13 +185,19 @@ add_pptx_sl_plot <- function(pptx, plot) { subtitle = paste(group_data$group)) %>% officer::ph_slidelink(ph_label = "Footer Placeholder 3", slide_index = (lst_group_slide + 1)) pptx <- .add_pptx_main_summary_slide(pptx, group_data, i, in_sections) + # Analyte comparison slide (mean plot faceted by DOSETRT × PARAM) + has_analyte_cmp <- in_sections("analyte_comparison") && !is.null(group_data$analyte_comparison) + if (has_analyte_cmp) { + pptx <- add_pptx_sl_plot(pptx, group_data$analyte_comparison) + } pptx <- pptx %>% { if (in_sections("linplot")) add_pptx_sl_plot(., group_data$linplot) else . } bp_result <- .add_pptx_boxplot_slides(pptx, group_data, in_sections) pptx <- bp_result$pptx n_main_slides <- as.integer(in_sections("meanplot") || in_sections("statistics")) - n_summary_slides <- 1L + n_main_slides + as.integer(in_sections("linplot")) + bp_result$n_slides + n_summary_slides <- 1L + n_main_slides + as.integer(has_analyte_cmp) + + as.integer(in_sections("linplot")) + bp_result$n_slides list(pptx = pptx, n_summary_slides = n_summary_slides) } @@ -211,7 +217,7 @@ add_pptx_sl_plot <- function(pptx, plot) { pptx <- ind_result$pptx n_ind_slides <- ind_result$n_slides has_summary <- in_sections("meanplot") || in_sections("statistics") || - in_sections("linplot") || in_sections("boxplot") + in_sections("analyte_comparison") || in_sections("linplot") || in_sections("boxplot") if (has_summary) { summary_result <- .add_pptx_group_summary(pptx, group_data, i, in_sections, lst_group_slide) pptx <- summary_result$pptx From 8ae51c0101290328d012754d85796549f83e5253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:34:31 +0000 Subject: [PATCH 4/6] Add analyte comparison slides to QMD export (#1174) Insert analyte comparison plot slide into the QMD summary section, between the mean plot and the spaghetti plot. Update has_summary check to include analyte_comparison. Co-authored-by: Ona --- R/quarto-utils.R | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/R/quarto-utils.R b/R/quarto-utils.R index 3ccd19238..a75a79a82 100644 --- a/R/quarto-utils.R +++ b/R/quarto-utils.R @@ -160,6 +160,12 @@ add_qmd_sl_plot <- function(quarto_path, plot, use_plotly = FALSE) { use_plotly = use_plotly ) } + if (in_sections("analyte_comparison") && + !is.null(res_dose_slides[[i]]$analyte_comparison)) { + add_qmd_sl_plot(quarto_path = quarto_path, + plot = paste0("res_dose_slides[[", i, "]]$analyte_comparison"), + use_plotly = use_plotly) + } if (in_sections("linplot")) { add_qmd_sl_plot(quarto_path = quarto_path, plot = paste0("res_dose_slides[[", i, "]]$linplot"), @@ -260,7 +266,7 @@ create_qmd_dose_slides <- function(res_dose_slides, quarto_path, title, use_plot # Mean plot + statistics block has_summary <- in_sections("meanplot") || in_sections("statistics") || - in_sections("linplot") || in_sections("boxplot") + in_sections("analyte_comparison") || in_sections("linplot") || in_sections("boxplot") has_individual <- in_sections("ind_plots") || in_sections("ind_params") if (has_summary) .add_qmd_summary_slides(quarto_path, res_dose_slides, in_sections, use_plotly) From 129ab067020e3719a9145deabb669fb1a852a4f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:35:47 +0000 Subject: [PATCH 5/6] Add tests for analyte comparison slides in PPTX and QMD (#1174) Test that the analyte_comparison slide is included/excluded based on slide_sections selection, and that it works with NULL slide_sections (backwards compat). Also add analyte_comparison to base_slides fixture in both test files. Co-authored-by: Ona --- tests/testthat/test-officer-utils.R | 56 +++++++++++++++++++++++++++++ tests/testthat/test-quarto-utils.R | 44 +++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/tests/testthat/test-officer-utils.R b/tests/testthat/test-officer-utils.R index 9ad743fd5..df8c02288 100644 --- a/tests/testthat/test-officer-utils.R +++ b/tests/testthat/test-officer-utils.R @@ -7,6 +7,7 @@ describe("create_pptx_dose_slides", { group = "A", statistics = data.frame(stat = "Mean", value = 1), meanplot = ggplot2::ggplot(), + analyte_comparison = ggplot2::ggplot(), linplot = ggplot2::ggplot(), boxplot = list(AUCIFO = ggplot2::ggplot()), ind_params = list(SUBJ01 = data.frame(param = "CMAX", value = 1)), @@ -187,4 +188,59 @@ describe("create_pptx_dose_slides", { expect_lt(length(officer::read_pptx(out_one)), length(officer::read_pptx(out_both))) }) + + it("includes analyte comparison slide when analyte_comparison is in slide_sections", { + slides <- base_slides + attr(slides, "slide_sections") <- c( + "meanplot", "analyte_comparison", "statistics", "ind_plots", "ind_params" + ) + out_with <- tempfile(fileext = ".pptx") + create_pptx_dose_slides(slides, out_with, "NCA", template) + + slides_without <- base_slides + attr(slides_without, "slide_sections") <- c( + "meanplot", "statistics", "ind_plots", "ind_params" + ) + out_without <- tempfile(fileext = ".pptx") + create_pptx_dose_slides(slides_without, out_without, "NCA", template) + + expect_gt(length(officer::read_pptx(out_with)), length(officer::read_pptx(out_without))) + }) + + it("omits analyte comparison slide when analyte_comparison is not in slide_sections", { + slides <- base_slides + attr(slides, "slide_sections") <- c("meanplot", "statistics", "ind_plots", "ind_params") + out <- tempfile(fileext = ".pptx") + create_pptx_dose_slides(slides, out, "NCA", template) + + slides_with <- base_slides + attr(slides_with, "slide_sections") <- c( + "meanplot", "analyte_comparison", "statistics", "ind_plots", "ind_params" + ) + out_with <- tempfile(fileext = ".pptx") + create_pptx_dose_slides(slides_with, out_with, "NCA", template) + + expect_lt(length(officer::read_pptx(out)), length(officer::read_pptx(out_with))) + }) + + it("includes analyte comparison slide when slide_sections is NULL (all selected)", { + out <- tempfile(fileext = ".pptx") + create_pptx_dose_slides(base_slides, out, "NCA", template) + + slides_no_ac <- base_slides + slides_no_ac[[1]]$analyte_comparison <- NULL + out_no_ac <- tempfile(fileext = ".pptx") + create_pptx_dose_slides(slides_no_ac, out_no_ac, "NCA", template) + + expect_gt(length(officer::read_pptx(out)), length(officer::read_pptx(out_no_ac))) + }) + + it("creates summary section when only analyte_comparison is selected", { + slides <- base_slides + attr(slides, "slide_sections") <- c("analyte_comparison") + out <- tempfile(fileext = ".pptx") + create_pptx_dose_slides(slides, out, "NCA", template) + # Title slide + group info + analyte comparison + Extra Figures title = at least 4 + expect_gte(length(officer::read_pptx(out)), 4) + }) }) diff --git a/tests/testthat/test-quarto-utils.R b/tests/testthat/test-quarto-utils.R index 2d33f8e55..30499f4de 100644 --- a/tests/testthat/test-quarto-utils.R +++ b/tests/testthat/test-quarto-utils.R @@ -4,6 +4,7 @@ describe("create_qmd_dose_slides", { info = data.frame(group = "A"), statistics = data.frame(stat = "Mean", value = 1), meanplot = ggplot2::ggplot(), + analyte_comparison = ggplot2::ggplot(), linplot = ggplot2::ggplot(), boxplot = list(AUCIFO = ggplot2::ggplot()), ind_params = list(SUBJ01 = data.frame(param = "CMAX", value = 1)), @@ -322,4 +323,47 @@ describe("create_qmd_dose_slides", { expect_true(grepl("Group 1 (Individual)", content, fixed = TRUE)) expect_false(grepl("Group 2 (Individual)", content, fixed = TRUE)) }) + + it("includes analyte_comparison expression when analyte_comparison is in slide_sections", { + slides <- base_slides + attr(slides, "slide_sections") <- c("meanplot", "analyte_comparison", "statistics") + out_file <- tempfile(fileext = ".qmd") + + create_qmd_dose_slides(slides, out_file, "NCA Results", use_plotly = FALSE) + content <- paste(readLines(out_file, warn = FALSE), collapse = "\n") + + expect_true(grepl("analyte_comparison", content, fixed = TRUE)) + }) + + it("omits analyte_comparison expression when analyte_comparison is not in slide_sections", { + slides <- base_slides + attr(slides, "slide_sections") <- c("meanplot", "statistics", "ind_plots", "ind_params") + out_file <- tempfile(fileext = ".qmd") + + create_qmd_dose_slides(slides, out_file, "NCA Results", use_plotly = FALSE) + content <- paste(readLines(out_file, warn = FALSE), collapse = "\n") + + expect_false(grepl("analyte_comparison", content, fixed = TRUE)) + }) + + it("includes analyte_comparison when slide_sections is NULL (all selected)", { + out_file <- tempfile(fileext = ".qmd") + create_qmd_dose_slides(base_slides, out_file, "NCA Results", use_plotly = FALSE) + content <- paste(readLines(out_file, warn = FALSE), collapse = "\n") + + expect_true(grepl("analyte_comparison", content, fixed = TRUE)) + }) + + it("creates summary section when only analyte_comparison is selected", { + slides <- base_slides + attr(slides, "slide_sections") <- c("analyte_comparison") + out_file <- tempfile(fileext = ".qmd") + + create_qmd_dose_slides(slides, out_file, "NCA Results", use_plotly = FALSE) + content <- paste(readLines(out_file, warn = FALSE), collapse = "\n") + + expect_true(grepl("analyte_comparison", content, fixed = TRUE)) + expect_true(grepl("# Group 1", content, fixed = TRUE)) + expect_false(grepl("meanplot", content, fixed = TRUE)) + }) }) From 90566f46c71087e41c160855b122d15d6f88a5ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:36:14 +0000 Subject: [PATCH 6/6] Bump version to 0.1.0.9137 and update NEWS.md (#1174) Co-authored-by: Ona --- DESCRIPTION | 2 +- NEWS.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 415e600fa..025547c4e 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: aNCA Title: (Pre-)Clinical NCA in a Dynamic Shiny App -Version: 0.1.0.9136 +Version: 0.1.0.9137 Authors@R: c( person("Ercan", "Suekuer", email = "ercan.suekuer@roche.com", role = "aut", comment = c(ORCID = "0009-0001-1626-1526")), diff --git a/NEWS.md b/NEWS.md index be06c547c..dad18db09 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,7 @@ ## Features added +* Added "Analyte Comparison" slide type under Summary Slides in the export customisation modal. Generates mean plots faceted by DOSETRT × PARAM for both PPTX and QMD exports (#1174) * Added "Min. Points for Half-life" setting in NCA > Settings > General Settings, allowing users to configure PKNCA's `min.hl.points` option (range 2–10, default 3) (#1155) * Settings version control: single YAML file stores multiple versions with metadata (timestamp, comment, dataset, aNCA version, active tab). Save button in header, version selection modal on upload, version delete support (#1103) * Right-side sidebars (Exploration, NCA Parameter Plots, TLG) can now be resized by dragging the left edge. Default width increased to 250px, left nav sidebar reduced to 150px (#1156)