Skip to content

fix(plots): Fix plotting bugs in statistical analysis page#203

Merged
tonywu1999 merged 11 commits into
develfrom
bugfix/diann
May 3, 2026
Merged

fix(plots): Fix plotting bugs in statistical analysis page#203
tonywu1999 merged 11 commits into
develfrom
bugfix/diann

Conversation

@swaraj-neu

@swaraj-neu swaraj-neu commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Statmodel Inference:

  • Remove "display protein name" checkbox
  • Set a default comparison name
  • Pre-select single comparison instead of "all"
  • Disable Heatmap when fewer than 2 comparisons
  • Remove "all" option for all cases of volcano plot
  • Write test cases for the above changes

Motivation and Context

DIANN dataset integration exposed UX and consistency issues in the Statmodel Inference visualization flow. The goal is to simplify the visualization UI, make sensible defaults, and remove a redundant per-plot protein-name toggle so behavior is consistent across interactive plots and downloads. The PR also ensures heatmap availability is conditioned on having multiple comparisons and auto-generates unique names for custom non-pairwise comparisons.

Short solution summary

  • Remove the "display protein name" UI and always pass ProteinName = TRUE for volcano/comparison plots (including download handlers).
  • Remove the "all" choice from comparison selection; populate comparison choices from contrast row names and default to the first available comparison.
  • Pre-select a single comparison by default; enable the Heatmap option only when at least two comparisons exist.
  • Auto-default a unique "custom comparison N" name when switching to custom_nonpairwise comparison mode.
  • Update tests to match the new UI/behavior.

Detailed changes

  • R/statmodel-server-visualization.R

    • render_group_comparison_plot_inputs: derive comparison choices from rownames(contrast matrix) with no "all" option; default visualization_which_comparison to the first available choice.
    • Plot-type selector logic: Volcano and Comparison Plot are always available; Heatmap is offered only when n_comparisons >= 2. Observer for comparison-mode is no-op in response-curve mode.
    • Removed server-side guard tied to an "all" selection that previously prevented multiple-screen volcano plots.
    • All group-comparison plot generation paths (PTM, TMT, other) now force ProteinName = TRUE.
    • which.Comparison mapping: heatmap generation maps internally to "all"; non-heatmap modes use the selected visualization_which_comparison.
    • create_download_plot_handler: for PTM and non-PTM group-comparison downloads, ProteinName is forced to TRUE; which.Comparison is "all" for heatmap downloads and selected comparison otherwise.
  • R/statmodel-ui-options-visualization.R

    • create_volcano_plot_options signature simplified to create_volcano_plot_options(ns) — removed the show_protein_name parameter.
    • Removed the conditional "display protein name" checkbox from volcano options UI.
    • Minor formatting edit in create_heatmap_options (no new UI controls added).
  • R/module-statmodel-server.R

    • Added reactive behavior to auto-generate and set a unique default name for custom non-pairwise comparisons: when mode changes to custom_nonpairwise, compute the lowest-unused "custom comparison N" and update the text input.
  • R/constants.R

    • Removed the visualization_volcano_display_protein_name key from NAMESPACE_STATMODEL.
  • Plot generation functions (create_group_comparison_plot and callers)

    • Updated to always pass ProteinName = TRUE and to set which.Comparison appropriately (heatmap → "all", others → selected comparison).

Unit tests added or modified

  • tests/testthat/test-statmodel-ui-options-visualization.R

    • Removed assertions that the visualization_volcano_display_protein_name checkbox exists in the volcano UI.
    • Retained checks for visualization_which_comparison, visualization_logp_base, visualization_volcano_significance_cutoff and other volcano-related controls.
  • tests/testthat/test-utils-statmodel-server.R

    • PTM-related test for extract_significant_proteins updated to assert row counts across ADJUSTED.Model, PTM.Model, and PROTEIN.Model subsets rather than a single aggregate row-count.

- Auto detect the DIANN dataset file type and automatically toggle the checkbox for better user experience, still retaining the ability to let user manually toggle the checkbox if required
- Show a warning if the user selects an invalid state of the checkbox based on the type of file uploaded
- Handle application crashing in case if the user uses an invalid configuration while uplaoding the data
- Write test cases for all edge cases of the DIANN file version detection
- Remove "display protein name" checkbox
- Set a default comparison name
- Pre-select single comparison instead of "all"
- Disable Heatmap when fewer than 2 comparisons
- Remove "all" option for all cases of volcano plot
@swaraj-neu swaraj-neu requested a review from tonywu1999 April 24, 2026 22:43
@swaraj-neu swaraj-neu self-assigned this Apr 24, 2026
@swaraj-neu swaraj-neu added the bug Something isn't working label Apr 24, 2026
@coderabbitai

coderabbitai Bot commented Apr 24, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Comparison selector now sources choices from contrast row names (removing "all") and defaults to the first; plot-type choices update reactively (heatmap shown only when ≥2 comparisons); volcano options drop the protein-name checkbox and calls force ProteinName = TRUE; heatmap uses which.Comparison = "all". Auto-defaulting added for custom non-pairwise names.

Changes

Group-comparison & visualization

