Skip to content

refactor: Move helper functions for statmodel server into separate files#155

Merged
tonywu1999 merged 7 commits intodevelfrom
refactor-statmodel
Feb 14, 2026
Merged

refactor: Move helper functions for statmodel server into separate files#155
tonywu1999 merged 7 commits intodevelfrom
refactor-statmodel

Conversation

@tonywu1999
Copy link
Copy Markdown
Contributor

@tonywu1999 tonywu1999 commented Feb 13, 2026

Motivation and Context

This PR refactors the statmodel server by extracting helper logic from the large R/module-statmodel-server.R into focused modules to improve organization, readability, and testability while preserving existing behavior and public API. The main module now orchestrates reactivity and delegates domain-specific tasks (contrast construction, visualization, results handling, modeling options, and code generation) to new helper files.

Summary of the solution

  • Decomposed a monolithic statmodel server into separate helper files that encapsulate contrast-building, plotting/visualization, results rendering/downloads, modeling options, and analysis-code generation.
  • module-statmodel-server.R now serves as an orchestration layer wiring reactivity and delegating to the new helper functions; functional behavior and exported API remain unchanged.
  • New helpers implement robust input validation, response-curve handling, safe DT cell-edit application, plot generation and download handlers, and code-generation logic for downstream MSstats calls.

Detailed changes

  • R/module-statmodel-server.R

    • Reduced to orchestration responsibilities; many assignment/operator style changes for consistency (e.g., using "=").
    • Delegates UI rendering, contrast building, plotting inputs, result extraction, code generation, and download handlers to new helper functions.
    • Integrates update_matrix_from_edit to safely apply DT cell edits.
    • Exposes existing module via statmodelServer (export preserved).
  • R/statmodel-server-comparisons.R (+417 lines)

    • Added utilities and contrast builders:
      • get_experimental_conditions(loadpage_input, preprocess_data)
      • get_contrast_panel_ui(mode, ns)
      • render_all_against_one_inputs(output, session, condition_list)
      • render_custom_pairwise_inputs(output, session, condition_list)
      • render_custom_non_pairwise_inputs(output, session, condition_list)
      • validate_contrast_inputs(input, contrast_mode, condition_list)
      • build_custom_pairwise_contrast(...)
      • build_custom_non_pairwise_contrast(...)
      • build_all_against_one_contrast(...)
      • build_all_pair_contrast(...)
      • build_response_curve_matrix(condition_list)
      • prepare_dose_response_fit(data)
      • update_matrix_from_edit(mat, info) — robust DT edit handling with coercion and rollback
    • Handles labeling, deduplication, self-pair edge cases, unit handling for dose-response, and UI integration.
    • All new functions documented with @nord where applicable.
  • R/statmodel-server-visualization.R (+132 lines)

    • New visualization helpers:
      • render_group_comparison_plot_inputs(output, session, rownames, get_data, input, loadpage_input, condition_list, contrast)
      • create_group_comparison_plot(input, loadpage_input, data_comparison) — branching for PTM/TMT/default, fold-change handling, error/spinner management
      • create_download_plot_handler(output) — download latest generated plot as ZIP
  • R/statmodel-server-results-table.R (+52 lines)

    • New result UI and export helpers:
      • render_results_table(output, session, data_comparison, SignificantProteins)
      • create_download_handlers(output, data_comparison, SignificantProteins, data_comparison_code)
      • extract_significant_proteins(data_comp, loadpage_input, signif_threshold) — handles PTM/TMT/default result shapes and thresholding
  • R/statmodel-server-download-code.R (+55 lines)

    • Added generate_analysis_code(qc_input, loadpage_input, comp_mat, input)
      • Reconstructs contrast.matrix and returns R code strings to call MSstatsTMT/MSstatsPTM/MSstats and plotting functions based on loadpage flags and inputs (no execution)
  • R/statmodel-server-options-modeling.R (+24 lines)

    • Added UI option helpers:
      • get_tmt_moderation_radio_button(loadpage_input, ns)
      • get_response_curve_fitting_options(mode, ns)
  • Tests and project files

    • Existing tests present: tests/testthat/test-module-statmodel-server.R and tests/testthat/test-utils-statmodel-server.R. No new test files added by this PR.
    • NAMESPACE remains unchanged and does not unintentionally export the new helper functions (new statmodel-related helpers are annotated @nord).

Unit tests added or modified

  • No unit tests were added or modified in this PR. Existing test files for the module remain but do not cover the substantial new logic introduced in the helper modules.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 13, 2026

Warning

Rate limit exceeded

@tonywu1999 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 49 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adds new server modules for contrast-building, plotting, results export, and analysis-code generation; the main module file received only syntactic assignment-style edits. New functions implement contrast UI/validation, matrix builders (including response-curve support), plotting and download handlers, and code assembly.

Changes

Cohort / File(s) Summary
Core server (stylistic updates)
R/module-statmodel-server.R
Syntactic/style-only edits (assignment operator and local variable style changes); no API or behavioral changes.
Contrast matrix & comparisons
R/statmodel-server-comparisons.R
New comprehensive module: experimental-condition resolution, contrast-panel UI dispatch, renderers for multiple comparison modes, input validation, builders for pairwise/all-vs-one/all-pair/non-pairwise contrasts, response-curve matrix construction, dose-response preparation, and DT edit handling (update_matrix_from_edit).
Visualization & plotting
R/statmodel-server-visualization.R
New UI renderers for comparison/protein/plot-type controls, conditional plot-option panels, plot generation branching for PTM/TMT/default, spinner/error handling, and plot download handler.
Results & exports
R/statmodel-server-results-table.R
New results UI rendering, significant-protein extraction logic, and download handlers for comparison CSV, exported code, and significant results.
Analysis code generation
R/statmodel-server-download-code.R
New generate_analysis_code() to assemble reproducible R analysis code: data preprocessing, contrast.matrix construction, model dispatch (MSstats / MSstatsPTM / MSstatsTMT), and plotting calls.
Modeling options UI
R/statmodel-server-options-modeling.R
New helpers for conditional TMT moderation radio buttons and response-curve fitting options (log-x and increasing-trend checkboxes).

Sequence Diagram(s)

sequenceDiagram
    participant User as "User (Browser)"
    participant UI as "Shiny UI"
    participant Server as "Shiny Server Modules"
    participant Model as "MSstats / PTM / TMT"
    participant FS as "Filesystem"

    User->>UI: select conditions, contrast mode, plot options
    UI->>Server: namespaced inputs (contrast, visualization, downloads)
    Server->>Server: resolve conditions, validate inputs
    Server->>Server: build/modify contrast matrix (incl. response-curve)
    Server->>Model: run groupComparison / groupComparisonPTM / groupComparisonTMT
    Model-->>Server: return results and plot artifacts
    Server->>FS: write generated analysis code and package plots
    Server-->>UI: render results table, plots, download endpoints
    UI-->>User: display outputs and provide downloads
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

Review effort 3/5

Suggested reviewers

  • sszvetecz

Poem

🐰 I hopped through rows of contrasts bright,
Built curves and plots by moonlit byte.
I stitched the UI, then danced with code,
Packaged downloads down the road —
Carrots for tests and sleepy byte-night 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (1 file):

⚔️ R/module-statmodel-server.R (content)

These conflicts must be resolved before merging into devel.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: refactoring by extracting helper functions into separate modular files (statmodel-server-comparisons.R, statmodel-server-download-code.R, statmodel-server-options-modeling.R, statmodel-server-results-table.R, and statmodel-server-visualization.R).
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor-statmodel

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

Failed to generate code suggestions for PR

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Fix all issues with AI agents
In `@R/statmodel-server-comparisons.R`:
- Around line 62-72: The loop construction in render_custom_non_pairwise_inputs
uses 1:length(condition_list()) which fails when condition_list() is empty;
change the iteration to use seq_along(condition_list()) (or
seq_len(length(condition_list()))) so the lapply call becomes
lapply(seq_along(condition_list()), ...) and similarly replace all other
occurrences of 1:length(...) noted in this file (the other blocks around the
same UI render functions that build numericInput elements using
NAMESPACE_STATMODEL$comparisons_custom_nonpairwise_weights) to avoid the 1:0
behavior and out-of-bounds indexing.
- Around line 90-116: The code currently deduplicates comp_list$dList (via
unique()) and contrast$matrix (via !duplicated on row values) independently,
which can cause a row-name length mismatch in build_custom_pairwise_contrast;
fix by deduplicating names and rows in tandem: after appending contrast$row and
the new name, compute a canonical key for each row (e.g., collapse numeric rows
to a string) and use duplicated() on those keys to derive a single set of unique
indices, then subset both contrast$matrix and comp_list$dList by the same unique
indices before assigning rownames; additionally add a guarded length check
before rownames(contrast$matrix) = comp_list$dList and apply the same
tandem-dedup pattern in the analogous block referenced (lines 118-146).

In `@R/statmodel-server-download-code.R`:
- Around line 7-11: The loop using for (i in 1:nrow(comp_mat)) is unsafe when
comp_mat has zero rows; replace that iteration with for (i in
seq_len(nrow(comp_mat))) (or guard with if (nrow(comp_mat) > 0) ...) so you
don't produce 1:0 and attempt to index non-existent rows—update the loop that
builds codes and appends to contrast.matrix (references: comp_mat, codes,
contrast.matrix) accordingly.

In `@R/statmodel-server-options-modeling.R`:
- Around line 7-11: The if in get_tmt_moderation_radio_button unguardedly
compares loadpage_input$DDA_DIA to "TMT" which can be NULL; update the condition
to first ensure DDA_DIA is present (e.g. !is.null(loadpage_input$DDA_DIA) &&
length(loadpage_input$DDA_DIA) > 0) before comparing to "TMT", and only call
create_moderation_radio_buttons(ns) when that guarded comparison is true.

In `@R/statmodel-server-results-table.R`:
- Around line 9-13: The req(SignificantProteins()) call makes the subsequent
is.null(SignificantProteins()) branch unreachable; either remove the req() guard
or replace the null check with a content-based emptiness check (e.g., test
nrow(SignificantProteins()) == 0 or length(SignificantProteins()) == 0) and keep
req(data_comparison()) so the empty-state branch (tagList(tags$br())) can run;
update the code around the SignificantProteins() usage accordingly, referencing
the req(SignificantProteins()), is.null(SignificantProteins()), and
tagList(tags$br()) locations.
- Around line 49-57: The function extract_significant_proteins has inconsistent
NA handling: the TMT branch uses a direct adj.pvalue < signif_threshold (which
preserves NA rows) while the default branch uses which(...) (dropping NAs);
update the TMT and PTM branches to match the default by using
which(data_comp$ComparisonResult$adj.pvalue < signif_threshold) and
which(data_comp$ADJUSTED.Model$adj.pvalue < signif_threshold) respectively, or
alternatively add an explicit !is.na(...) (e.g. adj.pvalue < signif_threshold &
!is.na(adj.pvalue)) to both branches so both paths consistently exclude NA
p-values when selecting rows from data_comp$ComparisonResult and
data_comp$ADJUSTED.Model in extract_significant_proteins.

In `@R/statmodel-server-visualization.R`:
- Around line 121-130: The download handler in create_download_plot_handler
registers a filename with a .zip extension but uses file.copy(latest_file, file)
which does not produce a ZIP archive; update the content function inside
downloadHandler (for NAMESPACE_STATMODEL$visualization_download_plot_results) to
create a real zip by calling zip(zipfile = file, files = latest_file) (or
alternatively change the filename() to match the true file extension), and
ensure you handle temporary paths and return/cleanup correctly so the served
file is a valid archive.
- Around line 64-118: The tryCatch around the plotting block currently swallows
errors by only calling message(), so user-facing errors (including the stop()
from the volcano-plot validation) never surface in the UI; update the error
handler (the error = function(e) block in the tryCatch that currently calls
remove_modal_spinner() and message()) to call a Shiny UI notification such as
shiny::showNotification(conditionMessage(e), type = "error", duration = NULL) or
shiny::showModal(shiny::modalDialog(title = "Error", conditionMessage(e))), keep
remove_modal_spinner() and also optionally log the error with message() or
warning() for console debugging, and ensure the validation stop() message passed
through conditionMessage(e) is what is displayed to the user.
🧹 Nitpick comments (4)
R/statmodel-server-download-code.R (2)

27-29: Redundant loadpage_input$BIO == "PTM" check inside the PTM branch.

Line 28 re-checks loadpage_input$BIO == "PTM" even though the else if on line 27 already guarantees it. This doesn't cause a bug but adds unnecessary noise.

Simplified condition
-    dt = if ((loadpage_input$BIO == "PTM" & loadpage_input$DDA_DIA == "TMT") | 
-             loadpage_input$filetype == 'phil') "TMT" else "LabelFree"
+    dt = if (loadpage_input$DDA_DIA == "TMT" || 
+             loadpage_input$filetype == "phil") "TMT" else "LabelFree"

46-52: Non-PTM generated code references groupComparisonPlots without package qualifier.

The TMT/default branch (lines 36–37) correctly qualifies MSstatsTMT::groupComparisonTMT and MSstats::groupComparison, but the plotting call on line 47 uses the bare name groupComparisonPlots without a namespace prefix. For a standalone reproducible script this may fail if the user hasn't loaded the package. The PTM branch (line 41) has the same issue with groupComparisonPlotsPTM.

R/statmodel-server-visualization.R (1)

80-111: TMT and default plot branches are nearly identical.

Lines 80–95 and 96–111 share nearly all parameters. Consider extracting the common logic into a helper to reduce duplication and ease future maintenance. This is optional for this refactoring PR.

R/statmodel-server-comparisons.R (1)

5-18: Redundant BIO == "PTM" sub-conditions clutter the logic.

The conditions on lines 6–7 and 10–11 each redundantly re-check loadpage_input$BIO == "PTM" inside an expression that is already predicated on it. Also, & (vectorized AND) is used on scalar values where && (short-circuit) is more idiomatic and safer.

Simplified version
 get_experimental_conditions = function(loadpage_input, preprocess_data) {
-  if (loadpage_input$BIO == "PTM" & 
-      ((loadpage_input$BIO == "PTM" & loadpage_input$DDA_DIA == "TMT") | 
-       loadpage_input$filetype == 'phil')) {
+  if (loadpage_input$BIO == "PTM" &&
+      (loadpage_input$DDA_DIA == "TMT" || loadpage_input$filetype == "phil")) {
     levels(preprocess_data$PTM$ProteinLevelData$Condition)
-  } else if (loadpage_input$BIO == "PTM" & 
-             (loadpage_input$BIO == "PTM" & loadpage_input$DDA_DIA != "TMT")) {
+  } else if (loadpage_input$BIO == "PTM") {
     levels(preprocess_data$PTM$ProteinLevelData$GROUP)
   } else if (loadpage_input$DDA_DIA == "TMT") {
     levels(preprocess_data$ProteinLevelData$Condition)
   } else {
     levels(preprocess_data$ProteinLevelData$GROUP)
   }
 }

Comment thread R/statmodel-server-comparisons.R
Comment on lines +90 to +116
build_custom_pairwise_contrast = function(input, condition_list, contrast, comp_list, row) {
if (input[[NAMESPACE_STATMODEL$comparisons_custom_pairwise_choice1]] == input[[NAMESPACE_STATMODEL$comparisons_custom_pairwise_choice2]]) {
return(contrast$matrix)
}

index1 = which(condition_list == input[[NAMESPACE_STATMODEL$comparisons_custom_pairwise_choice1]])
index2 = which(condition_list == input[[NAMESPACE_STATMODEL$comparisons_custom_pairwise_choice2]])

comp_list$dList = unique(c(isolate(comp_list$dList),
paste(input[[NAMESPACE_STATMODEL$comparisons_custom_pairwise_choice1]], "vs", input[[NAMESPACE_STATMODEL$comparisons_custom_pairwise_choice2]], sep = " ")))

contrast$row = matrix(row, nrow = 1)
contrast$row[index1] = 1
contrast$row[index2] = -1

if (is.null(contrast$matrix)) {
contrast$matrix = contrast$row
} else {
contrast$matrix = rbind(contrast$matrix, contrast$row)
contrast$matrix = rbind(contrast$matrix[!duplicated(contrast$matrix),])
}

rownames(contrast$matrix) = comp_list$dList
colnames(contrast$matrix) = condition_list

return(contrast$matrix)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

comp_list$dList and contrast$matrix are deduped independently — potential row-name mismatch.

comp_list$dList is deduped by name string (unique()), while contrast$matrix is deduped by row values (!duplicated()). If two differently-named comparisons happen to produce the same numeric row vector, the matrix will have fewer rows than comp_list$dList, and rownames(contrast$matrix) = comp_list$dList will fail with a dimension error.

Consider deduplicating both structures in tandem, or adding a length check before assigning row names.

Also applies to: 118-146

🤖 Prompt for AI Agents
In `@R/statmodel-server-comparisons.R` around lines 90 - 116, The code currently
deduplicates comp_list$dList (via unique()) and contrast$matrix (via !duplicated
on row values) independently, which can cause a row-name length mismatch in
build_custom_pairwise_contrast; fix by deduplicating names and rows in tandem:
after appending contrast$row and the new name, compute a canonical key for each
row (e.g., collapse numeric rows to a string) and use duplicated() on those keys
to derive a single set of unique indices, then subset both contrast$matrix and
comp_list$dList by the same unique indices before assigning rownames;
additionally add a guarded length check before rownames(contrast$matrix) =
comp_list$dList and apply the same tandem-dedup pattern in the analogous block
referenced (lines 118-146).

Comment thread R/statmodel-server-download-code.R Outdated
Comment thread R/statmodel-server-options-modeling.R Outdated
Comment on lines +7 to +11
get_tmt_moderation_radio_button <- function(loadpage_input, ns) {
if (loadpage_input$DDA_DIA == "TMT") {
create_moderation_radio_buttons(ns)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing NULL guard on loadpage_input$DDA_DIA.

get_response_curve_fitting_options (line 16) correctly guards against a NULL mode before comparing, but get_tmt_moderation_radio_button does not. If loadpage_input$DDA_DIA is NULL, the == comparison will produce logical(0), causing the if to error.

Proposed fix
 get_tmt_moderation_radio_button <- function(loadpage_input, ns) {
-  if (loadpage_input$DDA_DIA == "TMT") {
+  if (!is.null(loadpage_input$DDA_DIA) && loadpage_input$DDA_DIA == "TMT") {
     create_moderation_radio_buttons(ns)
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
get_tmt_moderation_radio_button <- function(loadpage_input, ns) {
if (loadpage_input$DDA_DIA == "TMT") {
create_moderation_radio_buttons(ns)
}
}
get_tmt_moderation_radio_button <- function(loadpage_input, ns) {
if (!is.null(loadpage_input$DDA_DIA) && loadpage_input$DDA_DIA == "TMT") {
create_moderation_radio_buttons(ns)
}
}
🤖 Prompt for AI Agents
In `@R/statmodel-server-options-modeling.R` around lines 7 - 11, The if in
get_tmt_moderation_radio_button unguardedly compares loadpage_input$DDA_DIA to
"TMT" which can be NULL; update the condition to first ensure DDA_DIA is present
(e.g. !is.null(loadpage_input$DDA_DIA) && length(loadpage_input$DDA_DIA) > 0)
before comparing to "TMT", and only call create_moderation_radio_buttons(ns)
when that guarded comparison is true.

Comment thread R/statmodel-server-results-table.R Outdated
Comment on lines +49 to +57
extract_significant_proteins = function(data_comp, loadpage_input, signif_threshold) {
if (loadpage_input$BIO == "PTM") {
data_comp$ADJUSTED.Model[data_comp$ADJUSTED.Model$adj.pvalue < signif_threshold,]
} else if (loadpage_input$DDA_DIA == "TMT") {
data_comp$ComparisonResult[data_comp$ComparisonResult$adj.pvalue < signif_threshold,]
} else {
data_comp$ComparisonResult[which(data_comp$ComparisonResult$adj.pvalue < signif_threshold),]
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent NA handling between TMT and default branches.

The TMT path (line 53) uses direct < comparison which will retain rows where adj.pvalue is NA (as NA < threshold is NA, producing NA rows). The default path (line 55) wraps the condition in which(), which drops NA rows. This inconsistency looks unintentional — consider using which() uniformly, or at minimum adding & !is.na(...) to the TMT/PTM branches.

Proposed fix — use which() consistently
 extract_significant_proteins = function(data_comp, loadpage_input, signif_threshold) {
   if (loadpage_input$BIO == "PTM") {
-    data_comp$ADJUSTED.Model[data_comp$ADJUSTED.Model$adj.pvalue < signif_threshold,]
+    data_comp$ADJUSTED.Model[which(data_comp$ADJUSTED.Model$adj.pvalue < signif_threshold),]
   } else if (loadpage_input$DDA_DIA == "TMT") {
-    data_comp$ComparisonResult[data_comp$ComparisonResult$adj.pvalue < signif_threshold,]
+    data_comp$ComparisonResult[which(data_comp$ComparisonResult$adj.pvalue < signif_threshold),]
   } else {
     data_comp$ComparisonResult[which(data_comp$ComparisonResult$adj.pvalue < signif_threshold),]
   }
 }
🤖 Prompt for AI Agents
In `@R/statmodel-server-results-table.R` around lines 49 - 57, The function
extract_significant_proteins has inconsistent NA handling: the TMT branch uses a
direct adj.pvalue < signif_threshold (which preserves NA rows) while the default
branch uses which(...) (dropping NAs); update the TMT and PTM branches to match
the default by using which(data_comp$ComparisonResult$adj.pvalue <
signif_threshold) and which(data_comp$ADJUSTED.Model$adj.pvalue <
signif_threshold) respectively, or alternatively add an explicit !is.na(...)
(e.g. adj.pvalue < signif_threshold & !is.na(adj.pvalue)) to both branches so
both paths consistently exclude NA p-values when selecting rows from
data_comp$ComparisonResult and data_comp$ADJUSTED.Model in
extract_significant_proteins.

Comment thread R/statmodel-server-visualization.R
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@R/statmodel-server-visualization.R`:
- Around line 45-57: The code calls prepare_dose_response_fit(contrast$matrix)
without guarding that contrast$matrix exists, causing a crash when it's NULL;
add a req(contrast$matrix) (or req(!is.null(contrast$matrix))) at the top of the
renderUI block so the UI only proceeds when contrast$matrix is available;
specifically update the renderUI for
output[[NAMESPACE_STATMODEL$visualization_response_curve_which_drug]] to call
req(contrast$matrix) before invoking prepare_dose_response_fit and then continue
with unique(response_curve_setup_matrix$drug) as before.
🧹 Nitpick comments (2)
R/statmodel-server-comparisons.R (2)

12-25: Redundant BIO == "PTM" checks in nested conditions.

The outer if already asserts BIO == "PTM", making the repeated checks in the inner conjunction redundant. This hurts readability and makes future maintenance error-prone. Also, prefer && over & for scalar short-circuit evaluation in if guards.

Simplified logic
 get_experimental_conditions = function(loadpage_input, preprocess_data) {
-  if (loadpage_input$BIO == "PTM" & 
-      ((loadpage_input$BIO == "PTM" & loadpage_input$DDA_DIA == "TMT") | 
-       loadpage_input$filetype == 'phil')) {
+  if (loadpage_input$BIO == "PTM" && 
+      (loadpage_input$DDA_DIA == "TMT" || loadpage_input$filetype == "phil")) {
     levels(preprocess_data$PTM$ProteinLevelData$Condition)
-  } else if (loadpage_input$BIO == "PTM" & 
-             (loadpage_input$BIO == "PTM" & loadpage_input$DDA_DIA != "TMT")) {
+  } else if (loadpage_input$BIO == "PTM") {
+    # At this point BIO == "PTM" && DDA_DIA != "TMT" && filetype != "phil"
     levels(preprocess_data$PTM$ProteinLevelData$GROUP)
   } else if (loadpage_input$DDA_DIA == "TMT") {
     levels(preprocess_data$ProteinLevelData$Condition)

223-247: loadpage_input parameter is unused.

The @param doc on line 219 already notes it as "(unused)". Consider removing it from the signature to keep the API clean, or add a comment explaining why it's retained (e.g., for interface consistency with other builders).

Same applies to input and loadpage_input in build_all_pair_contrast (line 260).

Comment thread R/statmodel-server-visualization.R
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@R/statmodel-server-comparisons.R`:
- Around line 372-374: Replace the plain stop(...) call in the else branch (the
line with stop("No intervention columns found (time, temperature, or
treatment)")) with a Shiny-friendly validation so the reactive can fail
gracefully; for example, call validate(need(...)) with the same user-facing
message (or showNotification(...) followed by req(FALSE)) so the UI displays a
friendly message instead of surfacing an unhandled error. Ensure you keep the
original message text and apply this change inside the same reactive context
where the intervention column check occurs.
- Around line 124-130: The current validation uses exact equality check wt_sum
== 0 inside the validate(need(...)) call which is fragile for floating-point
sums; change the condition to use a tolerance check (e.g., abs(wt_sum) < tol)
and pass that into need, defining a small tol (like 1e-10) nearby; apply the
same replacement for the identical check in build_custom_non_pairwise_contrast
so both validations use the tolerance-based abs(...) comparison instead of == 0.

In `@R/statmodel-server-results-table.R`:
- Around line 28-31: The download handler output$download_compar currently
always writes data_comparison()$ComparisonResult which is NULL for PTM datasets;
update output$download_compar to check loadpage_input$BIO == "PTM" and, when
true, export data_comparison()$ADJUSTED.Model (matching the logic used in
extract_significant_proteins) otherwise fall back to
data_comparison()$ComparisonResult; ensure the filename/content functions use
the chosen data object and handle NULL gracefully (e.g., skip/notify or write an
empty CSV) so PTM downloads export the ADJUSTED.Model results rather than an
empty ComparisonResult.
🧹 Nitpick comments (3)
R/statmodel-server-comparisons.R (3)

12-25: Redundant BIO == "PTM" sub-conditions clutter the logic.

Lines 14 and 18 re-check loadpage_input$BIO == "PTM" inside branches that are already guarded by loadpage_input$BIO == "PTM". This makes the intent harder to read.

Simplified logic
 get_experimental_conditions = function(loadpage_input, preprocess_data) {
-  if (loadpage_input$BIO == "PTM" & 
-      ((loadpage_input$BIO == "PTM" & loadpage_input$DDA_DIA == "TMT") | 
-       loadpage_input$filetype == 'phil')) {
+  if (loadpage_input$BIO == "PTM" & 
+      (loadpage_input$DDA_DIA == "TMT" | loadpage_input$filetype == 'phil')) {
     levels(preprocess_data$PTM$ProteinLevelData$Condition)
-  } else if (loadpage_input$BIO == "PTM" & 
-             (loadpage_input$BIO == "PTM" & loadpage_input$DDA_DIA != "TMT")) {
+  } else if (loadpage_input$BIO == "PTM") {
     levels(preprocess_data$PTM$ProteinLevelData$GROUP)
   } else if (loadpage_input$DDA_DIA == "TMT") {
     levels(preprocess_data$ProteinLevelData$Condition)
   } else {
     levels(preprocess_data$ProteinLevelData$GROUP)
   }
 }

260-289: rownames/colnames assigned inside the inner loop unnecessarily.

Lines 282-283 set rownames and colnames on every iteration of the inner loop, but only the final assignment matters. Moving them after both loops (before the return) would be cleaner and avoid repeated work.

Proposed fix
         } else {
           contrast$matrix = rbind(contrast$matrix, contrast$row)
           contrast$matrix = rbind(contrast$matrix[!duplicated(contrast$matrix),])
         }
-        
-        rownames(contrast$matrix) = comp_list$dList
-        colnames(contrast$matrix) = condition_list
       }
     }
   }
   
+  rownames(contrast$matrix) = comp_list$dList
+  colnames(contrast$matrix) = condition_list
+  
   return(contrast$matrix)
 }

301-301: Variable name matrix shadows the base R matrix() function.

Consider renaming to something like condition_df or result_df to avoid confusion if matrix() is called later in the scope.

Comment on lines +124 to +130
wt_sum = sum(sapply(seq_along(condition_list), function(i) {
input[[paste0(NAMESPACE_STATMODEL$comparisons_custom_nonpairwise_weights, i)]]
}))

validate(
need(wt_sum == 0, "The contrast weights should sum up to 0")
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Floating-point equality check on weight sum.

wt_sum == 0 (line 129) could fail for fractional weights due to floating-point arithmetic (e.g., 0.1 + 0.2 - 0.3 != 0 in R). Consider using a tolerance: abs(wt_sum) < 1e-10.

Proposed fix
-      need(wt_sum == 0, "The contrast weights should sum up to 0")
+      need(abs(wt_sum) < 1e-10, "The contrast weights should sum up to 0")

Note: Apply the same fix to the identical check on line 187 in build_custom_non_pairwise_contrast.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
wt_sum = sum(sapply(seq_along(condition_list), function(i) {
input[[paste0(NAMESPACE_STATMODEL$comparisons_custom_nonpairwise_weights, i)]]
}))
validate(
need(wt_sum == 0, "The contrast weights should sum up to 0")
)
wt_sum = sum(sapply(seq_along(condition_list), function(i) {
input[[paste0(NAMESPACE_STATMODEL$comparisons_custom_nonpairwise_weights, i)]]
}))
validate(
need(abs(wt_sum) < 1e-10, "The contrast weights should sum up to 0")
)
🤖 Prompt for AI Agents
In `@R/statmodel-server-comparisons.R` around lines 124 - 130, The current
validation uses exact equality check wt_sum == 0 inside the validate(need(...))
call which is fragile for floating-point sums; change the condition to use a
tolerance check (e.g., abs(wt_sum) < tol) and pass that into need, defining a
small tol (like 1e-10) nearby; apply the same replacement for the identical
check in build_custom_non_pairwise_contrast so both validations use the
tolerance-based abs(...) comparison instead of == 0.

Comment thread R/statmodel-server-comparisons.R
Comment on lines +28 to +31
output$download_compar = downloadHandler(
filename = function() paste("test_result-", Sys.Date(), ".csv", sep = ""),
content = function(file) write.csv(data_comparison()$ComparisonResult, file)
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for ComparisonResult and ADJUSTED.Model usage
rg -n "ComparisonResult|ADJUSTED\.Model" --type r -C 3

Repository: Vitek-Lab/MSstatsShiny

Length of output: 14576


Fix download handler to support PTM data structure.

The download handler unconditionally accesses data_comparison()$ComparisonResult, but PTM models store results under different keys: PTM.Model, PROTEIN.Model, and ADJUSTED.Model. For PTM users, ComparisonResult will be NULL, causing the download to fail or export empty data. The handler should check loadpage_input$BIO == "PTM" and export ADJUSTED.Model for PTM data, matching the logic in extract_significant_proteins (lines 44-52).

🤖 Prompt for AI Agents
In `@R/statmodel-server-results-table.R` around lines 28 - 31, The download
handler output$download_compar currently always writes
data_comparison()$ComparisonResult which is NULL for PTM datasets; update
output$download_compar to check loadpage_input$BIO == "PTM" and, when true,
export data_comparison()$ADJUSTED.Model (matching the logic used in
extract_significant_proteins) otherwise fall back to
data_comparison()$ComparisonResult; ensure the filename/content functions use
the chosen data object and handle NULL gracefully (e.g., skip/notify or write an
empty CSV) so PTM downloads export the ADJUSTED.Model results rather than an
empty ComparisonResult.

@Vitek-Lab Vitek-Lab deleted a comment from coderabbitai Bot Feb 14, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@R/statmodel-server-visualization.R`:
- Around line 125-129: The download handler's content function (content =
function(file)) assumes files <- list.files(... pattern="^Ex_") is non-empty and
uses latest_file <- files[which.max(file_info$mtime)] which fails when files is
empty; add a guard after listing files to check if length(files) == 0 and handle
it gracefully (e.g., stop() with a clear message like "No plot files available
to download" or create a placeholder/empty file), and only proceed to compute
file_info, which.max, and call file.copy(latest_file, file) when files is
non-empty; reference the symbols files, file_info, latest_file, which.max, and
file.copy in your change.
🧹 Nitpick comments (1)
R/statmodel-server-visualization.R (1)

81-112: TMT and default branches are nearly identical — consider extracting a helper.

The only difference between the TMT (lines 82–96) and default (lines 97–112) branches is the guard on loadpage_input$DDA_DIA == "TMT". The groupComparisonPlots(...) call, its arguments, and the [[1]] extraction are completely duplicated. Extracting the shared call into a local helper or collapsing the two branches would reduce maintenance burden.

♻️ Sketch
-    } else if (loadpage_input$DDA_DIA == "TMT") {
-      plot_result = groupComparisonPlots(
-        data = data_comparison$ComparisonResult,
-        ...
-      )[[1]]
     } else {
       plot_result = groupComparisonPlots(
         data = data_comparison$ComparisonResult,
-        ...
+        type = input[[NAMESPACE_STATMODEL$visualization_plot_type]],
+        sig = input[[NAMESPACE_STATMODEL$visualization_volcano_significance_cutoff]],
+        FCcutoff = fold_change_cutoff,
+        logBase.pvalue = as.numeric(input[[NAMESPACE_STATMODEL$visualization_logp_base]]),
+        ProteinName = input[[NAMESPACE_STATMODEL$visualization_volcano_display_protein_name]],
+        numProtein = input[[NAMESPACE_STATMODEL$visualization_heatmap_number_proteins]],
+        clustering = input[[NAMESPACE_STATMODEL$visualization_heatmap_cluster_option]],
+        which.Comparison = input[[NAMESPACE_STATMODEL$visualization_which_comparison]],
+        which.Protein = input[[NAMESPACE_STATMODEL$visualization_which_protein]],
+        height = input[[NAMESPACE_STATMODEL$visualization_plot_height_slider]],
+        address = "Ex_",
+        isPlotly = TRUE
       )[[1]]
     }
#!/bin/bash
# Check if TMT and default branches diverge elsewhere in the codebase
rg -n "groupComparisonPlots" --type=r -C3

Comment on lines +125 to +129
content = function(file) {
files = list.files(getwd(), pattern = "^Ex_", full.names = TRUE)
file_info = file.info(files)
latest_file = files[which.max(file_info$mtime)]
file.copy(latest_file, file)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

No guard against missing plot files.

If no files matching "^Ex_" exist in getwd() (e.g., user clicks download before generating a plot), which.max on an empty vector returns integer(0), and the subsequent file.copy/zip call will fail with an obscure error.

Proposed fix
     content = function(file) {
       files = list.files(getwd(), pattern = "^Ex_", full.names = TRUE)
+      if (length(files) == 0) {
+        showNotification("No plot files found. Please generate a plot first.", type = "warning")
+        return(NULL)
+      }
       file_info = file.info(files)
       latest_file = files[which.max(file_info$mtime)]
🤖 Prompt for AI Agents
In `@R/statmodel-server-visualization.R` around lines 125 - 129, The download
handler's content function (content = function(file)) assumes files <-
list.files(... pattern="^Ex_") is non-empty and uses latest_file <-
files[which.max(file_info$mtime)] which fails when files is empty; add a guard
after listing files to check if length(files) == 0 and handle it gracefully
(e.g., stop() with a clear message like "No plot files available to download" or
create a placeholder/empty file), and only proceed to compute file_info,
which.max, and call file.copy(latest_file, file) when files is non-empty;
reference the symbols files, file_info, latest_file, which.max, and file.copy in
your change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant