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
Architecture & Design
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
- Modular Architecture: Code organized into self-contained, testable modules with clear boundaries
- Box Module System: Explicit dependency management via
box::use()(notlibrary()) - Convention Over Configuration: Standardized directory structure (
app/,tests/,renv/) - Reproducibility First: Environment locking via
renvandDockerfile - 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
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 |
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()" | less10.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 teststests/testthat/fixtures/annotsv.tsv— Minimal AnnotSV validator fixturetests/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.RRun:
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.0Use your own data:
docker run --rm -p 3838:3838 \
-v /data/samples:/app/app/data:ro \
impact-vis:1.0.0Dockerfile 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-cypressRationale: 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.015.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
.rdsfiles - 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
)
)
}