Layer / File(s) Summary
Namespace / Constants
R/constants.R
Removed visualization_volcano_display_protein_name key from NAMESPACE_STATMODEL.
UI options signature
R/statmodel-ui-options-visualization.R
create_volcano_plot_options(ns) signature updated; protein-name checkbox UI removed.
Inputs rendering
R/statmodel-server-visualization.R
visualization_which_comparison populated from rownames(contrast$matrix) (no "all"), defaults to first comparison.
Plot-type choice wiring
R/statmodel-server-visualization.R
Observer updates visualization_plot_type choices dynamically; heatmap offered only when comparisons ≥2; no-op in response-curve mode.
Plot options UI
R/statmodel-server-visualization.R, R/statmodel-ui-options-visualization.R
Volcano branch calls create_volcano_plot_options(ns) without a show_protein_name flag.
Plot generation
R/statmodel-server-visualization.R
Removed server-side stop() that blocked volcano+"all"; plotting calls (PTM/TMT/other) force ProteinName = TRUE; which.Comparison set to "all" for heatmap, otherwise uses selected comparison.
Download handler
R/statmodel-server-visualization.R
Download plot generation forces ProteinName = TRUE for group-comparison plots; which.Comparison = "all" for heatmap, otherwise uses selected comparison.
Comparison-mode auto-default
R/module-statmodel-server.R
Observer auto-computes and sets a unique "custom comparison N" default when switching to custom_nonpairwise, based on contrast$matrix row names.
Tests updated
tests/testthat/test-statmodel-ui-options-visualization.R, tests/testthat/test-utils-statmodel-server.R
Volcano options test no longer expects protein-name checkbox; PTM extract_significant_proteins test now asserts row counts per model subset (ADJUSTED.Model, PTM.Model, PROTEIN.Model).

Sequence Diagram(s)

sequenceDiagram
  participant UI as "Shiny UI"
  participant Server as "Visualization Server"
  participant Contrast as "Contrast Matrix"
  participant Plotter as "Plot Generation"

  UI->>Server: open visualization / select dataset
  Server->>Contrast: read rownames(contrast$matrix)
  Contrast-->>Server: comparisons list
  Server->>UI: populate visualization_which_comparison (selected = first)
  UI->>Server: user selects plot type
  Server->>Server: evaluate n_comparisons
  alt heatmap and n_comparisons >= 2
    Server->>Plotter: request heatmap (which.Comparison = "all", ProteinName = TRUE)
  else volcano or comparison
    Server->>Plotter: request volcano/comparison (which.Comparison = selected, ProteinName = TRUE)
  end
  Plotter-->>UI: render or provide downloadable plot
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 I hopped through contrast rows at dawn,

Found names where the old "all" was gone.
Heatmaps wait when comparisons pair,
Volcanoes show names — always there.
A tiny hop, and plots appear.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title 'fix(plots): Fix plotting bugs in statistical analysis page' accurately describes the main changes: fixing plotting-related bugs across multiple visualization functions and UI components in the statistical analysis module.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bugfix/diann

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
R/statmodel-server-visualization.R (1)

105-149: ⚠️ Potential issue | 🟠 Major

ProteinName is inconsistent between on-screen plots and download for PTM/TMT.

On-screen plotting passes ProteinName = TRUE for PTM (line 113) and TMT (line 124), but the download handler passes ProteinName = FALSE for PTM (line 290) and for the non-PTM branch — which also covers TMT — at line 301. The PR description says "Remove the display protein name checkbox" and the volcano UI now hardcodes to FALSE, so the intent appears to be uniformly FALSE. The asymmetry means a downloaded volcano plot will look different from the one shown in-browser for the same data (labels missing in the PDF).

Please align the two flows. If the intent is uniformly FALSE, set lines 113 and 124 to FALSE. If the intent is to keep PTM/TMT labeled, set lines 290 and 301 to TRUE (or branch on loadpage_input()$BIO/DDA_DIA matching the on-screen branches).

Also applies to: 283-310

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@R/statmodel-server-visualization.R` around lines 105 - 149, The download vs
on-screen plotting code is inconsistent for the ProteinName argument causing
mismatched labels: update the calls so both UI rendering and the download
handler use the same ProteinName value; specifically edit the
groupComparisonPlotsPTM call (plot_result in the first tryCatch branch) and the
groupComparisonPlots call for the TMT branch (the branches that set ProteinName
= TRUE) to match the download-handler branches (or vice versa) so that
ProteinName is uniformly FALSE or TRUE across both flows (refer to function
calls groupComparisonPlotsPTM and groupComparisonPlots and the variables
loadpage_input$BIO / loadpage_input$DDA_DIA to decide which value to apply).
🧹 Nitpick comments (7)
tests/testthat/test-statmodel-ui-options-visualization.R (1)

42-43: Constant referenced here is now dead code.

NAMESPACE_STATMODEL$visualization_volcano_display_protein_name is still defined in R/constants.R (around line 39) but is no longer referenced anywhere in app code after this PR — only this negative-assertion test uses it. Consider removing the constant from R/constants.R and changing this assertion to a string literal (e.g., expect_false(grepl("visualization_volcano_display_protein_name", ui_html))) to avoid leaving an orphan namespace key that could mislead future contributors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/testthat/test-statmodel-ui-options-visualization.R` around lines 42 -
43, Remove the dead constant
NAMESPACE_STATMODEL$visualization_volcano_display_protein_name from
R/constants.R and update the test in
tests/testthat/test-statmodel-ui-options-visualization.R to assert against the
literal string used in the UI (e.g.,
"visualization_volcano_display_protein_name") instead of referencing the removed
constant; ensure no other code references
NAMESPACE_STATMODEL$visualization_volcano_display_protein_name before deleting
the constant and adjust any documentation or tests that referenced it.
R/statmodel-server-visualization.R (1)

