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/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/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..35877d3 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, @@ -820,5 +821,3 @@ Annotate <- R6::R6Class("Annotate", ### ) ) - - 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/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/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 diff --git a/maceR_Developer_Guide.pdf b/maceR_Developer_Guide.pdf new file mode 100644 index 0000000..e723c46 Binary files /dev/null and b/maceR_Developer_Guide.pdf differ