Architecture & Design

System architecture, module design, and dependency management
Authors
Affiliation

Nicholas Boehler

University of Toronto Mississauga

Hai-Ying Mary Cheng

University of Toronto Mississauga

Published

June 1, 2026

1 Rhinoverse Architecture

IMPACT-VIS is built using the Rhino framework developed by Appsilon. Rhino provides an opinionated, production-ready structure for R Shiny applications following the Rhinoverse philosophy: scalable, modular, reproducible, and professionally-engineered.

1.1 Core Rhino Tenets

  1. Modular Architecture: Code organized into self-contained, testable modules with clear boundaries
  2. Box Module System: Explicit dependency management via box::use() (not library())
  3. Convention Over Configuration: Standardized directory structure (app/, tests/, renv/)
  4. Reproducibility First: Environment locking via renv and Dockerfile
  5. Developer Experience: Integrated testing, hot-reloading, and IDE support

1.2 Rhino vs. Traditional Shiny

Aspect Traditional Shiny Rhino
Module System shiny::moduleServer() only Shiny + Box (R-level modules)
Imports library(), implicit globals box::use(), explicit dependencies
File Organization ui.R, server.R, misc files Strict app/{view,logic,styles}/ structure
Testing Manual or framework-agnostic Built-in testthat integration
Dependency Lock Manual or package-specific renv.lock + Dockerfile

Reference: Appsilon Rhino Documentation

2 Project Structure

IMPACT-VIS/
├── app.R                          # Entry point (calls rhino::app())
├── config.yml                     # Application configuration
├── dependencies.R                 # Package dependency declaration
├── renv.lock                      # Locked package versions
├── rhino.yml                      # Rhino configuration
├── app/
│   ├── main.R                     # Top-level UI/server orchestration
│   ├── view/                      # UI modules (Shiny frontend)
│   │   ├── sample_input.R
│   │   ├── plot_settings.R
│   │   ├── filters_ui.R
│   │   ├── main_panel.R
│   │   ├── variant_modal.R
│   │   ├── sv_variant_modal.R
│   │   ├── cnv_variant_modal.R
│   │   ├── export_ui.R
│   │   ├── tooltip_ui.R
│   │   ├── header_panel.R
│   │   └── __init__.R
│   ├── logic/                     # Business logic (data processing)
│   │   ├── data_manager.R
│   │   ├── plot_gds.R
│   │   ├── plot_sv.R
│   │   ├── plot_cnv.R
│   │   ├── validators.R
│   │   ├── sample_loader.R
│   │   ├── error_handler.R
│   │   ├── variant_modal_helpers.R
│   │   └── __init__.R
│   ├── styles/                    # Layout and theme styling
│   │   ├── layout.R
│   │   ├── main.scss
│   │   └── _layout.scss
│   ├── js/                        # JavaScript utilities
│   ├── static/                    # Static assets (images, etc.)
│   ├── data/                      # Sample data files (GDS, TSV, TXT)
│   └── __init__.R
├── tests/
│   ├── testthat.R
│   ├── testthat/
│   │   ├── test-load-gds.R
│   │   ├── test-load-sv.R
│   │   ├── test-validators.R
│   │   └── fixtures/
│   └── cypress/                   # E2E tests
├── vignettes/
│   └── quick-start.Rmd
├── docs/                          # Documentation (Quarto)
├── .github/workflows/
│   ├── rhino-test.yml
│   └── publish-quarto.yml
├── renv/
│   ├── activate.R
│   └── library/
├── Dockerfile
└── README.md

3 Box Module System

IMPACT-VIS uses Box for R-level modularity—think of it as an R package system embedded within your application. Each .R file is a self-contained module with explicit imports and exports, enabling true separation of concerns and preventing namespace pollution.

3.1 Why Box Instead of library()?

Problems with library(): - Pollutes global namespace with all functions - No explicit dependency declaration - Circular dependencies possible - IDE tooling limited

Box Benefits: - Only imported functions are available in scope - Dependencies explicitly declared at file top - Prevents naming conflicts - Full IDE support (autocomplete, go-to-definition) - Easier refactoring and dead-code elimination

3.2 Module Pattern: View vs. Logic

IMPACT-VIS strictly separates UI modules (app/view/) and logic modules (app/logic/). This separation is enforced by Rhino conventions.

3.2.1 Logic Modules (app/logic/*.R)

These are pure business logic with no Shiny dependencies. They export functions for data processing, validation, and plotting.

# app/logic/data_manager.R

box::use(
  SeqArray,
  readr,
  fs,
)

box::use(
  app/logic/validators,
)

# Private function (not exported)
.apply_gds_filters <- function(gds, filters) {
  # On-disk filtering logic
}

#' @export
load_gds_data <- function(gds_path, num_variants = 500, filters = list()) {
  gds <- SeqArray$seqOpen(gds_path, allow.duplicate = TRUE)
  on.exit(SeqArray$seqClose(gds), add = TRUE)
  
  validation_result <- validators$validate_gds_file(gds_path)
  if (!validation_result$is_valid) {
    warning("GDS validation failed")
    return(NULL)
  }
  
  # Apply on-disk filters
  .apply_gds_filters(gds, filters)
  
  # Extract and return data
}

Key Pattern: - All external functions are explicitly imported - Private functions prefixed with . - Only exported functions use #' @export - Functions are stateless (no side effects beyond return value) - Suitable for unit testing in isolation

3.2.2 UI Modules (app/view/*.R)

These define the Shiny user interface and reactive server logic. Each exports ui() and server() functions following the Shiny moduleServer pattern.

# app/view/sample_input.R

box::use(
  shiny[moduleServer, NS, reactive, observeEvent],
  shiny.fluent[Stack, Dropdown, PrimaryButton],
)

box::use(
  app/logic/sample_loader,
)

#' @export
ui <- function(id) {
  ns <- NS(id)
  
  Stack(
    Dropdown.shinyInput(
      id = ns("sample_picker"),
      label = "Select Sample",
      options = list()
    ),
    PrimaryButton.shinyInput(
      id = ns("load"),
      text = "Load Sample"
    )
  )
}

#' @export
server <- function(id) {
  moduleServer(id, function(input, output, session) {
    
    # Return reactive list for downstream modules
    reactive(list(
      selected_sample = reactive(input$sample_picker),
      files = reactive({
        sample_loader$expected_sample_paths(input$sample_picker)
      })
    ))
  })
}

Key Pattern: - Both ui() and server() are exported - server() returns a reactive list or reactive value - Downstream modules receive this reactive output as input - UI depends on shiny.fluent (not base Shiny)

3.3 Importing Modules

Direct Import (for ≤8 functions):

# Preferred for small APIs
box::use(
  app/logic/data_manager[load_gds_data, load_sv_data, load_cnv_data],
)

# Use directly
data <- load_gds_data(path)

Module-Qualified Import (for larger APIs):

# Use when importing >8 functions
box::use(
  app/logic/data_manager,
)

# Use with module prefix
data <- data_manager$load_gds_data(path)

Current IMPACT-VIS Module APIs:

Module Functions Import Style
data_manager 4 exported Module-qualified
plot_gds 2-3 exported Module-qualified
sample_loader 4 exported Direct
validators 4 exported Direct
error_handler 3 exported Direct
variant_modal_helpers 1 fn + 1 constant Direct
plot_combine 1 exported Direct

3.4 Module Structure in app/main.R

The orchestration happens in app/main.R, which composes all UI and logic modules:

# app/main.R

box::use(
  shiny[reactive, observe, observeEvent],
)

box::use(
  app/view/sample_input,
  app/view/plot_settings,
  app/view/main_panel,
  app/view/variant_modal,
)

box::use(
  app/logic/data_manager[load_gds_data],
  app/logic/plot_gds,
  app/logic/error_handler,
)

#' @export
ui <- function() {
  # Compose all UI modules
  tagList(
    sample_input$ui("sample_input"),
    plot_settings$ui("plot_settings"),
    main_panel$ui("main_panel"),
    variant_modal$ui("variant_modal")
  )
}

#' @export
server <- function(input, output, session) {
  # Instantiate modules and wire reactives
  
  # 1. Sample selection returns reactive list
  sample_api <- sample_input$server("sample_input")
  
  # 2. Plot settings depends on sample selection
  settings_api <- plot_settings$server("plot_settings", sample_api)
  
  # 3. Main panel uses both above
  main_panel$server(
    "main_panel", 
    sample_api, 
    settings_api,
    data_loader = load_gds_data
  )
  
  # 4. Variant modal depends on selected variant
  variant_modal$server("variant_modal", main_panel_api)
}

Module Wiring Pattern: - Each module server returns a reactive object (list, value, or function) - Downstream modules receive upstream reactives as dependencies - Reactives automatically unwrap when used (no need for ()) - Data flows unidirectionally: UI → Logic → Plotting → Display

4 Dependency Graph

graph TD
    subgraph "Entry Points"
        A["app.R"]
        B["main.R"]
    end

    subgraph "UI Layer (app/view/)"
        C["sample_input"]
        D["plot_settings"]
        E["filters_ui"]
        F["main_panel"]
        G["variant_modal"]
        H["export_ui"]
    end

    subgraph "Logic Layer (app/logic/)"
        I["data_manager"]
        J["plot_gds"]
        K["plot_sv"]
        L["plot_cnv"]
        M["validators"]
        N["error_handler"]
        O["variant_modal_helpers"]
    end

    subgraph "Style Layer (app/styles/)"
        P["layout"]
    end

    A -->|rhino::app| B
    B -->|module$server| C
    B -->|module$server| D
    B -->|module$server| E
    B -->|module$server| F
    B -->|module$server| H

    F -->|uses| I
    F -->|uses| J
    F -->|uses| K
    F -->|uses| L

    I -->|calls| M
    J -->|calls| N
    K -->|calls| N
    L -->|calls| N

    G -->|uses| O

    B -->|uses| P

    style A fill:#fff3e0
    style B fill:#fff3e0
    style C fill:#e8f5e9
    style D fill:#e8f5e9
    style E fill:#e8f5e9
    style F fill:#e8f5e9
    style G fill:#e8f5e9
    style H fill:#e8f5e9
    style I fill:#fff3e0
    style J fill:#fff3e0
    style K fill:#fff3e0
    style L fill:#fff3e0
    style M fill:#e1f5ff
    style N fill:#ffebee
    style O fill:#fff3e0
    style P fill:#f3e5f5

5 Reactive Programming

IMPACT-VIS uses Shiny’s reactive framework with careful dependency management:

5.1 Reactive Data Flow

sequenceDiagram
    participant User
    participant UI as Shiny UI
    participant React as Reactive Env
    participant Logic as Box Modules
    participant Plot as Plotting
    
    User->>UI: Select sample
    UI->>React: Update sample_input$selected
    React->>Logic: Trigger data load
    Logic->>Logic: Filter variants
    Logic->>React: Return data
    React->>Plot: Trigger plot update
    Plot->>UI: Render plot object
    UI->>User: Display plot

5.2 Reactive Pattern

# In app/main.R

# 1. Input reactive from UI module
sample_data <- sample_input$server("sample_input")  
# Returns: reactive list {$gds_path, $sv_path, $cnv_path}

# 2. Derived reactive (depends on sample)
plot_settings <- plot_settings$server("plot_settings", sample_data)
# Returns: reactive list {$plot_type, $colors, $filters}

# 3. Computed reactive (depends on both above)
filtered_data <- reactive({
  req(sample_data$gds_path())  # Ensure input is valid
  data_manager$load_gds_data(
    sample_data$gds_path(),
    filters = plot_settings$active_filters(),
    num_variants = 500
  )
})

# 4. Observer (executes side effects)
observe({
  data <- filtered_data()  # Auto-triggers when dependencies change
  if (is.null(data)) {
    error_handler$show_error("Failed to load data")
  } else {
    output$main_plot <- renderPlotly({
      plot_gds$plot_snv_scatter(data)
    })
  }
})

6 Module Responsibilities & Development Status

6.1 UI Layer (app/view/)

Module Responsibility Status Notes
sample_input Sample selection, file discovery ✅ Stable Core feature
plot_settings Plot customization controls ✅ Stable Core feature
filters_ui Variant filter controls ✅ Stable Core feature
main_panel Central plot display area ✅ Stable Core feature
variant_modal SNV detail modal ✅ Stable Core feature
sv_variant_modal SV detail modal ✅ Stable Core feature
cnv_variant_modal CNV detail modal ✅ Stable Core feature
export_ui Export options ✅ Stable Core feature
header_panel App header, title ✅ Stable Core feature
tooltip_ui Hover tooltips ✅ Stable Core feature
react.R Reactive orchestration helpers 🟡 Development Internal utilities

6.2 Logic Layer (app/logic/)

Module Responsibility Status Dependencies Notes
data_manager Load GDS/TSV/TXT, on-disk filtering ✅ Stable SeqArray, readr, validators Core data engine
plot_gds SNV/Indel panel (bottom of IMPACT Plot) ✅ Stable ggplot2, plotly High-frequency use
plot_sv SV panel with 5 tracks (top of IMPACT Plot) ✅ Stable ggplot2, plotly Conditional (SV data)
plot_cnv CNV track (middle of IMPACT Plot) ✅ Stable ggplot2, plotly Conditional (CNV data)
plot_combine IMPACT Plot 3-panel composition ✅ Stable plotly Synchronizes all 3 panels
validators File format validation ✅ Stable SeqArray Upstream of data_manager
sample_loader Sample discovery, file lookup ✅ Stable fs, stringr, purrr Sample initialization
error_handler Error/warning/success messages ✅ Stable shinyjs UI notifications
variant_modal_helpers Variant annotation lookup ✅ Stable dplyr Modal detail pages
WarningModule Development Notes

Under Active Development (react.R): Reactive orchestration utilities may change between releases. Do not depend on internal functions from this module.

Optional Data Types: Modules for SV (plot_sv) and CNV (plot_cnv) are optional—applications without corresponding data files function normally with graceful degradation.

Performance Critical: data_manager$load_gds_data() filters on-disk via SeqArray; changes here significantly impact startup time and memory usage.

6.3 Style Layer (app/styles/)

Module Responsibility
layout Container styles, responsive layout, Fluent UI configuration

7 Data Flow Diagram

graph TD
    User["👤 User"]
    UI["🎨 Shiny.Fluent UI<br/>(app/view)"]
    React["⚡ Reactive Context<br/>(app/main.R)"]
    Logic["🔧 Logic Modules<br/>(app/logic)"]
    Data["💾 File I/O<br/>(GDS/TSV/TXT)"]
    Plot["📊 Plotly Objects"]
    Display["🖥️ Browser Display"]
    
    User -->|Select Sample<br/>Adjust Filters| UI
    UI -->|Input Event| React
    React -->|Trigger Load| Logic
    Logic -->|Query| Data
    Data -->|Raw Variants| Logic
    Logic -->|Validate &<br/>Transform| Logic
    Logic -->|Return Data| React
    React -->|Trigger Plot| Logic
    Logic -->|ggplot2 →<br/>ggplotly| Plot
    Plot -->|Render| Display
    Display -->|Interactive<br/>Hover/Click| User
    
    style User fill:#e1f5ff
    style UI fill:#f3e5f5
    style React fill:#fff3e0
    style Logic fill:#e8f5e9
    style Data fill:#ffebee
    style Plot fill:#fce4ec
    style Display fill:#e0f2f1

8 Reactive Module Wiring

The reactive flow in app/main.R orchestrates all module interactions:

# Core pattern: Each module returns reactives to downstream dependents

server <- function(input, output, session) {
  # Level 1: Input modules (user selections)
  sample_api <- sample_input$server("sample_input")
  # Returns: list(gds_path = reactive, sv_path = reactive, cnv_path = reactive)
  
  # Level 2: Settings modules (depend on level 1)
  settings_api <- plot_settings$server("plot_settings", sample_api)
  # Returns: list(plot_type = reactive, filters = reactive, colors = reactive)
  
  # Level 3: Data computation (depends on level 1 & 2)
  filtered_data <- reactive({
    req(sample_api$gds_path())
    data_manager$load_gds_data(
      sample_api$gds_path(),
      filters = settings_api$filters(),
      num_variants = 500
    )
  })
  
  # Level 4: Plotting (depends on level 3)
  plot_object <- reactive({
    req(filtered_data())
    plot_gds$plot_snv_scatter(
      data = filtered_data(),
      colors = settings_api$colors()
    )
  })
  
  # Level 5: Display (depends on level 4)
  output$main_plot <- renderPlotly(plot_object())
}

Dependency Rules: - Each reactive() depends on upstream reactives - Reactives auto-unwrap in expressions (no () needed inside reactive context) - Use req() to short-circuit if dependencies are NULL - Avoid circular dependencies (detected at runtime)

9 Error Handling

IMPACT-VIS implements layered error handling:

Layer 1: Validation

# In data_manager.R
validation_result <- validate_gds_file(gds_path)
if (!validation_result$is_valid) {
  warning("GDS validation failed: ", validation_result$message)
  return(NULL)
}

Layer 2: Graceful Degradation

# In main_panel server
observe({
  data <- filtered_data()  
  if (is.null(data)) {
    # Render error message instead of plot
    output$main_plot <- renderUI({
      div(class = "alert alert-danger", "No data to display")
    })
  } else {
    output$main_plot <- renderPlotly({...})
  }
})

Layer 3: User Notification

# In error_handler.R
show_error <- function(message) {
  # Render modal dialog with error
  shinyjs::addClass("body", "modal-active")
  session$sendCustomMessage("show_error", message)
}

10 Testing Box Modules

Rhino integrates testthat for unit testing of Box modules. Each logic module should have a corresponding test file in tests/testthat/.

10.1 Testing Exported Functions

# tests/testthat/test-data-manager.R

box::use(
  app/logic/data_manager,
)

describe("data_manager$load_gds_data", {
  it("loads GDS and returns data.frame", {
    path <- "app/data/test_sample_SNV_IMPACT.gds"
    result <- data_manager$load_gds_data(path, num_variants = 100)
    
    expect_s3_class(result, "data.frame")
    expect_true(nrow(result) <= 100)
  })
  
  it("returns NULL for invalid path", {
    result <- data_manager$load_gds_data("nonexistent.gds")
    expect_null(result)
  })
  
  it("applies filters correctly", {
    path <- "app/data/test_sample_SNV_IMPACT.gds"
    
    unfiltered <- data_manager$load_gds_data(path)
    filtered <- data_manager$load_gds_data(
      path,
      filters = list(consequence_tier = "HIGH")
    )
    
    expect_true(nrow(filtered) <= nrow(unfiltered))
  })
})

10.2 Testing Non-Exported (Private) Functions

For testing internal functions (prefixed with .), access the module’s namespace:

# tests/testthat/test-validators.R

box::use(
  app/logic/validators,
)

describe("validators internals", {
  it("checks GDS structure correctly", {
    # Get internal namespace
    impl <- attr(validators, "namespace")
    
    path <- "app/data/test_sample_SNV_IMPACT.gds"
    
    # Test private function
    result <- impl$.check_gds_nodes(path)
    expect_true(result$has_impact_annotation)
  })
})

10.3 Running Tests

# Run all tests
R -e "testthat::test_dir('tests/testthat')"

# Run specific test file
R -e "testthat::test_file('tests/testthat/test-data-manager.R')"

# Run with coverage report
R -e "covr::package_coverage()" | less

10.4 Test Fixtures

Fixtures are stored in tests/testthat/fixtures/ and represent realistic (but anonymized) sample data:

  • app/data/test_sample_SNV_IMPACT.gds — Bundled demo GDS used by the app and loader tests
  • tests/testthat/fixtures/annotsv.tsv — Minimal AnnotSV validator fixture
  • tests/testthat/fixtures/sample_cnv.txt — Minimal CNV validator fixture

The bundled demo sample is intended for UI exploration, while the smaller fixtures target validator edge cases.

11 Error Handling & Resilience

IMPACT-VIS implements three-layer error handling to ensure graceful degradation and user-facing feedback:

11.1 Layer 1: Validation (Prevention)

Validation occurs at data entry—files are validated before loading:

# app/logic/data_manager.R

load_gds_data <- function(gds_path, ...) {
  # Validate first
  validation_result <- validators$validate_gds_file(gds_path)
  
  if (!validation_result$is_valid) {
    warning("GDS validation failed: ", validation_result$message)
    return(NULL)  # Signal failure to caller
  }
  
  # Proceed only if valid
  ...
}

Contract: All loaders (load_gds_data, load_sv_data, load_cnv_data) return NULL with warning() on failure—never raise exceptions.

11.2 Layer 2: Graceful Degradation (Response)

If data fails to load, modules respond appropriately without crashing:

# app/view/main_panel.R

server <- function(id, sample_api, settings_api) {
  moduleServer(id, function(input, output, session) {
    
    filtered_data <- reactive({
      data_manager$load_gds_data(...)
    })
    
    # Render error OR plot
    output$main_plot <- renderPlotly({
      data <- filtered_data()
      
      if (is.null(data)) {
        # Render error message instead of crashing
        plot_ly() %>%
          add_text(text = "No data available for selected sample",
                   textposition = "middle center",
                   showlegend = FALSE)
      } else {
        plot_gds$plot_snv_scatter(data)
      }
    })
  })
}

11.3 Layer 3: User Notification (Communication)

Errors are communicated to users via the error handler:

# app/logic/error_handler.R

show_error <- function(session, title, message, details = NULL) {
  shiny$showNotification(
    shiny$HTML(paste0("<strong>", title, "</strong><br>", message)),
    type = "error",
    duration = NULL
  )
}

show_warning <- function(session, title, message) {
  shiny$showNotification(
    shiny$HTML(paste0("<strong>", title, "</strong><br>", message)),
    type = "warning"
  )
}

show_success <- function(session, title, message) {
  shiny$showNotification(
    shiny$HTML(paste0("<strong>", title, "</strong><br>", message)),
    type = "message"
  )
}

Usage:

observe({
  data <- filtered_data()
  
  if (is.null(data)) {
    error_handler$show_error(
      session,
      "Data Loading Failed",
      "Unable to read the selected GDS file.",
      "Confirm the sample files are present and pass validation."
    )
  } else {
    error_handler$show_success(
      session,
      "Loaded",
      paste("Loaded", nrow(data), "variants")
    )
  }
})

11.4 File Access Constraints

IMPACT-VIS discovers samples from the configured data directory and groups files by naming convention. In the default configuration, that directory is app/data/.

box::use(
  app/logic/sample_loader[get_sample_names, expected_sample_paths]
)

samples <- get_sample_names("app/data")
paths <- expected_sample_paths("test_sample", "app/data")

For containerized deployments, the recommended pattern is to mount an external data directory into app/data/ rather than broadening file-system access inside the app.

12 Reproducibility & Environments

12.1 Local Development

Setup (one-time):

cd IMPACT-VIS
Rscript install.R

Run:

R -e "shiny::runApp()"

Rationale: install.R bootstraps renv and restores the package set locked in renv.lock.

12.2 Docker Deployment

Build:

docker build -t impact-vis:1.0.0 .

Run:

docker run --rm -p 3838:3838 \
  impact-vis:1.0.0

Use your own data:

docker run --rm -p 3838:3838 \
  -v /data/samples:/app/app/data:ro \
  impact-vis:1.0.0

Dockerfile Structure:

FROM rocker/r-ver:4.5.1
RUN apt-get update -y && apt-get install -y --no-install-recommends \
    build-essential pkg-config cmake \
    libnlopt-dev libuv1-dev libcurl4-openssl-dev \
    libssl-dev libxml2-dev zlib1g-dev \
    locales ca-certificates curl git && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY renv.lock ./
COPY renv/activate.R renv/settings.json ./renv/
RUN R -q -e 'install.packages(c("renv","BiocManager","rhino"), repos="https://cloud.r-project.org"); \
  options(repos = BiocManager::repositories(version = "3.22")); \
  renv::settings$bioconductor.version("3.22"); \
  renv::restore(prompt = FALSE)'
COPY . /app
CMD ["R", "-q", "-e", "options(shiny.host=Sys.getenv('SHINY_HOST', '0.0.0.0'), shiny.port=as.integer(Sys.getenv('PORT', Sys.getenv('SHINY_PORT', '3838')))); rhino::app()"]

Rationale: Docker pins the OS image, system libraries, Bioconductor release, and renv package set so runs are reproducible across hosts.

12.3 Dependency Versioning

renv.lock is the source of truth for reproducibility. It records the R version, package sources, package hashes, and Bioconductor provenance used by the project:

{
  "R": {
    "Version": "...",
    "Repositories": [{"Name": "CRAN", "URL": "..."}]
  },
  "Packages": {
    "SeqArray": {
      "Package": "SeqArray",
      "Version": "1.42.0",
      "Source": "Bioconductor",
      "Hash": "..."
    },
    ...
  }
}

Update Workflow:

# Install new package
R -e "install.packages('new_package')"

# Update lock file
R -e "renv::snapshot()"

# Commit changes
git add renv.lock
git commit -m "Update: added new_package v1.2.3"

12.4 CI/CD Reproducibility

GitHub Actions in .github/workflows/rhino-test.yml:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: Rscript -e "renv::restore(prompt = FALSE)"
      - run: Rscript -e "rhino::lint_r(); rhino::lint_js(); rhino::lint_sass()"
      - run: Rscript -e "testthat::test_dir('tests/testthat')"
      - run: npm run run-cypress

Rationale: Every pull request runs in a clean environment with locked R dependencies plus JavaScript, Sass, and Cypress checks.

13 Performance Optimization

13.1 Memory Efficiency with SeqArray

The critical performance bottleneck is loading large GDS files:

# Bad: Loads entire GDS into memory
gds <- seqOpen("file.gds")
all_data <- seqGetData(gds, "")  # Everything!
seqClose(gds)
# Good: Filter on-disk, then extract
gds <- seqOpen("file.gds")
on.exit(seqClose(gds), add = TRUE)

# Apply filters BEFORE extraction
seqSetFilter(gds, variant.sel = which_variants, sample.sel = which_samples)

# Extract only filtered data
data <- seqGetData(gds, "genotype/@data")

IMPACT-VIS Implementation: - data_manager$load_gds_data() filters on-disk via SeqArray - No intermediate matrices allocated - Memory usage scales with results, not input file size

13.2 Lazy Reactivity

Computations trigger only when dependencies change:

# This doesn't execute until someone reads `filtered_data()`
filtered_data <- reactive({
  data_manager$load_gds_data(...)  # Expensive operation
})

# UI only subscribes to what it displays
output$main_plot <- renderPlotly({
  filtered_data()  # Now it executes
})

13.3 Vectorized Operations

All variants processed at once, never in loops:

# Bad: O(n) passes through data
for (variant in 1:nrow(data)) {
  if (data$impact[variant] == "HIGH") { ... }
}

# Good: Single vectorized operation
high_impact <- data[data$impact == "HIGH", ]

14 Configuration Management

config.yml provides environment-specific settings:

default:
  rhino_log_level: !expr Sys.getenv("RHINO_LOG_LEVEL", "INFO")
  rhino_log_file: !expr Sys.getenv("RHINO_LOG_FILE", NA)
  default_sample_dir: "app/data"
  default_sample_id: "test_sample"

Access in R:

box::use(config)

sample_dir <- config$get("default_sample_dir")

15 Deployment Strategies

15.1 Local Development

# One-time setup
Rscript install.R

# Daily development
R -e "shiny::runApp()"

15.2 Docker for Production

# Build
docker build -t impact-vis:1.0.0 .

# Run with volume mount
docker run --rm -p 3838:3838 \
  -v /data/samples:/app/app/data \
  -e RHINO_LOG_LEVEL=INFO \
  impact-vis:1.0.0

15.3 Cloud Deployment

IMPACT-VIS can deploy to: - Shiny Server Pro: Enterprise self-hosted - ShinyApps.io: Rapid cloud deployment - Kubernetes: Scalable orchestration (via Docker)

16 Operational Considerations

16.1 Input Validation

All sample files are validated before the app tries to render them: - validate_gds_file() checks the SeqArray structure and required annotations - validate_sv_tsv() checks required AnnotSV columns - validate_cnv_txt() checks SCIP TXT layout and field formats - load_sample_files() and load_all_files_with_fallback() tolerate partial failure and surface actionable notifications

16.2 Resource Limits

options(shiny.maxRequestSize = 100 * 1024 ^ 2)

Large cohort deployments may need higher upload limits and more container memory.

16.3 Persistence Model

  • Variant annotations are stored beside the sample data as per-sample .rds files
  • This is simple and reproducible for local and small-team use
  • Multi-user deployments should replace file-backed state with a shared database or service

17 Extending IMPACT-VIS

17.1 Adding a New Plotting Function

Step 1: Enhance a plotting module (example: improve SV track styling)

# app/logic/plot_sv.R (modify exported function)

#' @export
plot_sv_tracks <- function(data, chr_lengths, selected_chr = "All", 
                          sv_types_to_show = c("DEL", "DUP", "INS", "INV", "BND")) {
  # Filter by SV type
  data <- data %>% filter(type %in% sv_types_to_show)
  
  # Create five horizontal tracks with improved styling
  ggplot(data, aes(x = start, y = track, color = type)) +
    geom_segment(size = 2) +
    scale_color_manual(values = sv_colors()) +
    labs(title = "Structural Variants") %>%
    ggplotly()
}

Step 2: Update IMPACT Plot composition

# app/view/main_panel.R (reactive that combines all panels)

impact_plot <- reactive({
  req(snv_data(), sv_data(), cnv_data())
  
  snv_panel <- plot_gds$plot_snv_scatter(snv_data())
  sv_panel <- plot_sv$plot_sv_tracks(sv_data())
  cnv_panel <- plot_cnv$plot_cnv_track(cnv_data())
  
  plot_combine$combine_plots(
    sv_plot = sv_panel,
    cnv_plot = cnv_panel,
    gds = list(plot = snv_panel, y_range = c(0, 100)),
    selected_chr = selected_chromosome()
  )
})

Step 3: Test the IMPACT Plot

# tests/testthat/test-plot-combine.R

test_that("IMPACT Plot renders all three panels", {
  snv_data <- sample_gds_data()
  sv_data <- sample_sv_data()
  cnv_data <- sample_cnv_data()
  
  snv_panel <- plot_gds$plot_snv_scatter(snv_data)
  sv_panel <- plot_sv$plot_sv_tracks(sv_data)
  cnv_panel <- plot_cnv$plot_cnv_track(cnv_data)
  
  result <- plot_combine$combine_plots(
    sv_plot = sv_panel,
    cnv_plot = cnv_panel,
    gds = list(plot = snv_panel, y_range = c(0, 100))
  )
  
  expect_s3_class(result, "plotly")
})

17.2 Adding a New Data Filter

Step 1: Implement in validators

# app/logic/validators.R (add new filter function)

#' @export
apply_transcript_filter <- function(data, gene_list) {
  data[data$gene %in% gene_list, ]
}

Step 2: Wire in data_manager

# app/logic/data_manager.R

if (!is.null(filters$genes)) {
  data <- validators$apply_transcript_filter(data, filters$genes)
}

Step 3: Add UI control

# app/view/filters_ui.R

ui <- function(id) {
  ns <- NS(id)
  
  TagList(
    ComboBox.shinyInput(
      id = ns("genes"),
      label = "Filter by Gene",
      multiSelect = TRUE,
      options = gene_options
    )
  )
}

18 References