16-39: updateSelectInput will silently reset the user's plot-type selection on every comparison-mode change.

This observe() fires every time input[[comparison_mode]] or rownames() changes. When comparison_mode changes but no comparisons have been submitted yet, rownames() returns NULL, so n_comparisons == 0 and choices collapse to Volcano + Comparison. updateSelectInput() is called with choices only (no selected), which causes Shiny to fall back to the first choice if the previous selection is no longer in the list — silently moving the user off Heatmap they previously selected.

Two suggestions:

  1. Preserve selection when possible by passing selected = isolate(input[[NAMESPACE_STATMODEL$visualization_plot_type]]) if it remains valid.
  2. De-duplicate the choices vector — only the Heatmap entry differs:
♻️ Proposed simplification
-    choices <- c(
-      "Volcano Plot" = CONSTANTS_STATMODEL$plot_type_volcano_plot,
-      "Comparison Plot" = CONSTANTS_STATMODEL$plot_type_comparison_plot
-    )
-    if (n_comparisons >= 2) {
-      choices <- c(
-        "Volcano Plot" = CONSTANTS_STATMODEL$plot_type_volcano_plot,
-        "Heatmap" = CONSTANTS_STATMODEL$plot_type_heatmap,
-        "Comparison Plot" = CONSTANTS_STATMODEL$plot_type_comparison_plot
-      )
-    }
-    updateSelectInput(session, NAMESPACE_STATMODEL$visualization_plot_type,
-                      choices = choices)
+    choices <- c(
+      "Volcano Plot"    = CONSTANTS_STATMODEL$plot_type_volcano_plot,
+      if (n_comparisons >= 2) c("Heatmap" = CONSTANTS_STATMODEL$plot_type_heatmap),
+      "Comparison Plot" = CONSTANTS_STATMODEL$plot_type_comparison_plot
+    )
+    current <- isolate(input[[NAMESPACE_STATMODEL$visualization_plot_type]])
+    keep <- if (!is.null(current) && current %in% choices) current else choices[[1]]
+    updateSelectInput(session, NAMESPACE_STATMODEL$visualization_plot_type,
+                      choices = choices, selected = keep)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@R/statmodel-server-visualization.R` around lines 16 - 39, The observe block
that builds choices and calls updateSelectInput resets the user's selection when
choices change; modify it to build a base choices vector once (using "Volcano
Plot" and "Comparison Plot" from CONSTANTS_STATMODEL) and only insert the
"Heatmap" entry when length(rownames()) >= 2, then call
updateSelectInput(session, NAMESPACE_STATMODEL$visualization_plot_type, choices
= choices, selected = {sel <-
isolate(input[[NAMESPACE_STATMODEL$visualization_plot_type]]); if (!is.null(sel)
&& sel %in% choices) sel else NULL}); this preserves the current selection when
still valid and avoids duplicate entries.
R/utils.R (2)

35-43: Detection logic looks solid; minor note on legacy precedence semantics.

The logic correctly distinguishes DIANN 2.0+ (numbered Fr.<n>.Quantity columns) from legacy format. The "legacy takes precedence" rule (returning FALSE when both signatures appear) is intentional and tested, but worth confirming against real-world data: if a future DIANN 2.x export ever retains a FragmentQuantCorrected-style column for backwards compatibility, the auto-detection would silently treat it as 1.x. Consider adding a brief comment on the rationale (e.g., "if both are present, legacy column is the authoritative one for older converters").

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@R/utils.R` around lines 35 - 43, Add a short clarifying comment inside the
.is_diann_2plus function explaining why legacy fragment columns take precedence
when both signatures are present (e.g., some exporters keep a legacy
FragmentQuantCorrected column for compatibility), so future readers understand
the intentional precedence logic around has_numbered_fragments and
has_legacy_fragments checks.

8-24: Consider using arrow::ParquetFileReader for more idiomatic schema extraction.

The current arrow::open_dataset(filepath, format = "parquet")$schema approach works for single files, but arrow::ParquetFileReader$create(filepath)$GetSchema() is the more direct and idiomatic choice. open_dataset() is designed for multi-file datasets and loads metadata without data, making it functional but less explicit than ParquetFileReader for this use case.

For reference: ParquetFileReader$create(filepath)$GetSchema() reads only file metadata and returns the Schema directly—making the intent clearer and the call more efficient.

No changes needed for the data.table::fread behavior; it returns a data.table (which inherits from data.frame), and the Roxygen docstring is accurate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@R/utils.R` around lines 8 - 24, Replace the parquet schema extraction in
.read_preview with the ParquetFileReader API: instead of using
arrow::open_dataset(filepath, format = "parquet")$schema, call
arrow::ParquetFileReader$create(filepath)$GetSchema() to obtain the schema
(assign to schema or schema_obj) and then derive col_names and the empty_df the
same way; keep the rest of .read_preview (including the fread branch and the
tryCatch) unchanged.
R/module-loadpage-server.R (3)

85-108: Forward reference: last_detected_diann_format is used at Line 95 but declared at Line 111.

R's lazy evaluation of observer bodies makes this work in practice (the observe body is not executed until after moduleServer's full body runs and reactivity fires), but it's fragile and confusing for readers. Consider hoisting the reactiveVal(NULL) declaration above this observe() block so the dependency order is clear.

♻️ Suggested reordering
+    # Track last detected DIANN format to avoid redundant notifications
+    last_detected_diann_format <- reactiveVal(NULL)
+
     # Read first 100 rows for preview-based UI features.
     # Supported: Metamorpheus PTM (modification ID dropdown), DIANN (version auto-detection).
     # TODO: Extend to other input formats (Spectronaut, MaxQuant) as needed.
     observe({
       should_preview <- (isTRUE(input$filetype == "meta") && isTRUE(input$BIO == "PTM")) ||
                        (isTRUE(input$filetype == "diann") && isTRUE(input$BIO != "PTM"))
       ...
     })

-    # Track last detected DIANN format to avoid redundant notifications
-    last_detected_diann_format <- reactiveVal(NULL)
-
     # Auto-toggle DIANN 2.0+ checkbox based on detected file format
     observe({
       ...
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@R/module-loadpage-server.R` around lines 85 - 108, The observer uses
last_detected_diann_format but that reactiveVal is declared later, which relies
on lazy evaluation and is fragile; move the reactiveVal declaration for
last_detected_diann_format(NULL) so it appears before the observe({ ... }) block
that references it (the observe that computes should_preview, reads
main_data_file(), calls .read_preview(), and sets preview_data); this ensures
the reactive dependency is defined before use and makes the module's
initialization order explicit.

135-150: Mismatch observer also fires on programmatic updates; verify no duplicate notifications.

observeEvent(input$diann_2plus, ...) will fire both for user toggles AND for the updateCheckboxInput() call in the auto-toggle observer (Line 123). In the programmatic case, the new input$diann_2plus value should match detected_2plus (no warning), so this is correct by construction — but it's a subtle invariant. A short inline comment explaining why no warning is shown in the auto-toggle path would help future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@R/module-loadpage-server.R` around lines 135 - 150, The observer on
input$diann_2plus (observeEvent(input$diann_2plus, ...)) can be triggered by
both user toggles and programmatic updates from the auto-toggle logic
(updateCheckboxInput), so add a short inline comment above or inside this
observer explaining that the auto-toggle path will set input$diann_2plus to
match the detected_2plus (via .is_diann_2plus(preview_data())), therefore no
warning will be emitted for programmatic updates and duplicate notifications are
not expected; reference the auto-toggle/updateCheckboxInput location and the
.is_diann_2plus and preview_data() calls so future maintainers understand the
invariant.

88-108: Cross-format reset can cause stale-preview re-detection.

When the user is on meta+PTM and a new meta file is previewed, this block calls last_detected_diann_format(NULL) (Line 95) regardless of file type. If the user then switches back to diann, preview_data may carry over from the prior meta file (or be NULL), and the auto-toggle observer's previous == is_2plus change-detection is reset, potentially re-firing the "Detected DIANN ..." notification on stale data.

Consider scoping the reset specifically to the DIANN branch:

♻️ Suggested scoping
       if (should_preview) {
         file_info <- main_data_file()
         if (!is.null(file_info)) {
-          # Reset DIANN detection tracker so a new file re-triggers the notification
-          last_detected_diann_format(NULL)
+          # Reset DIANN detection tracker so a new file re-triggers the notification
+          if (isTRUE(input$filetype == "diann")) {
+            last_detected_diann_format(NULL)
+          }
           preview <- .read_preview(file_info$datapath, file_info$name)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@R/module-loadpage-server.R` around lines 88 - 108, The reset of
last_detected_diann_format() is unconditional and should only run when we're
previewing a DIANN file; move the call to last_detected_diann_format(NULL) into
the DIANN branch (the branch where should_preview is true due to
isTRUE(input$filetype == "diann") && isTRUE(input$BIO != "PTM")), and only
execute it when file_info is non-NULL (i.e., alongside preview <-
.read_preview(...)) so switching between meta and diann doesn't reset the
DIANN-detection tracker on non-DIANN previews; keep preview_data(preview) /
preview_data(NULL) behavior as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@R/statmodel-ui-comparisons.R`:
- Around line 76-77: The default comparison name "custom comparison" can
collide; update the textInput handling for
ns(NAMESPACE_STATMODEL$comparisons_custom_nonpairwise_name) so names are unique
before adding to the contrast matrix built by matrix_build(): either generate a
unique default by auto-suffixing (e.g., "custom comparison 1", "custom
comparison 2") based on existing rownames(matrix_build(...)) or validate on
submit and call showNotification with an error if the chosen name already
exists; ensure this prevents duplicate rownames that would break the
selectInput(..., comparison_names, selected = comparison_names[1]) logic used
elsewhere.

---

Outside diff comments:
In `@R/statmodel-server-visualization.R`:
- Around line 105-149: The download vs on-screen plotting code is inconsistent
for the ProteinName argument causing mismatched labels: update the calls so both
UI rendering and the download handler use the same ProteinName value;
specifically edit the groupComparisonPlotsPTM call (plot_result in the first
tryCatch branch) and the groupComparisonPlots call for the TMT branch (the
branches that set ProteinName = TRUE) to match the download-handler branches (or
vice versa) so that ProteinName is uniformly FALSE or TRUE across both flows
(refer to function calls groupComparisonPlotsPTM and groupComparisonPlots and
the variables loadpage_input$BIO / loadpage_input$DDA_DIA to decide which value
to apply).

---

Nitpick comments:
In `@R/module-loadpage-server.R`:
- Around line 85-108: The observer uses last_detected_diann_format but that
reactiveVal is declared later, which relies on lazy evaluation and is fragile;
move the reactiveVal declaration for last_detected_diann_format(NULL) so it
appears before the observe({ ... }) block that references it (the observe that
computes should_preview, reads main_data_file(), calls .read_preview(), and sets
preview_data); this ensures the reactive dependency is defined before use and
makes the module's initialization order explicit.
- Around line 135-150: The observer on input$diann_2plus
(observeEvent(input$diann_2plus, ...)) can be triggered by both user toggles and
programmatic updates from the auto-toggle logic (updateCheckboxInput), so add a
short inline comment above or inside this observer explaining that the
auto-toggle path will set input$diann_2plus to match the detected_2plus (via
.is_diann_2plus(preview_data())), therefore no warning will be emitted for
programmatic updates and duplicate notifications are not expected; reference the
auto-toggle/updateCheckboxInput location and the .is_diann_2plus and
preview_data() calls so future maintainers understand the invariant.
- Around line 88-108: The reset of last_detected_diann_format() is unconditional
and should only run when we're previewing a DIANN file; move the call to
last_detected_diann_format(NULL) into the DIANN branch (the branch where
should_preview is true due to isTRUE(input$filetype == "diann") &&
isTRUE(input$BIO != "PTM")), and only execute it when file_info is non-NULL
(i.e., alongside preview <- .read_preview(...)) so switching between meta and
diann doesn't reset the DIANN-detection tracker on non-DIANN previews; keep
preview_data(preview) / preview_data(NULL) behavior as-is.

In `@R/statmodel-server-visualization.R`:
- Around line 16-39: The observe block that builds choices and calls
updateSelectInput resets the user's selection when choices change; modify it to
build a base choices vector once (using "Volcano Plot" and "Comparison Plot"
from CONSTANTS_STATMODEL) and only insert the "Heatmap" entry when
length(rownames()) >= 2, then call updateSelectInput(session,
NAMESPACE_STATMODEL$visualization_plot_type, choices = choices, selected = {sel
<- isolate(input[[NAMESPACE_STATMODEL$visualization_plot_type]]); if
(!is.null(sel) && sel %in% choices) sel else NULL}); this preserves the current
selection when still valid and avoids duplicate entries.

In `@R/utils.R`:
- Around line 35-43: Add a short clarifying comment inside the .is_diann_2plus
function explaining why legacy fragment columns take precedence when both
signatures are present (e.g., some exporters keep a legacy
FragmentQuantCorrected column for compatibility), so future readers understand
the intentional precedence logic around has_numbered_fragments and
has_legacy_fragments checks.
- Around line 8-24: Replace the parquet schema extraction in .read_preview with
the ParquetFileReader API: instead of using arrow::open_dataset(filepath, format
= "parquet")$schema, call arrow::ParquetFileReader$create(filepath)$GetSchema()
to obtain the schema (assign to schema or schema_obj) and then derive col_names
and the empty_df the same way; keep the rest of .read_preview (including the
fread branch and the tryCatch) unchanged.

In `@tests/testthat/test-statmodel-ui-options-visualization.R`:
- Around line 42-43: Remove the dead constant
NAMESPACE_STATMODEL$visualization_volcano_display_protein_name from
R/constants.R and update the test in
tests/testthat/test-statmodel-ui-options-visualization.R to assert against the
literal string used in the UI (e.g.,
"visualization_volcano_display_protein_name") instead of referencing the removed
constant; ensure no other code references
NAMESPACE_STATMODEL$visualization_volcano_display_protein_name before deleting
the constant and adjust any documentation or tests that referenced it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 43b9cbd3-27c9-478a-8030-885d19184be0

📥 Commits

Reviewing files that changed from the base of the PR and between e225219 and f547738.

📒 Files selected for processing (8)
  • R/module-loadpage-server.R
  • R/module-loadpage-ui.R
  • R/statmodel-server-visualization.R
  • R/statmodel-ui-comparisons.R
  • R/statmodel-ui-options-visualization.R
  • R/utils.R
  • tests/testthat/test-statmodel-ui-options-visualization.R
  • tests/testthat/test-utils.R

Comment thread R/statmodel-ui-comparisons.R Outdated
Comment on lines +76 to +77
textInput(ns(NAMESPACE_STATMODEL$comparisons_custom_nonpairwise_name),
label = "Comparison Name", value = ""),
label = "Comparison Name", value = "custom comparison"),

@coderabbitai coderabbitai Bot Apr 24, 2026

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

Default comparison name may collide on multiple submissions.

Setting a static default of "custom comparison" means a user who clicks "Add" twice in the non-pairwise panel without editing the name will submit two comparisons sharing the same row name. Depending on how matrix_build() aggregates rows, this may overwrite the previous entry or produce a contrast matrix with duplicated rownames (which can break the downstream selectInput(..., comparison_names, selected = comparison_names[1]) logic in R/statmodel-server-visualization.R, since rownames() is used as choices/values).

Consider either:

  • Auto-suffixing the default (e.g., "custom comparison 1", "custom comparison 2") based on existing rownames, or
  • Validating uniqueness on submit and surfacing a showNotification if the name already exists.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@R/statmodel-ui-comparisons.R` around lines 76 - 77, The default comparison
name "custom comparison" can collide; update the textInput handling for
ns(NAMESPACE_STATMODEL$comparisons_custom_nonpairwise_name) so names are unique
before adding to the contrast matrix built by matrix_build(): either generate a
unique default by auto-suffixing (e.g., "custom comparison 1", "custom
comparison 2") based on existing rownames(matrix_build(...)) or validate on
submit and call showNotification with an error if the chosen name already
exists; ensure this prevents duplicate rownames that would break the
selectInput(..., comparison_names, selected = comparison_names[1]) logic used
elsewhere.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess this is valid, but honestly not a big deal

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@tonywu1999 Understood — it's a low-risk edge case and the current UX already requires the user to intentionally click "Add" twice without changing the name. Happy to defer this to a future cleanup if it ever becomes a practical issue.

ui_html <- htmltools::renderTags(result)$html
expect_true(grepl(NAMESPACE_STATMODEL$visualization_which_comparison, ui_html),
info = "Which comparison namespace should be present")
expect_true(grepl(NAMESPACE_STATMODEL$visualization_volcano_display_protein_name, ui_html),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think you can just get rid of this unit test. Also, can the constant be deleted too?

Comment thread R/statmodel-ui-comparisons.R Outdated
Comment on lines +76 to +77
textInput(ns(NAMESPACE_STATMODEL$comparisons_custom_nonpairwise_name),
label = "Comparison Name", value = ""),
label = "Comparison Name", value = "custom comparison"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess this is valid, but honestly not a big deal

selectInput(ns(NAMESPACE_STATMODEL$visualization_which_comparison),
label = h5("Select comparison to plot"),
c("all", rownames()), selected = "all")
comparison_names, selected = comparison_names[1])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What happens with Heatmap plots here? I think Heatmap plots benefit from being able to select "all" (does a single comparison selected for a Heatmap work?).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@tonywu1999 You're right, it doesn't work. for a single comparison
Removed the "Select comparison to plot" dropdown entirely for Heatmap since heatmaps inherently visualize all comparisons together. The dropdown still appears for Volcano Plot and Comparison Plot where selecting a single comparison makes sense. Also hardcoded which.Comparison = "all" for Heatmap in create_group_comparison_plot as a safety net in case the dropdown's last input value persists when switching plot types.

Comment thread R/statmodel-server-visualization.R Outdated
FCcutoff = fold_change_cutoff,
logBase.pvalue = as.numeric(input[[NAMESPACE_STATMODEL$visualization_logp_base]]),
ProteinName = input[[NAMESPACE_STATMODEL$visualization_volcano_display_protein_name]],
ProteinName = FALSE,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When you hover over the volcano plot, can you see the protein name when hovering over a dot? This is the behavior we'd want.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@tonywu1999 Yes, hovering over a dot in the Volcano plot shows the protein name in the tooltip.

- Update the significant protein extraction test

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
R/statmodel-server-visualization.R (2)

8-14: Comparison selector resets user selection whenever rownames() changes.

renderUI rebuilds the selectInput from scratch every time rownames() updates, so the user's currently-selected comparison is silently reset to comparison_names[1] whenever the contrast matrix is edited or a new comparison row is added. Consider rendering the input once and updating choices via updateSelectInput, preserving the existing selection when it is still present in the new comparison_names.

♻️ Sketch
# Render the input once, then keep choices in sync without losing selection.
output[[NAMESPACE_STATMODEL$visualization_which_comparison]] = renderUI({
  selectInput(ns(NAMESPACE_STATMODEL$visualization_which_comparison),
              label = h5("Select comparison to plot"),
              choices = character(0))
})

observe({
  comparison_names <- rownames()
  req(length(comparison_names) > 0)
  current <- isolate(input[[NAMESPACE_STATMODEL$visualization_which_comparison]])
  selected <- if (!is.null(current) && current %in% comparison_names) current else comparison_names[1]
  updateSelectInput(session,
                    NAMESPACE_STATMODEL$visualization_which_comparison,
                    choices = comparison_names,
                    selected = selected)
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@R/statmodel-server-visualization.R` around lines 8 - 14, The current renderUI
for output[[NAMESPACE_STATMODEL$visualization_which_comparison]] rebuilds the
selectInput from scratch using rownames(), causing the user's selection to reset
to comparison_names[1] whenever rownames() changes; change this to render the
selectInput once (with empty choices) and add an observe that watches
rownames(), computes comparison_names and the preserved selected value (use
isolate(input[[NAMESPACE_STATMODEL$visualization_which_comparison]]) and keep it
if it exists in comparison_names, otherwise default to comparison_names[1]),
then call updateSelectInput(session,
NAMESPACE_STATMODEL$visualization_which_comparison, choices = comparison_names,
selected = selected) to update options without losing selection.

18-39: LGTM — Heatmap correctly gated on ≥2 comparisons.

This addresses the prior reviewer concern about Heatmap with a single comparison. One small nit: the choices vector is built twice; you can avoid the duplication by inserting Heatmap conditionally. No behavior change needed.

♻️ Optional dedup
-    choices <- c(
-      "Volcano Plot" = CONSTANTS_STATMODEL$plot_type_volcano_plot,
-      "Comparison Plot" = CONSTANTS_STATMODEL$plot_type_comparison_plot
-    )
-    if (n_comparisons >= 2) {
-      choices <- c(
-        "Volcano Plot" = CONSTANTS_STATMODEL$plot_type_volcano_plot,
-        "Heatmap" = CONSTANTS_STATMODEL$plot_type_heatmap,
-        "Comparison Plot" = CONSTANTS_STATMODEL$plot_type_comparison_plot
-      )
-    }
+    choices <- c("Volcano Plot" = CONSTANTS_STATMODEL$plot_type_volcano_plot)
+    if (n_comparisons >= 2) {
+      choices <- c(choices, "Heatmap" = CONSTANTS_STATMODEL$plot_type_heatmap)
+    }
+    choices <- c(choices, "Comparison Plot" = CONSTANTS_STATMODEL$plot_type_comparison_plot)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@R/statmodel-server-visualization.R` around lines 18 - 39, The choices vector
is duplicated; instead of redefining it for n_comparisons >= 2, initialize
choices with Volcano and Comparison Plot and then conditionally insert the
Heatmap when n_comparisons >= 2; update the observe block that sets choices
(variables: comparison_names, n_comparisons, choices) and leave the
updateSelectInput(session, NAMESPACE_STATMODEL$visualization_plot_type, choices
= choices) call unchanged so behavior is identical but the Heatmap option is
appended only when needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@R/module-statmodel-server.R`:
- Around line 133-147: The observer that auto-generates the default name (the
block that currently calls updateTextInput for
NAMESPACE_STATMODEL$comparisons_custom_nonpairwise_name) runs whenever
contrast$matrix changes and thus can overwrite user edits; change it to only run
on mode entry or when a genuinely new autogenerated name is required: replace
observe({...}) with observeEvent(input[[NAMESPACE_STATMODEL$comparison_mode]], {
... }) and inside the handler read contrast$matrix with isolate(contrast$matrix)
(or check the current input value via
input[[NAMESPACE_STATMODEL$comparisons_custom_nonpairwise_name]] and only call
updateTextInput when that value is empty or exactly matches a previous
autogenerated "custom comparison N"); keep the same name-generation loop but
ensure updateTextInput is conditional so it won't clobber user-typed names.

---

Nitpick comments:
In `@R/statmodel-server-visualization.R`:
- Around line 8-14: The current renderUI for
output[[NAMESPACE_STATMODEL$visualization_which_comparison]] rebuilds the
selectInput from scratch using rownames(), causing the user's selection to reset
to comparison_names[1] whenever rownames() changes; change this to render the
selectInput once (with empty choices) and add an observe that watches
rownames(), computes comparison_names and the preserved selected value (use
isolate(input[[NAMESPACE_STATMODEL$visualization_which_comparison]]) and keep it
if it exists in comparison_names, otherwise default to comparison_names[1]),
then call updateSelectInput(session,
NAMESPACE_STATMODEL$visualization_which_comparison, choices = comparison_names,
selected = selected) to update options without losing selection.
- Around line 18-39: The choices vector is duplicated; instead of redefining it
for n_comparisons >= 2, initialize choices with Volcano and Comparison Plot and
then conditionally insert the Heatmap when n_comparisons >= 2; update the
observe block that sets choices (variables: comparison_names, n_comparisons,
choices) and leave the updateSelectInput(session,
NAMESPACE_STATMODEL$visualization_plot_type, choices = choices) call unchanged
so behavior is identical but the Heatmap option is appended only when needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dd4a993c-0680-4a34-be05-75b9b31318ab

📥 Commits

Reviewing files that changed from the base of the PR and between f547738 and b46e6cd.

📒 Files selected for processing (3)
  • R/module-statmodel-server.R
  • R/statmodel-server-visualization.R
  • tests/testthat/test-utils-statmodel-server.R

Comment on lines +133 to +147

# Auto-generate unique default name for non-pairwise comparisons
observe({
req(input[[NAMESPACE_STATMODEL$comparison_mode]] ==
CONSTANTS_STATMODEL$comparison_mode_custom_nonpairwise)
existing_names <- if (!is.null(contrast$matrix)) rownames(contrast$matrix) else character(0)
# Find the next available "custom comparison N"
n <- 1
while (paste0("custom comparison ", n) %in% existing_names) {
n <- n + 1
}
updateTextInput(session,
NAMESPACE_STATMODEL$comparisons_custom_nonpairwise_name,
value = paste0("custom comparison ", n))
})

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

Auto-default observer can clobber user-typed comparison names.

This observe re-fires not only when input[[comparison_mode]] switches to custom_nonpairwise, but also any time contrast$matrix changes (e.g., the input$table_cell_edit observer at lines 396–400, or after matrix_build mutates the matrix). Each time it fires it unconditionally calls updateTextInput(...), which will overwrite any name the user has typed into the comparison name field (for example, while they are editing a cell in the existing matrix or right after submitting a row).

Consider gating the update so it only runs on mode entry or when a new row was actually appended (e.g., scope the dependency with observeEvent(input[[comparison_mode]], ...) and read contrast$matrix inside isolate(), or only update when the current text input value already matches a previously-generated "custom comparison N").

🛠️ Suggested fix sketch
-      # Auto-generate unique default name for non-pairwise comparisons
-      observe({
-        req(input[[NAMESPACE_STATMODEL$comparison_mode]] ==
-              CONSTANTS_STATMODEL$comparison_mode_custom_nonpairwise)
-        existing_names <- if (!is.null(contrast$matrix)) rownames(contrast$matrix) else character(0)
-        # Find the next available "custom comparison N"
-        n <- 1
-        while (paste0("custom comparison ", n) %in% existing_names) {
-          n <- n + 1
-        }
-        updateTextInput(session,
-                        NAMESPACE_STATMODEL$comparisons_custom_nonpairwise_name,
-                        value = paste0("custom comparison ", n))
-      })
+      # Auto-generate unique default name for non-pairwise comparisons.
+      # Re-evaluate on mode switch or when rows are added; do not overwrite a
+      # name the user has customized away from the auto-generated pattern.
+      observeEvent(
+        list(input[[NAMESPACE_STATMODEL$comparison_mode]],
+             nrow(contrast$matrix)),
+        {
+          req(input[[NAMESPACE_STATMODEL$comparison_mode]] ==
+                CONSTANTS_STATMODEL$comparison_mode_custom_nonpairwise)
+          existing_names <- if (!is.null(contrast$matrix)) rownames(contrast$matrix) else character(0)
+          current <- isolate(input[[NAMESPACE_STATMODEL$comparisons_custom_nonpairwise_name]])
+          is_auto <- is.null(current) || !nzchar(current) ||
+                     grepl("^custom comparison \\d+$", current)
+          if (!is_auto) return()
+          n <- 1
+          while (paste0("custom comparison ", n) %in% existing_names) n <- n + 1
+          updateTextInput(session,
+                          NAMESPACE_STATMODEL$comparisons_custom_nonpairwise_name,
+                          value = paste0("custom comparison ", n))
+        },
+        ignoreInit = FALSE
+      )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@R/module-statmodel-server.R` around lines 133 - 147, The observer that
auto-generates the default name (the block that currently calls updateTextInput
for NAMESPACE_STATMODEL$comparisons_custom_nonpairwise_name) runs whenever
contrast$matrix changes and thus can overwrite user edits; change it to only run
on mode entry or when a genuinely new autogenerated name is required: replace
observe({...}) with observeEvent(input[[NAMESPACE_STATMODEL$comparison_mode]], {
... }) and inside the handler read contrast$matrix with isolate(contrast$matrix)
(or check the current input value via
input[[NAMESPACE_STATMODEL$comparisons_custom_nonpairwise_name]] and only call
updateTextInput when that value is empty or exactly matches a previous
autogenerated "custom comparison N"); keep the same name-generation loop but
ensure updateTextInput is conditional so it won't clobber user-typed names.

… heatmap to all alogn with removing the single comparison options dropdown
Comment thread R/statmodel-ui-options-visualization.R Outdated
#' @noRd
create_heatmap_options <- function(ns) {
tagList(
uiOutput(ns(NAMESPACE_STATMODEL$visualization_which_comparison)),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this may be unnecessary

@swaraj-neu swaraj-neu May 3, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@tonywu1999
Got it, removing the uiOutput from create_heatmap_options. The dropdown was already guarded out in the server-side renderUI for Heatmap, so this slot was unused. Commit

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@R/statmodel-server-visualization.R`:
- Line 303: The heatmap download code is directly reading
input[[NAMESPACE_STATMODEL$visualization_which_comparison]] (symbol:
which.Comparison) and thus bypasses the "all" override logic implemented in
create_group_comparison_plot; update the heatmap download parameter to use the
same resolved comparison value that create_group_comparison_plot uses (e.g.,
reuse the reactive/resolution function or variable that applies the "all"
override) so which.Comparison can be "all" when appropriate instead of always
the raw single-selection input.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d67466f2-61e9-4ff9-ae1d-283b97ca6e98

📥 Commits

Reviewing files that changed from the base of the PR and between 3ccf5e8 and b023986.

📒 Files selected for processing (2)
  • R/statmodel-server-visualization.R
  • R/statmodel-ui-options-visualization.R

Comment thread R/statmodel-server-visualization.R Outdated
@tonywu1999 tonywu1999 changed the title Merge more bugfixes for DIANN dataset into Development fix(plots): Fix plotting bugs in statistical analysis page May 3, 2026
@tonywu1999 tonywu1999 merged commit 3aae854 into devel May 3, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants