Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3922f89
feat: add slide_sections filtering and generic additional_analysis to…
bachapman Mar 6, 2026
97d9ae8
feat: add slide_sections filtering and generic additional_analysis to…
bachapman Mar 6, 2026
7a38383
feat: thread slide_sections and additional_analysis attributes throug…
bachapman Mar 6, 2026
b9fbe70
feat: add slide content selection modal when exporting slide decks
bachapman Mar 6, 2026
fdba22a
feat: ZIP modal UX polish — Cancel button, easyClose=FALSE, instructi…
bachapman Mar 6, 2026
371456f
feat: ZIP modal polish — primary download buttons, modal-footer style…
bachapman Mar 6, 2026
58f25e3
fix: replace hidden downloadButton click with visible download modal …
bachapman Mar 9, 2026
06de7cb
feat: inline export validation — red bubble, button disable, empty-tr…
bachapman Mar 9, 2026
b11427d
fix: box plot (and linplot) not rendering when selected alone in slid…
bachapman Mar 9, 2026
f0e4f16
feat: multi-parameter boxplots and box plot UI controls in slide export
bachapman Mar 9, 2026
986e171
fix: pre-merge corrections for #972 slide export feature
bachapman Mar 10, 2026
3e7b42c
Merge branch 'main' into 972-enhancement-options-to-customise-outputs…
bachapman Mar 10, 2026
0a85960
fix: resolve lintr violations in zip export module
bachapman Mar 10, 2026
f3e7525
fix: pre-merge corrections for officer-utils and WORDLIST
bachapman Mar 10, 2026
c273c19
docs: regenerate roxygen man pages for new officer-utils helpers
bachapman Mar 10, 2026
6375ccc
fix: update WORDLIST — add lst, remove stale entries
bachapman Mar 10, 2026
f9f6f06
fix: preserve results_slides_outputs.rda in zip export
bachapman Mar 10, 2026
eb8c6ce
feat: restructure QMD revealjs output to match PPTX layout with TOC
bachapman Mar 11, 2026
7f110fe
docs: regenerate roxygen man page for .add_qmd_group_section_header
bachapman Mar 11, 2026
088b723
Merge branch 'main' into 972-enhancement-options-to-customise-outputs…
bachapman Mar 12, 2026
0f9c164
docs: mark internal dot-functions with @noRd to suppress Rd generation
bachapman Mar 13, 2026
45593f5
fix: include results_slides_outputs.rda in zip when qmd format is sel…
bachapman Mar 13, 2026
5d2f3c3
feat: stage 2 modal validation with conditional param visibility
bachapman Mar 13, 2026
c6cafc4
fix: also include rda ext and results_slides_outputs in fnames pattern
bachapman Mar 16, 2026
c05c907
Merge branch 'main' into 972-enhancement-options-to-customise-outputs…
bachapman Mar 30, 2026
f0b77be
Remove extra blank line in NEWS.md
bachapman Mar 30, 2026
62c2dd6
Fix missing individual slides for metabolite/secondary groups in PPT …
bachapman Mar 30, 2026
a507664
Hide additional_analysis from export tree when no results are present
bachapman Mar 30, 2026
8ea28d5
Merge branch 'main' into 972-enhancement-options-to-customise-outputs…
bachapman Apr 1, 2026
721aa85
Updated version to 0.1.0.9136
bachapman Apr 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ Imports:
ggplot2,
glue,
magrittr,
methods,
PKNCA (>= 0.12.1),
plotly (>= 4.11.0),
purrr,
Expand Down
5 changes: 2 additions & 3 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,8 @@ importFrom(dplyr,summarise)
importFrom(dplyr,sym)
importFrom(dplyr,ungroup)
importFrom(dplyr,where)
importFrom(formatters,"var_labels<-")
importFrom(formatters,`var_labels<-`)
importFrom(formatters,var_labels)
importFrom(glue,glue)
importFrom(ggplot2,aes)
importFrom(ggplot2,facet_wrap)
importFrom(ggplot2,geom_point)
Expand All @@ -129,9 +128,9 @@ importFrom(ggplot2,scale_colour_manual)
importFrom(ggplot2,scale_shape_manual)
importFrom(ggplot2,scale_x_continuous)
importFrom(ggplot2,theme_bw)
importFrom(glue,glue)
importFrom(grid,convertUnit)
importFrom(magrittr,`%>%`)
importFrom(methods,is)
importFrom(plotly,add_lines)
importFrom(plotly,add_trace)
importFrom(plotly,ggplotly)
Expand Down
7 changes: 5 additions & 2 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,13 @@
* Add x/y axis limits for the exploration plots (#817) and facet titles including subject count (#894)
* Settings upload and processing is flexible, so non-data specific template settings can be uploaded (#993)
* Mapping will allow custom numeric input values instead of columns for `ADOSEDUR` and `TRTRINT` (#1051)
* Help buttons have been included/updated for most App sections: `Parameter Selection`, `Slope Selector`, `Additional Analysis` and `Partial Interval calculations` (#975)
* Help buttons have been included/updated for most App sections: `Parameter Selection`,
`Slope Selector`, `Additional Analysis` and `Partial Interval calculations` (#975)
* Removed `methods`, `scales`, and `stringr` from package dependencies, replacing all usages with base R equivalents (#1108)
* SelectInputs updated using a new function to ensure all widgets include variable labels. (#899)

* Export modal now allows users to select which slide sections (mean plots, statistics,
line plots, box plots, individual plots/parameters, additional analysis) to include in
PPTX and HTML exports; box plot parameters are also configurable (#972)

## Bugs fixed
* ZIP folder with results will now include the exploration tab outputs: individual plots, mean plots (#794)
Expand Down
222 changes: 178 additions & 44 deletions R/officer-utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ add_pptx_sl_title <- function(pptx, title) {
add_pptx_sl_plottable <- function(pptx, df, plot) {
officer::add_slide(pptx, layout = "Content with Caption") %>%
officer::ph_with(value = plot, location = "Content Placeholder 1") %>%
officer::ph_with(
value = flextable::flextable(df, cwidth = 1), location = "Table Placeholder 1"
)
officer::ph_with(value = flextable::flextable(df, cwidth = 1), location = "Table Placeholder 1")
}

#' Add a slide with a table only
Expand Down Expand Up @@ -75,6 +73,157 @@ add_pptx_sl_plot <- function(pptx, plot) {
officer::ph_with(value = plot, location = "Picture Placeholder 2")
}

#' Add individual-subject slides for one dose group to a pptx object
#' @param pptx rpptx object
#' @param group_data Single element from res_dose_slides
#' @param group_index Integer index of the dose group
#' @param in_sections Function(id) returning TRUE when id is selected
#' @return List with updated pptx and n_slides count
#' @keywords internal
#' @noRd
.add_pptx_ind_slides <- function(pptx, group_data, group_index, in_sections) {
if (!in_sections("ind_plots") && !in_sections("ind_params")) {
return(list(pptx = pptx, n_slides = 0))
}
if (length(group_data$ind_params) == 0 && length(group_data$ind_plots) == 0) {
return(list(pptx = pptx, n_slides = 0))
}
pptx <- add_pptx_sl_table(
pptx, group_data$info,
title = paste0("Group ", group_index, " (individual)"),
subtitle = paste(group_data$group),
footer = ""
)
pptx <- purrr::reduce(
names(group_data$ind_params),
function(pptx, subj) {
if (in_sections("ind_plots") && in_sections("ind_params")) {
add_pptx_sl_plottable(pptx,
df = group_data$ind_params[[subj]],
plot = group_data$ind_plots[[subj]])
} else if (in_sections("ind_plots")) {
add_pptx_sl_plot(pptx, plot = group_data$ind_plots[[subj]])
} else {
add_pptx_sl_table(pptx, df = group_data$ind_params[[subj]], footer = "")
}
},
.init = pptx
)
n_slides <- 1 + length(group_data$ind_params)
list(pptx = pptx, n_slides = n_slides)
}

#' Filter an additional_analysis list to non-empty data frames,
#' optionally restricted to slide_sections
#' @param additional_analysis Named list of data frames
#' @param slide_sections Character vector of selected section IDs, or NULL for all
#' @return Filtered named list
#' @keywords internal
#' @noRd
.filter_additional_analysis <- function(additional_analysis, slide_sections) {
if (is.null(additional_analysis)) return(NULL)
result <- Filter(function(x) is.data.frame(x) && nrow(x) > 0, additional_analysis)
if (!is.null(slide_sections)) {
result <- result[names(result) %in% slide_sections]
}
result
}

#' Add the main summary slide (meanplot / statistics / both) for a dose group
#' @param pptx An officer pptx object.
#' @param group_data One element of res_dose_slides (a dose group).
#' @param i Integer index of this dose group.
#' @param in_sections Function(id) returning TRUE when the section id is selected.
#' @return Updated pptx object.
#' @keywords internal
#' @noRd
.add_pptx_main_summary_slide <- function(pptx, group_data, i, in_sections) {
if (in_sections("meanplot") && in_sections("statistics")) {
add_pptx_sl_plottable(pptx, df = group_data$statistics, plot = group_data$meanplot)
} else if (in_sections("meanplot")) {
add_pptx_sl_plot(pptx, plot = group_data$meanplot)
} else if (in_sections("statistics")) {
add_pptx_sl_table(pptx,
df = group_data$statistics,
title = paste0("Group ", i, " Summary Statistics"),
footer = "")
} else {
pptx
}
}

#' Add boxplot slides for a dose group and return updated pptx and slide count
#' @param pptx An officer pptx object.
#' @param group_data One element of res_dose_slides (a dose group).
#' @param in_sections Function(id) returning TRUE when the section id is selected.
#' @return List with elements `pptx`, `n_slides`.
#' @keywords internal
#' @noRd
.add_pptx_boxplot_slides <- function(pptx, group_data, in_sections) {
boxplots_i <- group_data$boxplot
if (!in_sections("boxplot") || !is.list(boxplots_i)) {
return(list(pptx = pptx, n_slides = 0L))
}
for (bp_plot in boxplots_i) {
if (!is.null(bp_plot)) pptx <- add_pptx_sl_plot(pptx, bp_plot)
}
n_slides <- sum(vapply(boxplots_i, Negate(is.null), logical(1)))
list(pptx = pptx, n_slides = n_slides)
}

#' Add summary slides for one dose group to a pptx object
#' @param pptx An officer pptx object.
#' @param group_data One element of res_dose_slides (a dose group).
#' @param i Integer index of this dose group.
#' @param in_sections Function(id) returning TRUE when the section id is selected.
#' @param lst_group_slide Integer slide index at the start of this group.
#' @return List with elements `pptx`, `n_summary_slides`.
#' @keywords internal
#' @noRd
.add_pptx_group_summary <- function(pptx, group_data, i, in_sections, lst_group_slide) {
pptx <- add_pptx_sl_table(pptx, group_data$info, paste0("Group ", i, " Summary"),
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)
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
list(pptx = pptx, n_summary_slides = n_summary_slides)
}

#' Process one dose group's slides, returning updated pptx, lst_group_slide, group_slides
#' @param pptx An officer pptx object.
#' @param group_data One element of res_dose_slides (a dose group).
#' @param i Integer index of this dose group.
#' @param in_sections Function(id) returning TRUE when the section id is selected.
#' @param lst_group_slide Current slide index counter.
#' @param group_slides Integer vector of summary slide indices accumulated so far.
#' @return List with elements `pptx`, `lst_group_slide`, `group_slides`.
#' @keywords internal
#' @noRd
.process_pptx_group_slides <- function(pptx, group_data, i, in_sections,
lst_group_slide, group_slides) {
ind_result <- .add_pptx_ind_slides(pptx, group_data, i, in_sections)
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")
if (has_summary) {
summary_result <- .add_pptx_group_summary(pptx, group_data, i, in_sections, lst_group_slide)
pptx <- summary_result$pptx
n_summary_slides <- summary_result$n_summary_slides
lst_group_slide <- lst_group_slide + n_ind_slides + n_summary_slides
group_slides <- c(group_slides, (lst_group_slide - n_summary_slides + 1):(lst_group_slide))
} else {
lst_group_slide <- lst_group_slide + n_ind_slides
}
list(pptx = pptx, lst_group_slide = lst_group_slide, group_slides = group_slides)
}

#' Create a PowerPoint presentation with dose escalation results, including main and extra figures
#' Adds slides for summary tables, mean plots, line plots, and individual subject results
#' @param res_dose_slides List of results for each dose group
Expand All @@ -92,58 +241,43 @@ create_pptx_dose_slides <- function(res_dose_slides, path, title, template) {
}
}

slide_sections <- attr(res_dose_slides, "slide_sections")
additional_analysis <- attr(res_dose_slides, "additional_analysis")

in_sections <- function(id) is.null(slide_sections) || id %in% slide_sections

pptx <- create_pptx_doc(path, title, template)

lst_group_slide <- 1
group_slides <- numeric()
for (i in seq_along(res_dose_slides)) {
result <- .process_pptx_group_slides(pptx, res_dose_slides[[i]], i, in_sections,
lst_group_slide, group_slides)
pptx <- result$pptx
lst_group_slide <- result$lst_group_slide
group_slides <- result$group_slides
}

# Generate the individual figures
pptx <- add_pptx_sl_table(
pptx, res_dose_slides[[i]]$info,
title = paste0("Group ", i, " (individual)"),
subtitle = paste(res_dose_slides[[i]]$group),
footer = ""
)
if (length(group_slides) > 0) {
group_slides_rev <- rev(group_slides) + (seq_along(group_slides) - 1)
pptx <- purrr::reduce(
names(res_dose_slides[[i]]$ind_params),
function(pptx, subj) {
add_pptx_sl_plottable(
pptx,
df = res_dose_slides[[i]]$ind_params[[subj]],
plot = res_dose_slides[[i]]$ind_plots[[subj]]
)
},
group_slides_rev,
function(pptx, slide_index) officer::move_slide(pptx, index = slide_index, to = 2),
.init = pptx
)

# Generate summary figures and tables
pptx <- add_pptx_sl_table(pptx, res_dose_slides[[i]]$info, paste0("Group ", i, " Summary"),
subtitle = paste(res_dose_slides[[i]]$group)) %>%
officer::ph_slidelink(ph_label = "Footer Placeholder 3",
slide_index = (lst_group_slide + 1)) %>%
add_pptx_sl_plottable(
df = res_dose_slides[[i]]$statistics,
plot = res_dose_slides[[i]]$meanplot
) %>%
add_pptx_sl_plot(res_dose_slides[[i]]$linplot) %>%
add_pptx_sl_plot(res_dose_slides[[i]]$boxplot)

n_ind <- length(res_dose_slides[[i]]$ind_params)
lst_group_slide <- lst_group_slide + 1 + n_ind + 4
group_slides <- c(group_slides, (lst_group_slide - 3):(lst_group_slide))
}

group_slides_rev <- rev(group_slides) + (seq_along(group_slides) - 1)
pptx <- purrr::reduce(
group_slides_rev,
function(pptx, slide_index) officer::move_slide(pptx, index = slide_index, to = 2),
.init = pptx
)
pptx <- add_pptx_sl_title(pptx, "Extra Figures")
pptx <- officer::move_slide(
x = pptx, index = length(pptx), to = (length(group_slides) + 2)
)
pptx <- officer::move_slide(x = pptx, index = length(pptx), to = (length(group_slides) + 2))

# Add additional analysis slides generically
non_empty <- .filter_additional_analysis(additional_analysis, slide_sections)
if (length(non_empty) > 0) {
pptx <- add_pptx_sl_title(pptx, "Additional Analysis Figures")
for (name in names(non_empty)) {
label <- tools::toTitleCase(gsub("_", " ", name))
pptx <- add_pptx_sl_table(pptx, non_empty[[name]], title = label)
}
}

print(pptx, target = path)
invisible(TRUE)
Expand Down
Loading
Loading