From 72223bafcdeadbd38e304b023a462efe8f4b3ed3 Mon Sep 17 00:00:00 2001 From: Marco Ferrari <67416528+cabaJr@users.noreply.github.com> Date: Sun, 5 Mar 2023 23:36:12 +0000 Subject: [PATCH 1/3] Update fct_R6_Annotate.R --- R/fct_R6_Annotate.R | 2 -- 1 file changed, 2 deletions(-) diff --git a/R/fct_R6_Annotate.R b/R/fct_R6_Annotate.R index bb71007..ff7b72a 100644 --- a/R/fct_R6_Annotate.R +++ b/R/fct_R6_Annotate.R @@ -820,5 +820,3 @@ Annotate <- R6::R6Class("Annotate", ### ) ) - - From eaefc6de6dc967258f4fc420ccaf956103eb751a Mon Sep 17 00:00:00 2001 From: Marco Ferrari Date: Wed, 15 Apr 2026 00:07:35 +0100 Subject: [PATCH 2/3] future development --- FUTURE_DEVELOPMENT.md | 1333 +++++++++++++++++++++++++++++++++++++ dev_guide.Rmd | 721 ++++++++++++++++++++ maceR_Developer_Guide.pdf | Bin 0 -> 90163 bytes 3 files changed, 2054 insertions(+) create mode 100644 FUTURE_DEVELOPMENT.md create mode 100644 dev_guide.Rmd create mode 100644 maceR_Developer_Guide.pdf diff --git a/FUTURE_DEVELOPMENT.md b/FUTURE_DEVELOPMENT.md new file mode 100644 index 0000000..ee24dc3 --- /dev/null +++ b/FUTURE_DEVELOPMENT.md @@ -0,0 +1,1333 @@ +# maceR Future Development Roadmap + +**Created**: April 15, 2026 +**Document Purpose**: Track architectural improvements and refactoring opportunities +**Target Audience**: Development team planning future iterations + +--- + +## Overview + +This document outlines 10 major architectural improvements recommended for the maceR codebase. Each improvement is documented with current state, proposed solution, code examples, and implementation guidance. + +**Quick Links**: +- [High Priority Items](#high-priority-high-impact-low-effort) +- [Medium Priority Items](#medium-priority-medium-impact-medium-effort) +- [Lower Priority Items](#lower-priority-high-impact-higher-effort) + +--- + +## 1. Consolidate Environment Management + +### Current State +- Four separate environment pointers: `env1`, `env2`, `env3`, `env4` +- Stored in `App_settings` object +- Opaque access pattern: `App_settings$env4$Annotate$Actograms` +- Difficult to track ownership and dependencies + +### Problems +- Hard to understand data flow +- No type safety or validation +- Difficult to refactor safely +- Confusing for new developers + +### Proposed Solution +Create a unified `DataStore` R6 class that encapsulates all data objects. + +### Implementation Code +```r +# R/fct_R6_DataStore.R + +DataStore <- R6::R6Class("DataStore", + public = list( + raw_mice = NULL, + clean_mice = NULL, + tables = NULL, + annotate = NULL, + + initialize = function() { + self$raw_mice <- list() + self$clean_mice <- list() + self$tables <- Custom_tables$new() + self$annotate <- Annotate$new() + }, + + #' Get all raw data + get_raw_data = function() { + self$raw_mice + }, + + #' Get cleaned animals + get_clean_mice = function() { + self$clean_mice + }, + + #' Check if data is loaded + is_initialized = function() { + length(self$raw_mice) > 0 && length(self$clean_mice) > 0 + } + ) +) +``` + +### Usage Changes +```r +# Old: +env$env4$Annotate$Actograms + +# New: +App_settings$data_store$annotate$Actograms +``` + +### Migration Path +1. Create `DataStore` class +2. Update `load_data()` to populate `data_store` instead of `env1-4` +3. Update all modules to use `App_settings$data_store` instead of `App_settings$env*` +4. Remove old `env1-4` fields after testing +5. Update developer guide + +### Files to Modify +- `R/fct_R6_DataStore.R` (new) +- `R/fct_data_loader.R` +- `R/app_server.R` +- `R/mod_*.R` (all modules) +- Documentation + +### Benefits +- Self-documenting code +- Type-safe access patterns +- Easier testing and refactoring +- Clear dependency graph + +### Effort Estimate +**Time**: 2-3 hours +**Complexity**: Medium +**Risk**: Low (non-breaking if done carefully) + +--- + +## 2. Add Data Validation Layer + +### Current State +- Basic file format checking in `mod_Input_DF` +- No schema validation +- Errors occur downstream during analysis +- Poor error messages for users + +### Problems +- Invalid data reaches analysis functions +- Hard to diagnose issues +- Prevents batch processing +- User frustration + +### Proposed Solution +Create comprehensive validation framework with schema definitions. + +### Implementation Code +```r +# R/utils_validation.R + +#' Define data schema +create_schema <- function() { + list( + activity_file = list( + required_columns = c("timepoint", "activity"), + column_types = c(timepoint = "numeric", activity = "numeric"), + constraints = list( + timepoint_ascending = function(x) all(diff(x) >= 0), + activity_non_negative = function(x) all(x >= 0) + ) + ), + metadata_file = list( + required_columns = c("id", "Sex", "Genotype", "Cabinet"), + column_types = c( + id = "character", + Sex = c("M", "F"), + Genotype = "character", + Cabinet = "integer" + ) + ) + ) +} + +#' Validate data against schema +validate_data <- function(df, schema_type, schema) { + errors <- list() + + # Check required columns + missing_cols <- setdiff(schema[[schema_type]]$required_columns, names(df)) + if (length(missing_cols) > 0) { + errors$missing_columns <- missing_cols + } + + # Check column types + for (col in names(schema[[schema_type]]$column_types)) { + expected_type <- schema[[schema_type]]$column_types[[col]] + if (is.character(expected_type)) { + if (expected_type == "numeric" && !is.numeric(df[[col]])) { + errors$type_mismatch <- c(errors$type_mismatch, col) + } + } else if (is.character(expected_type)) { + # Factor: check valid values + invalid_vals <- setdiff(df[[col]], expected_type) + if (length(invalid_vals) > 0) { + errors[[paste0(col, "_invalid")]] <- invalid_vals + } + } + } + + # Check constraints + constraints <- schema[[schema_type]]$constraints + for (constraint_name in names(constraints)) { + constraint_fn <- constraints[[constraint_name]] + if (!constraint_fn(df[[names(schema[[schema_type]]$column_types)[1]]])) { + errors[[constraint_name]] <- TRUE + } + } + + if (length(errors) > 0) { + stop("Validation failed:\n", + paste(names(errors), collapse = "\n")) + } + + TRUE +} + +#' Create user-friendly error message +format_validation_error <- function(errors) { + messages <- list() + if (!is.null(errors$missing_columns)) { + messages$missing <- sprintf( + "Missing required columns: %s", + paste(errors$missing_columns, collapse = ", ") + ) + } + if (!is.null(errors$type_mismatch)) { + messages$type <- sprintf( + "Invalid data types in columns: %s", + paste(errors$type_mismatch, collapse = ", ") + ) + } + + paste(unlist(messages), collapse = "\n") +} +``` + +### Usage in mod_Input_DF +```r +observeEvent(input$upload_files, { + schema <- create_schema() + + tryCatch({ + validate_data(activity_data, "activity_file", schema) + validate_data(metadata, "metadata_file", schema) + # Proceed with upload + }, error = function(e) { + showModal(modalDialog( + title = "Data Validation Error", + format_validation_error(e$message), + easyClose = TRUE + )) + }) +}) +``` + +### Files to Modify +- `R/utils_validation.R` (new) +- `R/mod_Input_DF.R` +- `R/fct_data_loader.R` + +### Benefits +- Fail fast with clear errors +- Better user experience +- Enables automated testing +- Prevents downstream crashes +- Supports batch validation + +### Effort Estimate +**Time**: 3-4 hours +**Complexity**: Medium +**Risk**: Low + +--- + +## 3. Implement Caching/Memoization + +### Current State +- All computations run fresh each time +- Periodograms recalculated on every analysis run +- No mechanism to cache expensive operations +- Slow interaction when user refines parameters + +### Problems +- Poor performance with large datasets +- Unnecessary CPU usage +- Laggy UI response +- Users may think app froze + +### Proposed Solution +Add memoization layer for analysis computations. + +### Implementation Code +```r +# R/fct_cache.R + +CacheManager <- R6::R6Class("CacheManager", + public = list( + cache = list(), + max_size = 1000, # MB + current_size = 0, + + initialize = function(max_size_mb = 1000) { + self$max_size <- max_size_mb * 1024 * 1024 # Convert to bytes + }, + + #' Generate cache key from parameters + make_key = function(data_id, analysis_type, params) { + params_str <- paste(names(params), params, + sep = "=", collapse = "|") + key <- digest::digest(paste(data_id, analysis_type, + params_str, sep = ":")) + key + }, + + #' Get cached result + get = function(key) { + if (key %in% names(self$cache)) { + self$cache[[key]]$result + } else { + NULL + } + }, + + #' Store result in cache + store = function(key, result, metadata = NULL) { + size <- object.size(result) + + # Check size limit + if (self$current_size + size > self$max_size) { + self$evict_oldest() + } + + self$cache[[key]] <- list( + result = result, + timestamp = Sys.time(), + metadata = metadata, + size = size + ) + self$current_size <- self$current_size + size + }, + + #' Remove oldest entry + evict_oldest = function() { + if (length(self$cache) == 0) return() + + timestamps <- sapply(self$cache, function(x) x$timestamp) + oldest_key <- names(timestamps)[which.min(timestamps)] + + self$current_size <- self$current_size - self$cache[[oldest_key]]$size + self$cache[[oldest_key]] <- NULL + }, + + #' Clear all cache + clear = function() { + self$cache <- list() + self$current_size <- 0 + } + ) +) + +# Wrapper function for cached analysis +cached_analysis <- function(data, analysis_type, params, + cache_mgr, analysis_fn) { + key <- cache_mgr$make_key( + digest::digest(data), + analysis_type, + params + ) + + # Try to get from cache + cached_result <- cache_mgr$get(key) + if (!is.null(cached_result)) { + message("Using cached result") + return(cached_result) + } + + # Compute if not cached + message("Computing (this may take a moment)...") + result <- analysis_fn(data, analysis_type, params) + + # Store in cache + cache_mgr$store(key, result, list( + data_id = digest::digest(data), + analysis_type = analysis_type + )) + + result +} +``` + +### Integration with Annotate +```r +# Add cache to App_settings initialization +App_settings$cache <- CacheManager$new(max_size_mb = 500) + +# In analysis modules: +Annotate$plot_periodogram <- function(env, params) { + result <- cached_analysis( + data = env$env3$Custom_tables$periodograms[[1]], + analysis_type = "periodogram", + params = params, + cache_mgr = App_settings$cache, + analysis_fn = function(data, type, p) { + # Original plotting code + zeitgebr::periodogram(data) + } + ) + self$period_plots$Per1 <- result +} +``` + +### Files to Modify +- `R/fct_cache.R` (new) +- `R/fct_R6_App_settings.R` (add cache field) +- `R/fct_R6_Annotate.R` (wrap compute functions) +- `DESCRIPTION` (add digest dependency) + +### Benefits +- Faster UI response (100-1000x for repeated operations) +- Reduced CPU usage +- Better user experience +- Automatic memory management + +### Effort Estimate +**Time**: 4-5 hours +**Complexity**: Medium +**Risk**: Low (errors don't break app, just cause recomputation) + +--- + +## 4. Add Centralized Logging/Error Handling + +### Current State +- Errors scattered throughout the app +- No audit trail or logging +- Difficult to debug production issues +- No usage analytics + +### Problems +- Hard to troubleshoot user issues +- No record of what went wrong +- Silent failures possible +- No performance metrics + +### Proposed Solution +Implement centralized logging system. + +### Implementation Code +```r +# R/fct_logger.R + +Logger <- R6::R6Class("Logger", + public = list( + log_file = NULL, + level = "INFO", + + initialize = function(log_path = NULL, level = "INFO") { + if (is.null(log_path)) { + log_path <- file.path(tempdir(), "maceR.log") + } + self$log_file <- log_path + self$level <- level + + # Create header + self$write(sprintf( + "=== maceR Session Start: %s ===\n", + format(Sys.time(), "%Y-%m-%d %H:%M:%S") + )) + }, + + write = function(message) { + cat(message, "\n", file = self$log_file, append = TRUE) + }, + + debug = function(message) { + if (self$level %in% c("DEBUG")) { + self$write(sprintf("[DEBUG] %s", message)) + } + }, + + info = function(message) { + if (self$level %in% c("DEBUG", "INFO")) { + self$write(sprintf("[INFO] %s", message)) + } + }, + + warn = function(message) { + self$write(sprintf("[WARN] %s", message)) + warning(message) + }, + + error = function(message, error_obj = NULL) { + timestamp <- format(Sys.time(), "%Y-%m-%d %H:%M:%S") + self$write(sprintf("[ERROR] %s: %s", timestamp, message)) + + if (!is.null(error_obj)) { + self$write(sprintf(" Traceback: %s", + paste(error_obj$traceback, collapse = "\n "))) + } + }, + + get_logs = function(n_lines = 50) { + if (!file.exists(self$log_file)) return(character(0)) + + all_lines <- readLines(self$log_file) + tail(all_lines, n_lines) + } + ) +) + +# Global logger instance +.maceR_logger <- NULL + +init_logger <- function(log_path = NULL, level = "INFO") { + .maceR_logger <<- Logger$new(log_path, level) +} + +get_logger <- function() { + if (is.null(.maceR_logger)) { + init_logger() + } + .maceR_logger +} +``` + +### Usage Throughout App +```r +# In run_app.R +run_app <- function(...) { + init_logger() + logger <- get_logger() + logger$info("Starting maceR application") + # ... rest of startup +} + +# In mod_Input_DF +observeEvent(input$upload_files, { + logger <- get_logger() + logger$info(sprintf("User uploaded %d files", length(input$files))) + + tryCatch({ + # processing + logger$info("Files validated successfully") + }, error = function(e) { + logger$error("File validation failed", e) + }) +}) + +# In mod_analysis +observeEvent(input$run_analysis, { + logger <- get_logger() + start_time <- Sys.time() + + Annotate$plot_periodogram(...) + + elapsed <- difftime(Sys.time(), start_time, units = "secs") + logger$info(sprintf("Analysis completed in %.2f seconds", elapsed)) +}) +``` + +### Files to Modify +- `R/fct_logger.R` (new) +- `R/run_app.R` +- `R/mod_*.R` (all modules, add key logging points) +- `R/app_server.R` (central error handling) + +### Benefits +- Debugging and troubleshooting +- Usage analytics +- Performance monitoring +- Audit trail for compliance +- Early warning system + +### Effort Estimate +**Time**: 2-3 hours +**Complexity**: Low +**Risk**: Very Low + +--- + +## 5. Separate UI State from Business State + +### Current State +- `App_settings$plotTab` mixes UI visibility with business logic +- No clear separation between what's visible and what's available +- Difficult to manage UI independently from data + +### Problems +- Confusing mental model +- Hard to test +- Difficult to maintain UI state independently +- Brittle dependencies + +### Proposed Solution +Create separate state objects for UI and business logic. + +### Implementation Code +```r +# R/fct_R6_UIState.R + +UIState <- R6::R6Class("UIState", + public = list( + visible_tabs = list(), + active_tab = NULL, + sidebar_expanded = TRUE, + show_debug_panel = FALSE, + plot_layout = "grid", # or "list" + + initialize = function() { + self$visible_tabs <- list( + home = TRUE, + input = FALSE, + data_structure = FALSE, + your_data = FALSE, + analysis = FALSE, + plots = FALSE, + reports = FALSE + ) + }, + + show_tab = function(tab_name) { + self$visible_tabs[[tab_name]] <- TRUE + self$active_tab <- tab_name + }, + + hide_tab = function(tab_name) { + self$visible_tabs[[tab_name]] <- FALSE + }, + + tab_visible = function(tab_name) { + isTRUE(self$visible_tabs[[tab_name]]) + }, + + all_visible_tabs = function() { + names(self$visible_tabs)[unlist(self$visible_tabs)] + } + ) +) + +# R/fct_R6_BusinessState.R + +BusinessState <- R6::R6Class("BusinessState", + public = list( + data_loaded = FALSE, + data_validated = FALSE, + analysis_complete = FALSE, + report_available = FALSE, + current_animals = 0, + + mark_data_loaded = function() { + self$data_loaded <- TRUE + }, + + mark_data_validated = function() { + self$data_validated <- TRUE + }, + + mark_analysis_complete = function() { + self$analysis_complete <- TRUE + self$report_available <- TRUE + }, + + reset_analysis = function() { + self$analysis_complete <- FALSE + self$report_available <- FALSE + }, + + can_proceed = function(to_step) { + switch(to_step, + "analysis" = self$data_loaded && self$data_validated, + "report" = self$analysis_complete, + FALSE + ) + } + ) +) +``` + +### Usage in App +```r +# Initialization +App_settings$ui_state <- UIState$new() +App_settings$business_state <- BusinessState$new() + +# In module +observeEvent(dataStructure_out$YourDataTab(), { + if (isTRUE(dataStructure_out$YourDataTab())) { + App_settings$business_state$mark_data_validated() + App_settings$ui_state$show_tab("your_data") + } +}) +``` + +### Files to Modify +- `R/fct_R6_UIState.R` (new) +- `R/fct_R6_BusinessState.R` (new) +- `R/fct_R6_App_settings.R` +- `R/mod_*.R` (update to use new state objects) + +### Benefits +- Clear separation of concerns +- Easier to test +- More maintainable +- Independent UI refresh +- Better debugging + +### Effort Estimate +**Time**: 3-4 hours +**Complexity**: Medium +**Risk**: Medium (behavioral changes) + +--- + +## 6. Use Factory Pattern for Object Creation + +### Current State +```r +# In load_data(): +Annotate <- Annotate$new() +Custom_tables <- Custom_tables$new() +# Scattered throughout +``` + +- Objects created inline +- Difficult to swap implementations +- Hard dependency injection + +### Proposed Solution +Create factory for consistent object creation. + +### Implementation Code +```r +# R/fct_object_factory.R + +ObjectFactory <- R6::R6Class("ObjectFactory", + public = list( + # Configuration + config = list(), + + initialize = function(config = list()) { + self$config <- config + }, + + create_raw_mouse_data = function(id) { + obj <- Raw_mouse_data$new() + obj$id <- id + obj + }, + + create_clean_mouse_data = function() { + Clean_mouse_data$new() + }, + + create_custom_tables = function() { + Custom_tables$new() + }, + + create_annotate = function() { + Annotate$new() + }, + + create_data_store = function() { + DataStore$new() + }, + + create_logger = function() { + Logger$new( + log_path = self$config$log_path %||% NULL, + level = self$config$log_level %||% "INFO" + ) + } + ) +) + +# Usage +factory <- ObjectFactory$new(list( + log_path = "logs/maceR.log", + log_level = "DEBUG" +)) + +annotate <- factory$create_annotate() +logger <- factory$create_logger() +``` + +### Files to Modify +- `R/fct_object_factory.R` (new) +- `R/fct_data_loader.R` +- `R/app_server.R` + +### Benefits +- Consistent object creation +- Easy to mock for testing +- Dependency injection ready +- Configuration-driven initialization + +### Effort Estimate +**Time**: 1-2 hours +**Complexity**: Low +**Risk**: Low + +--- + +## 7. Add Progress Tracking + +### Current State +- Long operations complete silently +- User unsure if app is responsive +- No feedback during heavy computation + +### Problems +- Poor user experience +- Users think app froze +- No indication of time remaining + +### Proposed Solution +Add progress callbacks to all long operations. + +### Implementation Code +```r +# R/fct_progress.R + +#' Create progress tracker +progress_tracker <- function(total_steps, title = "Processing") { + list( + total = total_steps, + current = 0, + title = title, + start_time = Sys.time(), + + update = function(step, message = NULL) { + self$current <- step + percent <- (step / self$total) * 100 + elapsed <- difftime(Sys.time(), self$start_time, units = "secs") + + if (!is.null(message)) { + cat(sprintf("%s: %d/%d (%.1f%%) - %s [%.1fs]\n", + self$title, step, self$total, percent, + message, as.numeric(elapsed))) + } + } + ) +} + +# Enhanced load_data with progress +load_data_with_progress <- function(env, progress_callback = NULL) { + total_steps <- 5 + + if (!is.null(progress_callback)) { + progress_callback(1/total_steps, "Loading raw data...") + } + myMice2 <- env$env1$myMice + myCleanMice <- list() + + if (!is.null(progress_callback)) { + progress_callback(2/total_steps, "Cleaning data...") + } + for (i in seq_along(myMice2)) { + myCleanMice[[i]] <- Clean_mouse_data$new() + myCleanMice[[i]]$compile(myMice2[[i]]) + } + + if (!is.null(progress_callback)) { + progress_callback(3/total_steps, "Creating tables...") + } + Custom_tables <- Custom_tables$new() + Custom_tables$compile(env$env2) + + if (!is.null(progress_callback)) { + progress_callback(4/total_steps, "Finalizing...") + } + Annotate <- Annotate$new() + + if (!is.null(progress_callback)) { + progress_callback(5/total_steps, "Complete") + } +} +``` + +### Usage in Shiny +```r +# In module +observeEvent(input$analyze, { + withProgress(message = "Running analysis...", value = 0, { + load_data_with_progress(env, function(progress, message) { + incProgress(progress, detail = message) + }) + }) +}) +``` + +### Files to Modify +- `R/fct_progress.R` (new) +- `R/fct_data_loader.R` +- `R/mod_analysis.R` +- `R/mod_report.R` + +### Benefits +- Better UX +- Users know app is working +- Transparency +- Can stop long operations + +### Effort Estimate +**Time**: 2-3 hours +**Complexity**: Low +**Risk**: Very Low + +--- + +## 8. Module Registration System + +### Current State +```r +# In app_server.R - manual calls +mod_Input_DF_server("Input_DF_ui_1", App_settings) +mod_data_structure_server("data_structure_ui_1", App_settings) +# ... many more +``` + +- Manual module registration +- Implicit dependencies +- Hard to manage as app grows + +### Proposed Solution +Create module registry for declarative management. + +### Implementation Code +```r +# R/fct_module_registry.R + +ModuleRegistry <- R6::R6Class("ModuleRegistry", + public = list( + modules = list(), + + register = function(name, ui_fn, server_fn, + dependencies = NULL, config = NULL) { + self$modules[[name]] <- list( + ui = ui_fn, + server = server_fn, + deps = dependencies, + config = config + ) + invisible(self) + }, + + get_module = function(name) { + if (!name %in% names(self$modules)) { + stop(sprintf("Module '%s' not registered", name)) + } + self$modules[[name]] + }, + + get_dependencies = function(name) { + self$get_module(name)$deps + }, + + is_registered = function(name) { + name %in% names(self$modules) + }, + + list_modules = function() { + names(self$modules) + } + ) +) + +# Usage +module_registry <- ModuleRegistry$new() + +module_registry$register( + "input_df", + ui_fn = mod_Input_DF_ui, + server_fn = mod_Input_DF_server, + dependencies = NULL +) + +module_registry$register( + "data_structure", + ui_fn = mod_data_structure_ui, + server_fn = mod_data_structure_server, + dependencies = c("input_df") +) + +module_registry$register( + "analysis", + ui_fn = mod_analysis_ui, + server_fn = mod_analysis_server, + dependencies = c("data_structure") +) + +# Initialization +init_modules <- function(registry, App_settings) { + for (module_name in registry$list_modules()) { + module <- registry$get_module(module_name) + module$server(module_name, App_settings) + } +} +``` + +### Files to Modify +- `R/fct_module_registry.R` (new) +- `R/app_server.R` +- `R/app_ui.R` + +### Benefits +- Automatic dependency resolution +- Easy to add/remove modules +- Self-documenting module graph +- Can validate dependencies at startup + +### Effort Estimate +**Time**: 2-3 hours +**Complexity**: Medium +**Risk**: Medium + +--- + +## 9. Configuration Management + +### Current State +- Hard-coded settings throughout: + - Pandoc path: `/opt/homebrew/bin/pandoc` + - Temp directories: `tempdir()` + - Cache sizes: inline numbers + - Plot dimensions: scattered + +### Problems +- Difficult deployment +- Environment-specific issues +- No centralized settings +- Hard to customize + +### Proposed Solution +Implement configuration system with file-based and environment variable support. + +### Implementation Code +```r +# R/fct_config.R + +#' Load configuration +load_config <- function(config_file = NULL) { + default_config <- list( + # Paths + pandoc_path = Sys.getenv("PANDOC_PATH", "/opt/homebrew/bin/pandoc"), + temp_dir = Sys.getenv("MACE_TEMP_DIR", tempdir()), + log_dir = Sys.getenv("MACE_LOG_DIR", "logs"), + + # Limits + max_animals = as.numeric(Sys.getenv("MACE_MAX_ANIMALS", 1000)), + max_datapoints = as.numeric(Sys.getenv("MACE_MAX_DATAPOINTS", 1000000)), + cache_size_mb = as.numeric(Sys.getenv("MACE_CACHE_SIZE", 500)), + + # Plotting + plot_width = as.numeric(Sys.getenv("MACE_PLOT_WIDTH", 800)), + plot_height = as.numeric(Sys.getenv("MACE_PLOT_HEIGHT", 600)), + + # Report + report_template_dir = system.file( + "rmarkdown/templates/maceR_report", + package = "maceR" + ), + + # Logging + log_level = Sys.getenv("MACE_LOG_LEVEL", "INFO"), + enable_debug = as.logical(Sys.getenv("MACE_DEBUG", "FALSE")) + ) + + # Load from file if provided + if (!is.null(config_file) && file.exists(config_file)) { + file_config <- yaml::read_yaml(config_file) + default_config <- utils::modifyList(default_config, file_config) + } + + default_config +} + +# Usage +config <- load_config("~/.maceR/config.yaml") + +# Access config +if (file.exists(config$pandoc_path)) { + Sys.setenv(PATH = paste(dirname(config$pandoc_path), + Sys.getenv("PATH"), sep = ":")) +} +``` + +### Configuration File Format +```yaml +# ~/.maceR/config.yaml +pandoc_path: /usr/local/bin/pandoc +temp_dir: /var/tmp/maceR +log_dir: ~/.maceR/logs +cache_size_mb: 1000 + +max_animals: 500 +max_datapoints: 10000000 + +plot_width: 1024 +plot_height: 768 + +log_level: DEBUG +enable_debug: false +``` + +### Files to Modify +- `R/fct_config.R` (new) +- `R/run_app.R` +- `DESCRIPTION` (add yaml dependency) +- Create `inst/examples/config.yaml` + +### Benefits +- Environment-specific settings +- Easy deployment +- Configuration without code changes +- Better security (no hardcoded paths) + +### Effort Estimate +**Time**: 1-2 hours +**Complexity**: Low +**Risk**: Low + +--- + +## 10. Add Session Recovery + +### Current State +- Browser refresh loses all work +- No session persistence +- Users must restart analysis + +### Problems +- Poor resilience +- Lost productivity +- Network glitches cause data loss +- Bad user experience + +### Proposed Solution +Implement session state persistence and recovery. + +### Implementation Code +```r +# R/fct_session_manager.R + +SessionManager <- R6::R6Class("SessionManager", + public = list( + session_dir = NULL, + + initialize = function(base_dir = "sessions") { + self$session_dir <- base_dir + dir.create(base_dir, showWarnings = FALSE) + }, + + save_session = function(session_id, App_settings) { + session_file <- file.path(self$session_dir, + paste0(session_id, ".rdata")) + + # Save only serializable objects + session_data <- list( + metadata = App_settings$metadata$name, + data_list = App_settings$dataList$name, + subsetting = App_settings$subsetting, + ld_params = App_settings$LDparams, + timestamp = Sys.time() + ) + + saveRDS(session_data, session_file) + }, + + load_session = function(session_id) { + session_file <- file.path(self$session_dir, + paste0(session_id, ".rdata")) + + if (file.exists(session_file)) { + readRDS(session_file) + } else { + NULL + } + }, + + session_exists = function(session_id) { + session_file <- file.path(self$session_dir, + paste0(session_id, ".rdata")) + file.exists(session_file) + }, + + list_sessions = function() { + session_files <- list.files(self$session_dir, + pattern = "*.rdata") + sub(".rdata$", "", session_files) + }, + + delete_session = function(session_id) { + session_file <- file.path(self$session_dir, + paste0(session_id, ".rdata")) + if (file.exists(session_file)) { + file.remove(session_file) + return(TRUE) + } + FALSE + } + ) +) + +# Integration with Shiny +mod_session_recovery_ui <- function(id) { + ns <- NS(id) + tagList( + conditionalPanel( + condition = "output.previous_session_exists", + div( + class = "alert alert-info", + p("A previous session was found. Would you like to restore it?"), + actionButton(ns("restore"), "Restore Session"), + actionButton(ns("new_session"), "Start New Session") + ) + ) + ) +} + +mod_session_recovery_server <- function(id, App_settings, + session_mgr) { + moduleServer(id, function(input, output, session) { + session_id <- session$token + + output$previous_session_exists <- reactive({ + session_mgr$session_exists(session_id) + }) + outputOptions(output, "previous_session_exists", + suspendWhenHidden = FALSE) + + observeEvent(input$restore, { + session_data <- session_mgr$load_session(session_id) + if (!is.null(session_data)) { + # Restore App_settings + App_settings$metadata <- session_data$metadata + App_settings$dataList <- session_data$data_list + App_settings$subsetting <- session_data$subsetting + App_settings$LDparams <- session_data$ld_params + + showNotification("Session restored", type = "message") + } + }) + + # Auto-save periodically + observe({ + invalidateLater(60000) # Every 60 seconds + session_mgr$save_session(session_id, App_settings) + }) + }) +} +``` + +### Files to Modify +- `R/fct_session_manager.R` (new) +- `R/mod_session_recovery.R` (new) +- `R/app_server.R` +- `R/app_ui.R` + +### Benefits +- Resilience to network issues +- Better user experience +- Work preservation +- Session history available + +### Effort Estimate +**Time**: 3-4 hours +**Complexity**: Medium +**Risk**: Medium + +--- + +# Implementation Priority Matrix + +## High Priority - High Impact, Low Effort +1. **Consolidate Environment Management** (1) +2. **Add Data Validation Layer** (2) +3. **Configuration Management** (9) + +**Recommended for next release**. These provide significant improvements with manageable effort. + +## Medium Priority - Medium Impact, Medium Effort +4. **Add Centralized Logging/Error Handling** (4) +5. **Separate UI State from Business State** (5) +6. **Module Registration System** (8) +7. **Add Progress Tracking** (7) + +**Recommended for release 2-3**. These improve developer experience and user experience significantly. + +## Lower Priority - High Impact, Higher Effort +8. **Use Factory Pattern** (6) +9. **Implement Caching/Memoization** (3) +10. **Add Session Recovery** (10) + +**Recommended for later releases**. These provide major improvements but require more development time. + +--- + +# Implementation Timeline + +### Phase 1 (Weeks 1-2) +- [ ] Implement Configuration Management (9) +- [ ] Add Data Validation Layer (2) +- [ ] Consolidate Environment Management (1) + +### Phase 2 (Weeks 3-4) +- [ ] Add Centralized Logging (4) +- [ ] Add Progress Tracking (7) +- [ ] Use Factory Pattern (6) + +### Phase 3 (Weeks 5-6) +- [ ] Separate UI/Business State (5) +- [ ] Module Registration (8) +- [ ] Implement Caching (3) + +### Phase 4 (Weeks 7+) +- [ ] Add Session Recovery (10) +- [ ] Additional features/bug fixes + +--- + +# Testing Recommendations + +For each improvement: + +1. **Unit Tests**: Test R6 classes and utility functions +2. **Integration Tests**: Test with actual Shiny modules +3. **UI Tests**: Test user workflows end-to-end +4. **Performance Tests**: Benchmark caching, logging overhead +5. **Backward Compatibility**: Ensure existing features still work + +--- + +# Rollback Plan + +Each improvement should include a rollback plan: +- Git branch with working version +- Feature flags to enable/disable new code +- Data migration scripts if needed +- Clear commit history for easy revert + +--- + +# References + +- [Developer Guide](maceR_Developer_Guide.pdf) +- [Shiny Best Practices](https://shiny.rstudio.com/articles/) +- [R6 Documentation](https://r6.r-lib.org/) +- [Design Patterns in R](https://adv-r.hadley.nz/oo.html) + +--- + +**Last Updated**: April 15, 2026 +**Next Review**: After Phase 1 completion +**Owner**: Development Team diff --git a/dev_guide.Rmd b/dev_guide.Rmd new file mode 100644 index 0000000..688dd7e --- /dev/null +++ b/dev_guide.Rmd @@ -0,0 +1,721 @@ +--- +title: "maceR Developer Guide" +subtitle: "Architecture, Workflow, and Component Integration" +author: "Marco Ferrari" +date: "`r format(Sys.time(), '%d %B, %Y')`" +output: + pdf_document: + toc: true + toc_depth: 3 + number_sections: true + highlight: tango + latex_engine: xelatex +--- + +\pagebreak + +# Introduction + +This guide is designed for developers new to the maceR codebase. It provides an overview of the application architecture, explains the core workflow, and demonstrates how to integrate new components and functionalities into the system. + +## Quick Facts + +- **Framework**: Shiny (via Golem boilerplate) +- **Architecture**: Modular with R6 state management +- **Primary Use**: Interactive analysis of circadian locomotor activity data +- **Language**: R +- **Key Dependencies**: behavr, ggetho, zeitgebr, ggplot2, data.table, dplyr + +--- + +# Part 1: Application Architecture + +## High-Level Overview + +The maceR application follows a **modular Shiny architecture** with centralized state management using **R6 classes**. Data flows through a series of transformations, from raw upload to visualization and reporting. + +### Core Design Pattern + +``` +User Input (UI) + ↓ +Module Server Logic (Reactivity) + ↓ +R6 State Objects (Persistent Storage) + ↓ +Analysis Functions (Computation) + ↓ +Visualization (Plots & Reports) + ↓ +Download / Export +``` + +## Directory Structure + +``` +maceR/ +├── R/ +│ ├── app_ui.R # Main UI definition +│ ├── app_server.R # Main server orchestration +│ ├── mod_*.R # Feature modules (UI + Server) +│ ├── fct_*.R # Utility & computation functions +│ ├── fct_R6_*.R # R6 class definitions +│ ├── utils_*.R # Helper functions +│ └── run_app.R # App startup +├── inst/ +│ ├── app/www/ # Static assets (CSS, JS) +│ └── rmarkdown/ # Report templates +├── man/ # Documentation files +├── data-raw/ # Raw data for examples +└── DESCRIPTION # Package metadata +``` + +--- + +# Part 2: State Management with R6 Classes + +## Overview + +R6 classes are the backbone of maceR's state management. They store application state in a persistent, accessible way across different modules and sessions. + +## Key R6 Classes + +### 1. `App_settings` (fct_R6_App_settings.R) + +**Purpose**: Central configuration and environment holder + +**Key Fields**: +- `dashboardHeader_appearance`: UI theme toggle +- `dataList`, `metadata`: File upload paths +- `listMice`: Lists of processed animals +- `subsetting`: Filter parameters +- `LDcondition`, `LDparams`: Light cycle parameters +- `plotTab`: Visibility flags for tabs +- `env1`, `env2`, `env3`, `env4`: Environment pointers (see below) +- `env_msg`: Messages object environment + +**Key Methods**: +- `setData()`, `setMeta()`: Store uploaded files +- `setListMice()`: Process mouse ID list +- `setListMiceFiltered()`: Apply filtering +- `setLD()`: Configure light cycle + +**Where It's Stored**: +``` +App_settings (created in app_server.R) + ├── env1 → myMice environment (Raw_mouse_data objects) + ├── env2 → myCleanMice + Custom_tables environments + ├── env3 → Custom_tables environment + └── env4 → Annotate environment (plots stored here) +``` + +### 2. `Raw_mouse_data` (fct_R6_Raw_mouse_data.R) + +**Purpose**: Store raw data from uploaded files + +**Key Fields**: +- `id`: Animal identifier +- `metadata`: Raw metadata row +- `timepoint`: Time vector +- `countsMinute`: Activity counts + +**Key Methods**: +- `add()`: Load and parse data files + +**Storage**: Created in `preload_data()` (fct_data_loader.R), stored in `env$env1$myMice` + +### 3. `Clean_mouse_data` (fct_R6_Clean_mouse_data.R) + +**Purpose**: Cleaned, transformed data ready for analysis + +**Key Fields**: +- Individual animal data with converted time formats +- `sex`, `genotype`, `cabinet`: Metadata fields +- `lightOn`: Light-on hour +- `length`: Number of datapoints + +**Key Methods**: +- `compile()`: Transform raw data +- `addData()`: Populate fields + +**Storage**: Created in `load_data()`, stored in `env$env2$myCleanMice` + +### 4. `Custom_tables` (fct_R6_Custom_tables.R) + +**Purpose**: Aggregated data suitable for visualization + +**Key Fields**: +- `locomotor_act`: Lists of activity data at different aggregations +- `daily_act`: Daily activity summaries +- `periodograms`: Circadian period data +- `average_day`: Average daily patterns + +**Key Methods**: +- `compile()`: Create tables from clean data +- `locomotor_subset()`, `daily_subset()`: Filter functions + +**Storage**: Created in `load_data()`, stored in `env$env3$Custom_tables` + +### 5. `Annotate` (fct_R6_Annotate.R) + +**Purpose**: Store generated plots and metadata + +**Key Fields**: +- `Actograms`: Single-plotted actogram list +- `DPActograms`: Double-plotted actogram list +- `DAct_plots`: Daily activity plots +- `period_plots`: Periodogram plots +- `period_plots_box`: Period distribution plots +- `avg_day_plots`: Average day plots +- `Report`: Generated report data (NEW) +- `metaTable`: Animal metadata +- `actTable`: Currently displayed activity table + +**Key Methods**: +- `showMeta()`: Display metadata +- `showData()`: Display animal data +- `plot_actogram()`: Generate actogram +- `plot_double_actogram()`: Generate double-plotted actogram +- (and many others for different plot types) + +**Storage**: Created in `load_data()`, stored in `env$env4$Annotate` + +--- + +# Part 3: Data Flow & Workflow + +## Complete User Journey + +### Step 1: Data Upload (mod_Input_DF) + +``` +User uploads CSV files + metadata + ↓ +mod_Input_DF_server() validates and stores paths + ↓ +App_settings$setData() & setMeta() + ↓ +preload_data() creates Raw_mouse_data objects + ↓ +Raw data stored in env1 +``` + +### Step 2: Data Structure Check (mod_data_structure) + +``` +User reviews data structure + ↓ +mod_data_structure_server() calls load_data() + ↓ +load_data() creates: + - myCleanMice list (env2) + - Custom_tables (env3) + - Annotate object (env4) +``` + +### Step 3: Subsetting (mod_your_data) + +``` +User selects animals/filters + ↓ +mod_your_data_server() captures selections + ↓ +App_settings$setListMiceFiltered() + ↓ +Filtered list stored in App_settings$subsetting +``` + +### Step 4: Analysis (mod_analysis) + +``` +User selects plot types & parameters + ↓ +mod_analysis_server() triggers analyses + ↓ +Calls Annotate$plot_*() methods + ↓ +Plots stored in Annotate$ fields + ↓ +Observes trigger plot loading +``` + +### Step 5: Visualization (mod_plots) + +``` +load_plots() called with plot list + ↓ +Creates UI boxes dynamically + ↓ +Renders plots from Annotate + ↓ +User can download individual plots +``` + +### Step 6: Report Generation (mod_report) + +``` +User selects report options + ↓ +mod_report_server() collects all plots + ↓ +Stores in Annotate$Report + ↓ +User clicks download + ↓ +rmarkdown::render() with parameters + ↓ +Pandoc generates HTML or PDF + ↓ +Browser downloads file +``` + +--- + +# Part 4: Module Structure + +## What is a Module? + +A Shiny module is a reusable piece of UI and server logic. maceR uses modules extensively to keep code organized and maintainable. + +### Module Naming Convention + +- **UI Function**: `mod__ui(id)` +- **Server Function**: `mod__server(id, ...)` +- **File Location**: `R/mod_.R` + +## Standard Module Template + +```r +# UI Function +mod_example_ui <- function(id) { + ns <- NS(id) + tagList( + shinydashboardPlus::box( + title = "Example Module", + width = 12, + # UI elements here + actionButton(ns("action"), "Do Something") + ) + ) +} + +# Server Function +mod_example_server <- function(id, env) { + moduleServer(id, function(input, output, session) { + ns <- session$ns + + # Reactive values + example_data <- reactiveVal(NULL) + + # Observers & event handlers + observeEvent(input$action, { + # Logic here + example_data(new_data) + }) + + # Return reactive outputs that other modules can use + return(reactive({ + list(data = example_data(), success = TRUE) + })) + }) +} +``` + +## Key Modules in maceR + +| Module | Purpose | Key Return | +|--------|---------|-----------| +| `mod_landing_page` | Welcome screen | None | +| `mod_Input_DF` | File upload | Trigger signal | +| `mod_data_structure` | Data validation | Trigger for next step | +| `mod_your_data` | Data inspection | None | +| `mod_analysis` | Plot parameter selection | Plot triggers | +| `mod_plots` | Plot display | None | +| `mod_report` | Report generation | Download handler | + +--- + +# Part 5: Adding New Functionality + +## Scenario 1: Add a New Plot Type + +**Goal**: Create a new analysis plot (e.g., "Average hourly activity") + +### Step 1: Add Method to Annotate R6 Class + +**File**: `R/fct_R6_Annotate.R` + +```r +Annotate <- R6::R6Class("Annotate", + list( + # ... existing fields ... + avg_hourly_plots = list(AvgHour1 = list()), + + # New method + plot_average_hourly = function(env, type) { + data <- env$env3$Custom_tables$locomotor_act[[1]] + # calculation logic + plot <- ggplot2::ggplot(data) + + # plot specification + ggplot2::ggtitle("Average Hourly Activity") + + self$avg_hourly_plots$AvgHour1[[1]] <- plot + } + ) +) +``` + +### Step 2: Add Handler in Analysis Module + +**File**: `R/mod_analysis.R` + +```r +observeEvent(input$avg_hourly_checkbox, { + if (input$avg_hourly_checkbox) { + Annotate <- App_settings$env4$Annotate + Annotate$plot_average_hourly(App_settings$env3, type = "total") + # Trigger plot loading + load_plots(env = App_settings, + plot_list = "avg_hourly", + session = session) + } +}) +``` + +### Step 3: Add UI Control + +**File**: `R/mod_analysis.R` (UI function) + +```r +checkboxInput( + ns("avg_hourly_checkbox"), + "Average Hourly Activity", + value = FALSE +) +``` + +### Step 4: Register in Output List + +**File**: `R/fct_R6_Annotate.R` + +Add to `output_list_acto` tibble: +```r +# ... in output_list_acto data frame +"hourlyAvg" # handler +"AvgHour1" # destination +"Annotate$avg_hourly_plots$AvgHour1[[1]]" # location +"Average Hourly Activity" # title +# ... and other metadata columns +``` + +--- + +## Scenario 2: Add a New Analysis Tab + +**Goal**: Create a new analysis workflow (e.g., "Statistical Comparison") + +### Step 1: Create Module File + +**File**: `R/mod_statistics.R` + +```r +mod_statistics_ui <- function(id) { + ns <- NS(id) + tagList( + shinydashboardPlus::box( + title = "Statistical Comparison", + width = 12, + selectInput(ns("stat_type"), "Test:", + choices = c("t-test", "ANOVA")), + actionButton(ns("run_test"), "Run Analysis") + ) + ) +} + +mod_statistics_server <- function(id, env) { + moduleServer(id, function(input, output, session) { + observeEvent(input$run_test, { + data <- env$env3$Custom_tables$locomotor_act[[1]] + # Perform statistical analysis + results <- your_analysis_function(data, input$stat_type) + # Store or display results + }) + }) +} +``` + +### Step 2: Add to Main App + +**File**: `R/app_ui.R` + +```r +# In the shinydashboardPlus::dashboardBody +menuItem("Statistics", tabName = "Statistics", icon = icon("chart-bar")) +``` + +**File**: `R/app_server.R` + +```r +# Create the tab when analysis is complete +observeEvent(analysis_out$plotTab(), { + output$Statistics <- render_tabItem_ui(tabname = "Statistics", + text = "Statistics", + icon = "chart-bar") +}) + +# Call module server +mod_statistics_server("stats_ui_1", App_settings) +``` + +--- + +## Scenario 3: Add Data Export Format + +**Goal**: Allow exporting data as Excel files + +### Step 1: Install Package + +```r +# In DESCRIPTION, add to Imports: +openxlsx +``` + +### Step 2: Create Export Function + +**File**: `R/utils_converters.R` + +```r +export_to_excel <- function(data_list, filename) { + wb <- openxlsx::createWorkbook() + + for (sheet_name in names(data_list)) { + openxlsx::addWorksheet(wb, sheetName = sheet_name) + openxlsx::writeData(wb, sheet_name, data_list[[sheet_name]]) + } + + openxlsx::saveWorkbook(wb, filename) +} +``` + +### Step 3: Add Download Button + +**File**: `R/mod_your_data.R` (or wherever appropriate) + +```r +output$download_excel <- downloadHandler( + filename = function() { + paste0("maceR_data_", format(Sys.Date(), "%Y%m%d"), ".xlsx") + }, + content = function(file) { + data_list <- list( + metadata = App_settings$env4$Annotate$metaTable, + activity = App_settings$env4$Annotate$actTable + ) + export_to_excel(data_list, file) + } +) +``` + +--- + +# Part 6: Adding to Report Generation + +## Integrating New Content into Reports + +The report system is parameterized through the `Annotate$Report` field. + +### Step 1: Collect Data in mod_report + +**File**: `R/mod_report.R` + +```r +# In the generate_report observer +plots_data <- list( + # ... existing plots ... + new_analysis = env$env4$Annotate$new_analysis_plots, + statistics_results = env$env4$Annotate$stat_results # NEW +) +``` + +### Step 2: Update Report Template + +**File**: `inst/rmarkdown/templates/maceR_report/skeleton/skeleton.Rmd` + +```markdown +# New Analysis Section + +\`\`\`{r new-analysis} +if (!is.null(params$plots_data$new_analysis)) { + for (i in seq_along(params$plots_data$new_analysis)) { + if (!is.null(params$plots_data$new_analysis[[i]])) { + print(params$plots_data$new_analysis[[i]]) + } + } +} +\`\`\` +``` + +--- + +# Part 7: Common Patterns & Best Practices + +## Pattern 1: Reactive Data Flow + +```r +# Good: Chain reactives for clarity +user_selection <- reactive({ input$selected_ids }) +filtered_data <- reactive({ + Custom_tables$filter_by_ids(user_selection()) +}) +results <- reactive({ + compute_analysis(filtered_data()) +}) + +# Less clear: All logic in one reactive +results <- reactive({ + # multiple steps mixed together +}) +``` + +## Pattern 2: Accessing R6 Objects from Modules + +```r +# Recommended: Pass entire environment +mod_analysis_server <- function(id, env) { + moduleServer(id, function(input, output, session) { + # Access via env$env4$Annotate + }) +} + +# Alternative: Pass App_settings directly +mod_analysis_server <- function(id, App_settings) { + moduleServer(id, function(input, output, session) { + # Access via App_settings$env4$Annotate + }) +} +``` + +## Pattern 3: Loading Dynamic Plots + +```r +# Use the load_plots() utility function +load_plots(env = App_settings, + plot_list = c("plot_type1", "plot_type2"), + session = session) + +# This creates UI boxes and renders plots dynamically +``` + +## Pattern 4: Error Handling in Reports + +```r +# Template code +\`\`\`{r section, error=TRUE} +if (!is.null(params$plots_data$analysis)) { + tryCatch({ + print(params$plots_data$analysis[[1]]) + }, error = function(e) { + cat("Error:", conditionMessage(e), "\n") + }) +} else { + cat("No analysis available") +} +\`\`\` +``` + +--- + +# Part 8: Debugging & Development Tips + +## Setting Up Development Environment + +```r +# Load all code and dependencies +devtools::load_all() + +# Run tests +devtools::test() + +# Check package integrity +devtools::check() + +# Run the app +maceR::run_app() +``` + +## Using browser() for Debugging + +```r +# In any server function or reactive +observeEvent(input$action, { + browser() # Execution pauses here + # Inspect App_settings, env$env4$Annotate, etc. +}) +``` + +## Inspecting R6 Objects + +```r +# View structure +str(App_settings$env4$Annotate) + +# Check specific field +App_settings$env4$Annotate$Actograms + +# Verify method +head(App_settings$env4$Annotate$metaTable) +``` + +## Common Issues + +| Issue | Solution | +|-------|----------| +| "Plot not displaying" | Check `load_plots()` call and verify plot in Annotate | +| "NULL reference error" | Ensure prerequisite steps completed (data loaded, analysis run) | +| "Module not rendering" | Check module call in app_server.R with correct ID | +| "Report won't download" | Verify params declared in template YAML | +| "File upload fails" | Check file format matches expected CSV structure | + +--- + +# Part 9: Quick Reference + +## Creating a New Component Checklist + +- [ ] Define R6 class or add method to existing class +- [ ] Add UI controls in module +- [ ] Add server logic to handle UI events +- [ ] Update `App_settings` or state object if needed +- [ ] Register in main app (app_ui.R, app_server.R) +- [ ] Test with `devtools::load_all()` and `run_app()` +- [ ] Add documentation/comments +- [ ] Include in report if applicable + +## File Organization + +**Data handling**: `R/fct_R6_*.R` and `R/fct_data_loader.R` +**UI components**: `R/mod_*.R` (UI section) +**Business logic**: `R/mod_*.R` (server section) +**Utilities**: `R/utils_*.R` and `R/fct_*.R` +**App entry point**: `R/app_ui.R`, `R/app_server.R`, `R/run_app.R` +**Reports**: `inst/rmarkdown/templates/` + +--- + +# Conclusion + +The maceR application provides a solid foundation for circadian activity analysis with a clear architecture that supports easy extension. Key takeaways: + +1. **R6 objects** handle all persistent state +2. **Modules** keep UI and logic organized +3. **Data flows** through predictable stages (raw → clean → tables → plots) +4. **Reports** automatically include generated visualizations +5. **New features** can be added by extending R6 classes and creating new modules + +For questions about specific implementations, refer to the inline code comments in each file and the main server/UI logic in `app_server.R` and `app_ui.R`. + +--- + +**Document Version**: 1.0 +**Last Updated**: `r format(Sys.time(), '%d %B, %Y')` +**Target Audience**: Developers new to maceR codebase diff --git a/maceR_Developer_Guide.pdf b/maceR_Developer_Guide.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e723c461898ccf9a2a7a9cdb5c1292a2ee3bea05 GIT binary patch literal 90163 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
2WO6S6 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 literal 0 HcmV?d00001 From 43685a5ea5a82727e271e761e8b8be99c02364ba Mon Sep 17 00:00:00 2001 From: Marco Ferrari Date: Wed, 15 Apr 2026 00:08:03 +0100 Subject: [PATCH 3/3] added reporting --- DESCRIPTION | 4 + NAMESPACE | 7 + R/app_server.R | 14 +- R/app_ui.R | 9 +- R/fct_R6_Annotate.R | 5 +- R/fct_R6_Custom_tables.R | 12 +- R/fct_data_loader.R | 6 +- R/mod_report.R | 229 ++++++++++++++++++ R/run_app.R | 11 + README.Rmd | 53 +++- .../maceR_report/skeleton/skeleton.Rmd | 152 ++++++++++++ .../templates/maceR_report/template.yaml | 5 + 12 files changed, 490 insertions(+), 17 deletions(-) create mode 100644 R/mod_report.R create mode 100644 inst/rmarkdown/templates/maceR_report/skeleton/skeleton.Rmd create mode 100644 inst/rmarkdown/templates/maceR_report/template.yaml diff --git a/DESCRIPTION b/DESCRIPTION index f4992c2..0ba17ab 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -18,13 +18,17 @@ Imports: gert, ggetho, ggplot2, + glue, golem (>= 0.3.1), + kableExtra, + knitr, lubridate, magrittr, pryr, purrr, R6, rhandsontable, + rmarkdown, shiny (>= 1.6.0), shinycssloaders, shinydashboard, diff --git a/NAMESPACE b/NAMESPACE index a26afc5..d4345ca 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -11,6 +11,7 @@ export(run_app) export(showall_datastr_ui) export(subset_input_check) export(update_DS_ui) +export(upload_subsetting) import(shiny) importFrom(golem,activate_js) importFrom(golem,add_resource_path) @@ -25,3 +26,9 @@ importFrom(shiny,shinyApp) importFrom(shiny,tagAppendAttributes) importFrom(shiny,tagList) importFrom(shiny,tags) +importFrom(grDevices,dev.off) +importFrom(grDevices,png) +importFrom(stats,sd) +importFrom(utils,sessionInfo) +importFrom(utils,write.csv) +importFrom(utils,write.csv2) diff --git a/R/app_server.R b/R/app_server.R index a95a222..f678f2f 100644 --- a/R/app_server.R +++ b/R/app_server.R @@ -66,9 +66,6 @@ app_server <- function( input, output, session ) { } }) - # get return values from mod_your_data - yourData_out <- mod_your_data_server("your_data_ui_1", App_settings) - # get return values from mod_analysis analysis_out <- mod_analysis_server("analysis_ui_1", App_settings) @@ -78,6 +75,17 @@ app_server <- function( input, output, session ) { output$Plots <- render_tabItem_ui(tabname = "Plots", text = "Plots", icon = "chart-pie") + # Also render Reports tab when plots are available + output$Reports <- render_tabItem_ui(tabname = "Reports", + text = "Reports", + icon = "file-alt") + } + }, once = TRUE) + + # Call report module server when Reports tab is available + observeEvent(analysis_out$plotTab(), { + if(isTRUE(analysis_out$plotTab()) == TRUE){ #to be changed with req() + mod_report_server("report_ui_1", App_settings) } }, once = TRUE) diff --git a/R/app_ui.R b/R/app_ui.R index 52e0c8f..d7611a6 100644 --- a/R/app_ui.R +++ b/R/app_ui.R @@ -24,7 +24,8 @@ app_ui <- function(request) { shinydashboard::menuItemOutput(outputId = "DataStructure"), shinydashboard::menuItemOutput(outputId = "YourData"), shinydashboard::menuItemOutput(outputId = "Analysis"), - shinydashboard::menuItemOutput(outputId = "Plots") + shinydashboard::menuItemOutput(outputId = "Plots"), + shinydashboard::menuItemOutput(outputId = "Reports") ) ), body = shinydashboard::dashboardBody( @@ -59,7 +60,11 @@ app_ui <- function(request) { #### Plots ###### shinydashboard::tabItem(tabName = "Plots", mod_plots_ui("plots_ui_1") - ) #endPlots + ), #endPlots + #### Reports ###### + shinydashboard::tabItem(tabName = "Reports", + mod_report_ui("report_ui_1") + ) #endReports ) #end Tab Items ), #end dashboardBody diff --git a/R/fct_R6_Annotate.R b/R/fct_R6_Annotate.R index bb71007..c7641c3 100644 --- a/R/fct_R6_Annotate.R +++ b/R/fct_R6_Annotate.R @@ -52,6 +52,7 @@ Annotate <- R6::R6Class("Annotate", avg_day_plots = list(AvgDay1 = list() ), + Report = list(), output_list_acto = dplyr::tibble( # unique keyword that identifies the plots that have been requested from the user in the Analysis tab "handler" = c("total", "sex", "genotype", "cabinet", @@ -152,7 +153,7 @@ Annotate <- R6::R6Class("Annotate", "Light_On" = as.character(), "Datapoints" = as.character() ) - for(h in seq_len(length(myCleanMice))){ + for(h in seq_along(myCleanMice)){ d2 <- dplyr::tibble( "id" = myCleanMice[[h]]$id, "Sex" = myCleanMice[[h]]$sex, @@ -184,7 +185,7 @@ Annotate <- R6::R6Class("Annotate", "Activity" = as.character() ) if (id == "All"){ - for (h in seq_len(length(myCleanMice))){ + for (h in seq_along(myCleanMice)){ d1 <- dplyr::tibble( "id" = myCleanMice[[h]]$id, "Sex" = myCleanMice[[h]]$sex, diff --git a/R/fct_R6_Custom_tables.R b/R/fct_R6_Custom_tables.R index 21c0bad..bb596bf 100644 --- a/R/fct_R6_Custom_tables.R +++ b/R/fct_R6_Custom_tables.R @@ -46,7 +46,7 @@ Custom_tables <- R6::R6Class("Custom_tables", lengthList <- c(0,0) myCleanMice <- env$myCleanMice - for (i in seq_len(length(myCleanMice))){ + for (i in seq_along(myCleanMice)){ idList[i] <- as.character(myCleanMice[[i]]$id) sexList[i] <- as.character(myCleanMice[[i]]$sex) geneList[i] <- as.character(myCleanMice[[i]]$genotype) @@ -92,11 +92,11 @@ Custom_tables <- R6::R6Class("Custom_tables", range = c((env$subsetting$timespan[1]*1440),(env$subsetting$timespan[2]*1440)) }, "No" = { - filteredMice <- seq_len(length(env$env2$myCleanMice)) + filteredMice <- seq_along(env$env2$myCleanMice) range = c(0,max(env$env2$Annotate$metaTable$Datapoints)) }) myCleanMice <- env$env3$myCleanMice[filteredMice] - for (i in seq_len(length(myCleanMice))){ + for (i in seq_along(myCleanMice)){ #get light length light_len <- env$LDparams$light # discard half of light_len to align data @@ -192,7 +192,7 @@ Custom_tables <- R6::R6Class("Custom_tables", range = c((x$subsetting$timespan[1]*1440),(x$subsetting$timespan[2]*1440)) }, "No" = { - toLoad <- seq_len(length(myCleanMice)) + toLoad <- seq_along(myCleanMice) range = c(0,max(x$env2$Annotate$metaTable$Datapoints)) }) for (i in toLoad){ @@ -241,7 +241,7 @@ Custom_tables <- R6::R6Class("Custom_tables", range = c((env$subsetting$timespan[1]*1440),(env$subsetting$timespan[2]*1440)) }, "No" = { - filteredMice <- seq_len(length(env$env2$myCleanMice)) + filteredMice <- seq_along(env$env2$myCleanMice) range = c(0,max(env$env2$Annotate$metaTable$Datapoints)) }) startDay = range[1]/1440 @@ -254,7 +254,7 @@ Custom_tables <- R6::R6Class("Custom_tables", "Cabinet" = as.numeric(), "Sex" = as.character(), "Genotype" = as.character()) - for (h in seq_len(length(filteredMice))){ + for (h in seq_along(filteredMice)){ #if condition to handle time subsetting if(subsetVal == "Yes"){ range1 <- which(mouseData[[h]]$timepoint >= range[1]*60) diff --git a/R/fct_data_loader.R b/R/fct_data_loader.R index 27879b3..45bb14b 100644 --- a/R/fct_data_loader.R +++ b/R/fct_data_loader.R @@ -12,7 +12,7 @@ preload_data <- function(App_settings){ myMice <- NULL - for(i in seq_len(length.out = length(App_settings$dataList$name))) { + for(i in seq_along(App_settings$dataList$name)) { myMice[[i]] <- Raw_mouse_data$new() myMice[[i]]$add(App_settings$dataList$datapath[i], App_settings$metadata$datapath, App_settings) } @@ -31,7 +31,7 @@ load_data <- function(env){ myCleanMice <- list() # go through all the raw_mouse_data object in the myMice list and compile # the clean_mouse_data object - for (i in seq_len(length.out = length(App_settings$dataList$name))){ + for (i in seq_along(App_settings$dataList$name)){ myCleanMice[[i]] <- Clean_mouse_data$new() myCleanMice[[i]]$compile(myMice2[[i]]) myCleanMice[[i]]$addData(myMice2[[i]], App_settings) @@ -57,7 +57,7 @@ upload_subsetting <- function(funEnv, session, input_result){ listMice <- App_settings$listMice[,2] metadata <- App_settings$env2$Annotate$metaTable #update subsetting fields - updateSelectInput(session, "idSubsetList", choice = c("choose" = "", listMice), selected = NULL) + updateSelectInput(session, "idSubsetList", choices = c("choose" = "", listMice), selected = NULL) listSex <- unique(metadata$Sex) updateSelectInput(session, "sexSubsetList", choices = c("choose" = "", listSex), selected = NULL) listGenotype <- unique(metadata$Genotype) diff --git a/R/mod_report.R b/R/mod_report.R new file mode 100644 index 0000000..0b575cd --- /dev/null +++ b/R/mod_report.R @@ -0,0 +1,229 @@ +#' report UI Function +#' +#' @description A shiny Module to generate downloadable reports from the analysis +#' +#' @param id,input,output,session Internal parameters for {shiny}. +#' +#' @noRd +#' +#' @importFrom shiny NS tagList +mod_report_ui <- function(id){ + ns <- NS(id) + tagList( + shinydashboardPlus::box( + title = "Generate Analysis Report", + status = "info", + solidHeader = TRUE, + width = 12, + fluidRow( + column( + width = 6, + h4("Report Options"), + radioButtons( + ns("report_format"), + "Output Format:", + choices = c("HTML" = "html_document", "PDF" = "pdf_document"), + selected = "html_document" + ), + checkboxInput( + ns("include_metadata"), + "Include animal metadata table", + value = TRUE + ), + checkboxInput( + ns("include_summary"), + "Include analysis summary", + value = TRUE + ) + ), + column( + width = 6, + h4("Available Content"), + uiOutput(ns("content_summary")), + br(), + actionButton( + ns("generate_report"), + "Generate Report", + icon = icon("file-alt"), + class = "btn-primary btn-lg" + ), + br(), + br(), + uiOutput(ns("download_ui")) + ) + ) + ) + ) +} + +#' report Server Functions +#' +#' @noRd +mod_report_server <- function(id, env){ + moduleServer(id, function(input, output, session){ + ns <- session$ns + + # Reactive value to store report data + report_data <- reactiveVal(NULL) + if (!is.null(env$env4$Annotate$Report) && length(env$env4$Annotate$Report) > 0) { + report_data(env$env4$Annotate$Report) + } + + # Content summary + output$content_summary <- renderUI({ + req(env$env4$Annotate) + + plot_counts <- list( + actograms = length(env$env4$Annotate$Actograms[sapply(env$env4$Annotate$Actograms, function(x) length(x) > 0)]), + dp_actograms = length(env$env4$Annotate$DPActograms[sapply(env$env4$Annotate$DPActograms, function(x) length(x) > 0)]), + daily_activity = length(env$env4$Annotate$DAct_plots[sapply(env$env4$Annotate$DAct_plots, function(x) !is.null(x) && length(x) > 0)]), + periodograms = length(env$env4$Annotate$period_plots[sapply(env$env4$Annotate$period_plots, function(x) !is.null(x) && length(x) > 0)]), + period_boxplots = length(env$env4$Annotate$period_plots_box[sapply(env$env4$Annotate$period_plots_box, function(x) !is.null(x) && length(x) > 0)]), + average_day = length(env$env4$Annotate$avg_day_plots[sapply(env$env4$Annotate$avg_day_plots, function(x) !is.null(x) && length(x) > 0)]) + ) + + total_plots <- sum(unlist(plot_counts)) + + if (total_plots == 0) { + return(tags$div( + class = "alert alert-warning", + icon("exclamation-triangle"), + "No plots have been generated yet. Please run analyses in the Analysis tab first." + )) + } + + tags$div( + tags$ul( + if (plot_counts$actograms > 0) tags$li(glue::glue("{plot_counts$actograms} Actogram(s)")), + if (plot_counts$dp_actograms > 0) tags$li(glue::glue("{plot_counts$dp_actograms} Double-plotted actogram(s)")), + if (plot_counts$daily_activity > 0) tags$li(glue::glue("{plot_counts$daily_activity} Daily activity plot(s)")), + if (plot_counts$periodograms > 0) tags$li(glue::glue("{plot_counts$periodograms} Periodogram(s)")), + if (plot_counts$period_boxplots > 0) tags$li(glue::glue("{plot_counts$period_boxplots} Period distribution plot(s)")), + if (plot_counts$average_day > 0) tags$li(glue::glue("{plot_counts$average_day} Average daily activity plot(s)")) + ), + tags$p(glue::glue("Total: {total_plots} plot(s) ready for report")) + ) + }) + + # Generate report data + observeEvent(input$generate_report, { + req(env$env4$Annotate) + + # Collect all plots + plots_data <- list( + actograms = lapply(env$env4$Annotate$Actograms, function(x) if(length(x) > 0) x[[1]] else NULL), + dp_actograms = lapply(env$env4$Annotate$DPActograms, function(x) if(length(x) > 0) x[[1]] else NULL), + daily_activity = env$env4$Annotate$DAct_plots, + periodograms = env$env4$Annotate$period_plots, + period_boxplots = env$env4$Annotate$period_plots_box, + average_day = env$env4$Annotate$avg_day_plots + ) + + # Remove NULL entries + plots_data <- lapply(plots_data, function(x) x[!sapply(x, is.null)]) + + # Collect metadata if requested + metadata <- NULL + if (input$include_metadata && !is.null(env$env4$Annotate$metaTable)) { + metadata <- env$env4$Annotate$metaTable + } + + # Create analysis summary + analysis_summary <- NULL + if (input$include_summary) { + analysis_summary <- data.frame( + Parameter = c("Report Generated", "Total Animals", "Plots Included"), + Value = c( + format(Sys.time(), "%Y-%m-%d %H:%M:%S"), + ifelse(!is.null(metadata), nrow(metadata), "Unknown"), + length(unlist(plots_data, recursive = FALSE)) + ) + ) + } + + report_list <- list( + plots_data = plots_data, + metadata = metadata, + analysis_summary = analysis_summary, + format = input$report_format + ) + + report_data(report_list) + env$env4$Annotate$Report <- report_list + + showNotification("Report data prepared! Click download to generate the file.", + type = "message", duration = 5) + }) + + # Download UI + output$download_ui <- renderUI({ + req(report_data()) + downloadButton( + ns("download_report"), + "Download Report", + icon = icon("download"), + class = "btn-success btn-lg" + ) + }) + + # Download handler + output$download_report <- downloadHandler( + filename = function() { + ext <- ifelse(report_data()$format == "html_document", "html", "pdf") + paste0("maceR_report_", format(Sys.Date(), "%Y%m%d"), ".", ext) + }, + content = function(file) { + # Create temporary directory for rendering + temp_dir <- tempdir() + temp_rmd <- file.path(temp_dir, "report.Rmd") + + # Get the skeleton template + skeleton_path <- system.file("rmarkdown", "templates", "maceR_report", "skeleton", "skeleton.Rmd", + package = "maceR") + + if (skeleton_path == "") { + stop("Report template not found") + } else { + rmd_content <- readLines(skeleton_path) + } + + # Write the Rmd file + writeLines(rmd_content, temp_rmd) + + # Extract format and remove from params before rendering + output_fmt <- report_data()$format + params_to_pass <- report_data() + params_to_pass$format <- NULL + + # Render the report with error handling + tryCatch({ + rendered_file <- rmarkdown::render( + temp_rmd, + output_format = output_fmt, + params = params_to_pass, + output_dir = temp_dir, + quiet = FALSE, + envir = new.env() + ) + + # Copy rendered file to download location + if (file.exists(rendered_file)) { + file.copy(rendered_file, file, overwrite = TRUE) + } else { + stop("Rendered file not found at: ", rendered_file) + } + + }, error = function(e) { + stop("Error rendering report: ", conditionMessage(e)) + }) + } + ) + + }) +} + +## To be copied in the UI +# mod_report_ui("report_ui_1") + +## To be copied in the server +# mod_report_server("report_ui_1") \ No newline at end of file diff --git a/R/run_app.R b/R/run_app.R index 93200ed..4148378 100644 --- a/R/run_app.R +++ b/R/run_app.R @@ -16,6 +16,17 @@ run_app <- function( uiPattern = "/", ... ) { + # Ensure Pandoc is available for rmarkdown report generation + # Check common Homebrew installation path + homebrew_pandoc <- "/opt/homebrew/bin/pandoc" + if (file.exists(homebrew_pandoc)) { + # Add Homebrew bin to PATH if Pandoc is there + current_path <- Sys.getenv("PATH") + if (!grepl("/opt/homebrew/bin", current_path)) { + Sys.setenv(PATH = paste("/opt/homebrew/bin", current_path, sep = ":")) + } + } + with_golem_options( app = shinyApp( ui = app_ui, diff --git a/README.Rmd b/README.Rmd index 9204820..493c9eb 100644 --- a/README.Rmd +++ b/README.Rmd @@ -23,7 +23,58 @@ knitr::opts_chunk$set( [![CircleCI build status](https://circleci.com/gh/cabaJr/MACE_golem.svg?style=svg)](https://circleci.com/gh/cabaJr/MACE_golem) -The goal of MACE is to ... +The goal of maceR is to provide an interactive Shiny application for analyzing circadian locomotor activity data from laboratory mouse circadian cabinets. It streamlines data import, quality control, visualization, and statistical analysis of activity patterns across different experimental conditions. + +## Features + +- **Data Import**: Upload activity count data and experimental metadata (CSV format) +- **Data Structure Validation**: Check and validate your data before analysis +- **Interactive Subsetting**: Filter data by animal ID, sex, genotype, or time windows +- **Visualization Suite**: + - Single-plotted actograms + - Double-plotted actograms + - Daily activity patterns + - Periodograms with circadian period estimation +- **Data Export**: Download analysis tables, plots, and metadata in various formats +- **Flexible Grouping**: Analyze individual animals or aggregate by sex, genotype, or experimental group + +## Installation + +To install the development version from GitHub: + +``` r +# install.packages("devtools") +devtools::install_github("cabaJr/maceR") +``` + +## Usage + +Launch the application with: + +```{r example, eval = FALSE} +library(maceR) +run_app() +``` + +Then: + +1. Navigate to the **Input** tab and upload your activity data CSV files and metadata file +2. Review the **Data Structure** tab to verify data loading +3. Use the **Your Data** tab to inspect individual animal records +4. Set analysis parameters in the **Analysis** tab +5. Generate and export plots and tables in the **Plots** tab + +## Data Requirements + +- **Activity data files** (CSV): Must contain columns for time/timepoint and activity counts +- **Metadata file** (CSV): Must include animal identifiers and experimental factors (sex, genotype, cabinet, etc.) +- See [data-raw/maceR_toy_dataset/](data-raw/maceR_toy_dataset/) for example data format + +## Citation + +If you use maceR in your research, please cite: + +Ferrari, M. maceR: Interactive Analysis of Circadian Locomotor Activity. ## Installation diff --git a/inst/rmarkdown/templates/maceR_report/skeleton/skeleton.Rmd b/inst/rmarkdown/templates/maceR_report/skeleton/skeleton.Rmd new file mode 100644 index 0000000..9174c55 --- /dev/null +++ b/inst/rmarkdown/templates/maceR_report/skeleton/skeleton.Rmd @@ -0,0 +1,152 @@ +--- +title: "maceR Analysis Report" +subtitle: "Circadian Locomotor Activity Analysis" +date: "`r format(Sys.time(), '%d %B, %Y')`" +output: + html_document: + toc: true + toc_float: true + theme: cosmo + highlight: tango + pdf_document: + toc: true + latex_engine: xelatex +params: + plots_data: NA + metadata: NA + analysis_summary: NA +--- + +```{r setup, include=FALSE} +knitr::opts_chunk$set(echo = FALSE, warning = FALSE, message = FALSE, fig.width = 10, fig.height = 6) +library(ggplot2) +library(dplyr) +library(knitr) +library(kableExtra) +``` + +# Analysis Summary + +```{r summary} +if (!is.null(params$analysis_summary)) { + kable(params$analysis_summary, caption = "Analysis Parameters") %>% + kable_styling(bootstrap_options = c("striped", "hover", "condensed")) +} else { + cat("No analysis summary available.") +} +``` + +# Dataset Overview + +```{r metadata} +if (!is.null(params$metadata)) { + kable(params$metadata, caption = "Animal Metadata") %>% + kable_styling(bootstrap_options = c("striped", "hover", "condensed")) +} else { + cat("No metadata available.") +} +``` + +# Actograms + +## Single-Line Actograms + +```{r actograms, fig.cap="Single-line actograms showing locomotor activity patterns"} +if (!is.null(params$plots_data$actograms)) { + for (i in seq_along(params$plots_data$actograms)) { + if (!is.null(params$plots_data$actograms[[i]])) { + print(params$plots_data$actograms[[i]]) + cat("\n\n") + } + } +} else { + cat("No actograms generated.") +} +``` + +## Double-Plotted Actograms + +```{r dp-actograms, fig.cap="Double-plotted actograms for clearer visualization of circadian rhythms"} +if (!is.null(params$plots_data$dp_actograms)) { + for (i in seq_along(params$plots_data$dp_actograms)) { + if (!is.null(params$plots_data$dp_actograms[[i]])) { + print(params$plots_data$dp_actograms[[i]]) + cat("\n\n") + } + } +} else { + cat("No double-plotted actograms generated.") +} +``` + +# Daily Activity Patterns + +```{r daily-activity, fig.cap="Daily activity patterns showing average locomotor activity over 24 hours"} +if (!is.null(params$plots_data$daily_activity)) { + for (i in seq_along(params$plots_data$daily_activity)) { + if (!is.null(params$plots_data$daily_activity[[i]])) { + print(params$plots_data$daily_activity[[i]]) + cat("\n\n") + } + } +} else { + cat("No daily activity plots generated.") +} +``` + +# Periodogram Analysis + +## Periodograms + +```{r periodograms, fig.cap="Periodograms showing circadian rhythm periods"} +if (!is.null(params$plots_data$periodograms)) { + for (i in seq_along(params$plots_data$periodograms)) { + if (!is.null(params$plots_data$periodograms[[i]])) { + print(params$plots_data$periodograms[[i]]) + cat("\n\n") + } + } +} else { + cat("No periodograms generated.") +} +``` + +## Period Distribution + +```{r period-boxplots, fig.cap="Distribution of circadian periods across experimental groups"} +if (!is.null(params$plots_data$period_boxplots)) { + for (i in seq_along(params$plots_data$period_boxplots)) { + if (!is.null(params$plots_data$period_boxplots[[i]])) { + print(params$plots_data$period_boxplots[[i]]) + cat("\n\n") + } + } +} else { + cat("No period distribution plots generated.") +} +``` + +# Average Daily Activity + +```{r average-day, fig.cap="Average daily activity profiles"} +if (!is.null(params$plots_data$average_day)) { + for (i in seq_along(params$plots_data$average_day)) { + if (!is.null(params$plots_data$average_day[[i]])) { + print(params$plots_data$average_day[[i]]) + cat("\n\n") + } + } +} else { + cat("No average daily activity plots generated.") +} +``` + +# Session Information + +```{r session-info} +sessionInfo() +``` + +--- + +**Report generated by maceR on `r format(Sys.time(), '%d %B, %Y at %H:%M')`** diff --git a/inst/rmarkdown/templates/maceR_report/template.yaml b/inst/rmarkdown/templates/maceR_report/template.yaml new file mode 100644 index 0000000..3c6cc2a --- /dev/null +++ b/inst/rmarkdown/templates/maceR_report/template.yaml @@ -0,0 +1,5 @@ +name: maceR Report +description: > + Generate a comprehensive report of circadian locomotor activity analysis + including actograms, periodograms, and statistical summaries. +create_dir: false \ No newline at end of file