Skip to content

Merge the feature to download all the plots (including group comparison) from statmodel into Development#174

Open
swaraj-neu wants to merge 6 commits intodevelfrom
feat/statmodel
Open

Merge the feature to download all the plots (including group comparison) from statmodel into Development#174
swaraj-neu wants to merge 6 commits intodevelfrom
feat/statmodel

Conversation

@swaraj-neu
Copy link
Contributor

@swaraj-neu swaraj-neu commented Feb 28, 2026

feat: update create_download_plot_handler to support response curve plots to be exported as a zip file

Motivation and Context

This PR extends MSstatsShiny's plot download capability to allow exporting dynamically generated response-curve plots as zipped artifacts. Response-curve plots require reactive context (input, contrast, preprocess_data) to build dose-response visualizations from ProteinLevelData, so the download handler was extended to receive that context. The implementation adds a response-curve generation and zip-export path while preserving the legacy behavior for other plot types.

Solution summary: the download handler API was changed to accept (output, input, contrast, preprocess_data). For response-curve requests it validates the contrast matrix, prepares dose-response data, builds the response-curve plot (via the existing visualizeResponseProtein flow), saves the plot to a temporary PDF, zips it, and serves the ZIP. Non-response-curve downloads continue to use the legacy artifact-copy/zip fallback.

Detailed Changes

  • R/statmodel-server-visualization.R

    • Signature changed: create_download_plot_handler(output) → create_download_plot_handler(output, input, contrast, preprocess_data).
    • Download filename logic: Response-curve → "ResponseCurvePlot-.zip"; other → "SummaryPlot-.zip".
    • Added response-curve download flow:
      • Validates contrast$matrix and shows an error notification when missing.
      • Prepares dose-response fit data by merging preprocess_data()$ProteinLevelData with contrast (by GROUP).
      • Generates the response-curve plot (via MSstatsResponse visualization helper), writes a temporary PDF (ggplot2::ggsave), zips it (utils::zip), copies the zip to the download destination, and cleans up temp files.
      • Error handling: notifications for missing contrast, file copy failures, and other errors.
    • Kept the legacy non-response-curve flow:
      • Scans for files matching "^Ex_" and copies the latest to the download destination (legacy behavior retained, marked as TODO for refactor).
      • Preserves existing backup/zip logic and error handling for the legacy path.
    • Added explicit roxygen import hints for ggplot2::ggsave and utils::zip.
  • R/module-statmodel-server.R

    • Call site updated to pass additional reactive context: create_download_plot_handler(output, input, contrast, preprocess_data).
  • NAMESPACE

    • Added importFrom(ggplot2, ggsave) and importFrom(utils, zip).
  • Misc

    • Commits include improved error handling and use of unique temporary files for the export flow.

Unit Tests Added / Modified

  • tests/testthat/test-module-statmodel-server.R
    • Added tests to validate the updated download handler:
      • Asserts create_download_plot_handler registers a downloadHandler; server initialization with the 4-argument signature does not crash (using testServer with mocks).
      • Verifies filename selection: response-curve plot path yields a .zip named with "ResponseCurvePlot", other plot types yield a .zip named with "SummaryPlot".
    • Tests mock preprocess_data and input to avoid runtime plotting and check initialization and filename logic.

Coding Guidelines and Notes (violations / technical debt)

  • Legacy filesystem scanning remains: the non-response-curve branch still searches the working directory for "Ex_*" files and copies the latest. This is brittle and flagged as technical debt (TODO) — recommend refactoring to a deterministic artifact-generation/selection mechanism.
  • Packaging note: new imports (ggplot2::ggsave and utils::zip) were added to NAMESPACE; confirm corresponding DESCRIPTION entries if required by the project's packaging policy.

@swaraj-neu swaraj-neu self-assigned this Feb 28, 2026
@swaraj-neu swaraj-neu added the enhancement New feature or request label Feb 28, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

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

The download plot handler signature was extended to accept (input, contrast, preprocess_data) and now branches: a ResponseCurve path that validates contrasts, prepares dose-response data, generates a PDF and returns a ZIP, and a fallback legacy path that preserves summary-plot download behavior.

Changes

Cohort / File(s) Summary
Download handler wiring
R/module-statmodel-server.R
Call site updated to pass input, contrast, and preprocess_data into create_download_plot_handler instead of only output.
Download handler implementation
R/statmodel-server-visualization.R
Signature changed to create_download_plot_handler(output, input, contrast, preprocess_data); added branching for ResponseCurve downloads (contrast validation, dose-response prep, response-curve PDF generation, zip packaging and cleanup) while preserving legacy summary-plot file-copy/zip flow. Added ggsave and utils::zip usage.
Tests
tests/testthat/test-module-statmodel-server.R
Added tests verifying the downloadHandler registration, server initialization with the 4-arg signature, and filename selection producing ResponseCurvePlot-<date>.zip vs SummaryPlot-<date>.zip.
Namespace imports
NAMESPACE
Added importFrom(ggplot2, ggsave) and importFrom(utils, zip) to support plot export and zipping.

Sequence Diagram

sequenceDiagram
    participant User
    participant Handler as DownloadHandler
    participant Contrast
    participant Data as PreprocessData
    participant PlotGen as PlotGenerator
    participant FS as FileSystem

    User->>Handler: initiate download
    Handler->>Handler: inspect plot type

    alt Response Curve
        Handler->>Contrast: request/validate contrast matrix
        Contrast-->>Handler: matrix or NULL
        Handler->>Data: preprocess / prepare dose-response data
        Data-->>Handler: dose-response dataset
        Handler->>PlotGen: generate response-curve plot (PDF)
        PlotGen-->>FS: write temp PDF
        Handler->>FS: zip PDF -> create ZIP
        FS-->>Handler: ZIP path
        Handler->>User: serve ZIP download
        Handler->>FS: cleanup temp files
    else Summary Plot
        Handler->>FS: find most recent Ex_* file
        FS-->>Handler: file path
        Handler->>FS: copy file to download target or zip as needed
        Handler->>User: serve file download
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐇
I zipped the curve beneath the moon,
contrasted leaps in midnight tune,
a PDF snug, tidy, bright,
old summaries still hold the light,
downloads dance — a rabbit's boon ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title mentions 'download all the plots (including group comparison)' but the PR primarily adds response curve plot download functionality, which is only one specific plot type, not all plots. Consider clarifying the title to be more specific: 'Add response curve plot download as zip file' or 'Implement download feature for response curve plots' would better reflect the actual scope.
✅ Passed checks (2 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.

✏️ 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 feat/statmodel

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Robustness

The download handler assumes required inputs/data always exist (e.g., contrast$matrix, preprocess_data()$ProteinLevelData, matching GROUP values for merge, and presence of files matching ^Ex_ in the working directory). Consider validating prerequisites (e.g., req(...)/explicit checks) and providing a user-facing error when missing, to avoid silent failures or cryptic errors.

create_download_plot_handler <- function(output, input, contrast, preprocess_data) {
  output[[NAMESPACE_STATMODEL$visualization_download_plot_results]] <- downloadHandler(
    filename = function() {
      if (input[[NAMESPACE_STATMODEL$visualization_plot_type]] ==
        CONSTANTS_STATMODEL$plot_type_response_curve) {
        paste("ResponseCurvePlot-", Sys.Date(), ".zip", sep = "")
      } else {
        paste("SummaryPlot-", Sys.Date(), ".zip", sep = "")
      }
    },
    content = function(file) {
      if (input[[NAMESPACE_STATMODEL$visualization_plot_type]] ==
        CONSTANTS_STATMODEL$plot_type_response_curve) {
        # Generate response curve plot
        matrix <- contrast$matrix
        protein_level_data <- merge(preprocess_data()$ProteinLevelData, matrix, by = "GROUP")
        dia_prepared <- prepare_dose_response_fit(data = protein_level_data)

        response_plot <- visualizeResponseProtein(
          data = dia_prepared,
          protein_name = input[[NAMESPACE_STATMODEL$visualization_which_protein]],
          drug_name = input[[NAMESPACE_STATMODEL$visualization_response_curve_which_drug]],
          ratio_response = isTRUE(input[[NAMESPACE_STATMODEL$visualization_response_curve_ratio_scale]]),
          show_ic50 = TRUE,
          add_ci = TRUE,
          transform_dose = input[[NAMESPACE_STATMODEL$modeling_response_curve_log_xaxis]],
          n_samples = 1000,
          increasing = input[[NAMESPACE_STATMODEL$modeling_response_curve_increasing_trend]]
        )

        # Save plot to a temp PDF, then zip it
        temp_dir <- tempdir()
        pdf_path <- file.path(temp_dir, "Ex_ResponseCurvePlot.pdf")
        ggplot2::ggsave(pdf_path,
          plot = response_plot, device = "pdf",
          width = 10, height = 8
        )

        zip_path <- file.path(temp_dir, "Ex_ResponseCurvePlot.zip")
        utils::zip(zip_path, files = pdf_path, flags = "-j")
        file.copy(zip_path, file)
      } else {
        # Existing behavior for other plot types (TODO: refactor in future issue)
        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)
      }
Performance

Response-curve downloads recompute model/plot generation on-demand and hardcode n_samples = 1000, which may be slow for interactive downloads. Consider making sampling configurable, caching prepared data/fit results, or reducing defaults to prevent timeouts for large datasets.

# Generate response curve plot
matrix <- contrast$matrix
protein_level_data <- merge(preprocess_data()$ProteinLevelData, matrix, by = "GROUP")
dia_prepared <- prepare_dose_response_fit(data = protein_level_data)

response_plot <- visualizeResponseProtein(
  data = dia_prepared,
  protein_name = input[[NAMESPACE_STATMODEL$visualization_which_protein]],
  drug_name = input[[NAMESPACE_STATMODEL$visualization_response_curve_which_drug]],
  ratio_response = isTRUE(input[[NAMESPACE_STATMODEL$visualization_response_curve_ratio_scale]]),
  show_ic50 = TRUE,
  add_ci = TRUE,
  transform_dose = input[[NAMESPACE_STATMODEL$modeling_response_curve_log_xaxis]],
  n_samples = 1000,
  increasing = input[[NAMESPACE_STATMODEL$modeling_response_curve_increasing_trend]]
)
Test Quality

The added tests primarily assert server initialization and are duplicated across two files, but they don’t validate the new behavior (zip naming/content differences for response curve vs. other plot types, and correct generation of the PDF/zip). Consider adding focused tests for the download handler output and removing duplication/misplaced server tests in UI-focused spec files.

test_that("statmodelServer initializes with updated download handler without error", {
  expect_error(
    testServer(
      statmodelServer,
      args = list(
        parent_session = MockShinySession$new(),
        loadpage_input = reactive({
          list(BIO = "protein", DDA_DIA = "DDA", filetype = "standard", proceed1 = 0)
        }),
        qc_input = reactive({ list(normalization = "equalizeMedians") }),
        get_data = reactive({ create_mock_raw_data() }),
        preprocess_data = reactive({ create_mock_data("DDA", "protein") })
      ),
      {
        # Server initialized successfully with updated create_download_plot_handler
        expect_null(contrast$matrix)
      }
    ),
    NA
  )
})

@swaraj-neu swaraj-neu linked an issue Feb 28, 2026 that may be closed by this pull request
@github-actions
Copy link

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Guard against missing exported files

Handle the case where no matching Ex_ files exist (or file.info() returns empty),
otherwise which.max() will error and break the download. Fail with a clear message
when there is nothing to download.

R/statmodel-server-visualization.R [164-168]

 files <- list.files(getwd(), pattern = "^Ex_", full.names = TRUE)
+if (length(files) == 0) {
+  stop("No exported plot found to download.")
+}
+
 file_info <- file.info(files)
-latest_file <- files[which.max(file_info$mtime)]
-file.copy(latest_file, file)
+idx <- which.max(file_info$mtime)
+if (length(idx) == 0 || is.na(idx)) {
+  stop("No exported plot found to download.")
+}
 
+latest_file <- files[idx]
+file.copy(latest_file, file, overwrite = TRUE)
+
Suggestion importance[1-10]: 7

__

Why: This correctly prevents a runtime error from which.max() when no Ex_ files exist (or timestamps are unusable) and replaces it with a clear failure message. It meaningfully improves the stability of the non-response-curve download path.

Medium
Validate required data before merging

Guard the response-curve download path with req()/validate() so it fails fast with a
clear message when contrast$matrix or the expected columns/data are missing (e.g.,
when contrast$matrix is NULL). This prevents runtime errors from merge() inside the
download handler.

R/statmodel-server-visualization.R [132-137]

 if (input[[NAMESPACE_STATMODEL$visualization_plot_type]] ==
   CONSTANTS_STATMODEL$plot_type_response_curve) {
   # Generate response curve plot
+  req(contrast$matrix)
+  req(preprocess_data())
+  req(preprocess_data()$ProteinLevelData)
+
+  validate(
+    need("GROUP" %in% names(preprocess_data()$ProteinLevelData), "Missing GROUP column in ProteinLevelData."),
+    need("GROUP" %in% names(contrast$matrix), "Missing GROUP column in contrast matrix.")
+  )
+
   matrix <- contrast$matrix
   protein_level_data <- merge(preprocess_data()$ProteinLevelData, matrix, by = "GROUP")
   dia_prepared <- prepare_dose_response_fit(data = protein_level_data)
Suggestion importance[1-10]: 6

__

Why: This is a reasonable safeguard to prevent merge() from failing at download time when contrast$matrix/ProteinLevelData are missing, improving robustness. However, using validate()/need() inside a downloadHandler() is less standard than in render contexts and the column check should ideally consider colnames() if contrast$matrix is not a data frame.

Low
Avoid temp collisions and fragile zip flags

Use unique temp file names to avoid collisions across concurrent sessions, and avoid
utils::zip(..., flags = "-j") which can be brittle across platforms. Zipping from
the file’s directory with a relative filename is more portable; also ensure temp
artifacts are cleaned up.

R/statmodel-server-visualization.R [151-161]

 # Save plot to a temp PDF, then zip it
 temp_dir <- tempdir()
-pdf_path <- file.path(temp_dir, "Ex_ResponseCurvePlot.pdf")
-ggplot2::ggsave(pdf_path,
-  plot = response_plot, device = "pdf",
-  width = 10, height = 8
+pdf_path <- tempfile("Ex_ResponseCurvePlot_", tmpdir = temp_dir, fileext = ".pdf")
+zip_path <- tempfile("Ex_ResponseCurvePlot_", tmpdir = temp_dir, fileext = ".zip")
+on.exit(unlink(c(pdf_path, zip_path), force = TRUE), add = TRUE)
+
+ggplot2::ggsave(
+  pdf_path,
+  plot = response_plot,
+  device = "pdf",
+  width = 10,
+  height = 8
 )
 
-zip_path <- file.path(temp_dir, "Ex_ResponseCurvePlot.zip")
-utils::zip(zip_path, files = pdf_path, flags = "-j")
-file.copy(zip_path, file)
+old_wd <- getwd()
+on.exit(setwd(old_wd), add = TRUE)
+setwd(dirname(pdf_path))
+utils::zip(zip_path, files = basename(pdf_path))
 
+file.copy(zip_path, file, overwrite = TRUE)
+
Suggestion importance[1-10]: 4

__

Why: Using tempfile() and cleaning up temp artifacts is a solid improvement for concurrency and hygiene. But changing the process working directory via setwd() inside a Shiny download path can cause subtle cross-session side effects, making this refactor potentially risky.

Low

Copy link

@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: 2

🧹 Nitpick comments (3)
tests/testthat/test-module-statmodel-server.R (1)

642-660: Test validates initialization but not download functionality.

The test confirms the server initializes without error after the download handler signature change, which is valuable for regression protection. However, it doesn't test the actual download handler behavior (filename generation, content creation, or zip file output).

Consider adding functional tests that mock the input state for response curve plot type and verify:

  1. The filename function returns "ResponseCurvePlot-<date>.zip" for response curves
  2. The content function generates a valid PDF and zips it correctly

Would you like me to help draft a more comprehensive test that verifies the download handler behavior?

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

In `@tests/testthat/test-module-statmodel-server.R` around lines 642 - 660, The
test test_that("statmodelServer initializes with updated download handler
without error") only checks initialization but not the download handler
behavior; add functional assertions that exercise the download handler returned
by statmodelServer: call or invoke the handler's filename-generating function
and assert it matches the pattern "ResponseCurvePlot-<date>.zip" for response
curve inputs, then invoke the handler's content function (mocking any file/plot
generation dependencies) and verify it writes a valid PDF inside a ZIP (open the
ZIP, check for a .pdf entry and non-zero size). Locate the server object via
statmodelServer in the test harness and reference the download handler object
(the reactive or list element named for response curve download in
statmodelServer) when adding these assertions, mocking input state to set plot
type to "response curve" and supplying temporary file paths to capture output.
R/statmodel-server-visualization.R (1)

151-161: Consider cleaning up temporary files after use.

The PDF and ZIP files created in tempdir() are not explicitly removed after copying to the download destination. While R will eventually clean these up, explicitly removing them after use is good practice to avoid accumulating temp files during long-running sessions.

♻️ Proposed cleanup
         zip_path <- file.path(temp_dir, "Ex_ResponseCurvePlot.zip")
         utils::zip(zip_path, files = pdf_path, flags = "-j")
         file.copy(zip_path, file)
+        # Clean up temp files
+        unlink(c(pdf_path, zip_path))
       } else {
🤖 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 151 - 161, The temp PDF and
ZIP created in the block using temp_dir / pdf_path / zip_path (after
ggplot2::ggsave, utils::zip and file.copy) are never removed; add cleanup to
delete these temp artifacts after the file.copy completes (or use an on.exit
handler) by calling unlink on pdf_path and zip_path (and temp_dir if
appropriate) to ensure temporary files are removed even on error.
tests/testthat/test-statmodel-ui-options-visualization.R (1)

68-87: Consider consolidating duplicate tests.

This test is nearly identical to the one in test-module-statmodel-server.R (lines 642-660). Both tests verify the same initialization behavior with the updated download handler. Consider keeping only one copy to avoid maintenance overhead and potential drift between duplicates.

Additionally, this test only verifies that initialization doesn't error and that contrast$matrix is NULL - it doesn't actually test the download handler functionality (e.g., that the correct filename is generated or that the content function produces a valid zip file for response curves). Consider adding functional tests for the download handler behavior.

🤖 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 68 -
87, This test duplicates coverage in test-module-statmodel-server.R; remove or
consolidate this test from test-statmodel-ui-options-visualization.R and keep a
single initialization test that uses testServer(statmodelServer, args =
list(parent_session = MockShinySession$new(), loadpage_input = reactive(...),
qc_input = reactive(...), get_data = reactive(...), preprocess_data =
reactive(...))) and still asserts expect_null(contrast$matrix). Then extend the
retained test in test-module-statmodel-server.R (or the consolidated one) to add
functional checks for the download handler: locate and exercise
create_download_plot_handler (or the server export that wraps it) to capture the
generated filename and invoke its content function, asserting the filename
format and that the produced bytes form a valid zip containing the expected
response-curve files/plots.
🤖 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`:
- Around line 164-167: The code assumes there is at least one "Ex_*" file; guard
the block that builds files, file_info, latest_file and calls file.copy by
checking that files is non-empty (e.g., if (length(files) == 0) stop/return with
a clear message for non-response-curve downloads), and only compute
which.max(file_info$mtime) and call file.copy(latest_file, file) when files
exists; reference the variables/files: files, file_info, latest_file, and the
file.copy(...) invocation so you validate and fail-fast before attempting the
copy.
- Around line 131-168: Before calling merge() on contrast$matrix inside the
downloadHandler content (content = function(file)), add a null-check for
contrast$matrix and short-circuit with a clear validation/error to avoid the
merge crash; e.g. right before "matrix <- contrast$matrix" check
is.null(contrast$matrix) and call
shiny::validate(shiny::need(!is.null(contrast$matrix), "Contrast matrix not
built; please create it before downloading the response curve.")) or stop() with
an informative message, so prepare_dose_response_fit and
visualizeResponseProtein are only run when contrast$matrix is present.

---

Nitpick comments:
In `@R/statmodel-server-visualization.R`:
- Around line 151-161: The temp PDF and ZIP created in the block using temp_dir
/ pdf_path / zip_path (after ggplot2::ggsave, utils::zip and file.copy) are
never removed; add cleanup to delete these temp artifacts after the file.copy
completes (or use an on.exit handler) by calling unlink on pdf_path and zip_path
(and temp_dir if appropriate) to ensure temporary files are removed even on
error.

In `@tests/testthat/test-module-statmodel-server.R`:
- Around line 642-660: The test test_that("statmodelServer initializes with
updated download handler without error") only checks initialization but not the
download handler behavior; add functional assertions that exercise the download
handler returned by statmodelServer: call or invoke the handler's
filename-generating function and assert it matches the pattern
"ResponseCurvePlot-<date>.zip" for response curve inputs, then invoke the
handler's content function (mocking any file/plot generation dependencies) and
verify it writes a valid PDF inside a ZIP (open the ZIP, check for a .pdf entry
and non-zero size). Locate the server object via statmodelServer in the test
harness and reference the download handler object (the reactive or list element
named for response curve download in statmodelServer) when adding these
assertions, mocking input state to set plot type to "response curve" and
supplying temporary file paths to capture output.

In `@tests/testthat/test-statmodel-ui-options-visualization.R`:
- Around line 68-87: This test duplicates coverage in
test-module-statmodel-server.R; remove or consolidate this test from
test-statmodel-ui-options-visualization.R and keep a single initialization test
that uses testServer(statmodelServer, args = list(parent_session =
MockShinySession$new(), loadpage_input = reactive(...), qc_input =
reactive(...), get_data = reactive(...), preprocess_data = reactive(...))) and
still asserts expect_null(contrast$matrix). Then extend the retained test in
test-module-statmodel-server.R (or the consolidated one) to add functional
checks for the download handler: locate and exercise
create_download_plot_handler (or the server export that wraps it) to capture the
generated filename and invoke its content function, asserting the filename
format and that the produced bytes form a valid zip containing the expected
response-curve files/plots.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 94155dc and 39e434b.

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

@swaraj-neu
Copy link
Contributor Author

Please don't delete the feature branch post merge.

# Save plot to a temp PDF, then zip it
temp_dir <- tempdir()
pdf_path <- file.path(temp_dir, "Ex_ResponseCurvePlot.pdf")
ggplot2::ggsave(pdf_path,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you import the dependency @importFrom ggplot2 ggsave?

)

zip_path <- file.path(temp_dir, "Ex_ResponseCurvePlot.zip")
utils::zip(zip_path, files = pdf_path, flags = "-j")
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar import the dependency @importFrom utils zip

# DOWNLOAD PLOT HANDLER TESTS
# ============================================================================

test_that("statmodelServer initializes with updated download handler without error", {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we try writing tests verifying the logic of the create_download_plot_handler instead? You'd need to mock certain functions and verify certain functions get called, e.g. ggplot2::ggsave, utils::zip, etc.

As we had discussed earlier, see this file for smaller unit tests that don't use testServer

Copy link

@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: 2

🤖 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`:
- Around line 125-131: The filename() helper always returns a ".zip" name even
when the served content (latest_file) is a PDF/PNG; update filename() or the
download logic so the returned name matches the actual content or actually
creates a ZIP for the summary branch: either (A) detect the real file extension
of latest_file and return paste("SummaryPlot-", Sys.Date(), ".",
tools::file_ext(latest_file), sep="") for the summary branch, or (B) package
latest_file into a real ZIP archive and return the .zip name; apply this change
where filename() and the code that copies latest_file (variables: filename,
latest_file, input[[NAMESPACE_STATMODEL$visualization_plot_type]],
CONSTANTS_STATMODEL$plot_type_response_curve) are used (also update the summary
branch referenced around the 169-179 region).
- Around line 157-168: The code currently writes fixed filenames into tempdir()
(pdf_path, zip_path) causing cross-session collisions; change to use unique temp
files (e.g., tempfile() or tempfile(pattern=...)) for pdf_path and zip_path,
pass those unique paths into ggplot2::ggsave and utils::zip, ensure
file.copy(file, destination, overwrite=TRUE) or equivalent is used to atomically
move the zip to the final location, and update the cleanup to unlink only those
specific tempfile paths (pdf_path, zip_path) to avoid accidentally deleting
others.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 74743fcb-8d06-4dbf-9ae0-2a8c4dfe68a7

📥 Commits

Reviewing files that changed from the base of the PR and between 182ced8 and 12a971a.

📒 Files selected for processing (3)
  • NAMESPACE
  • R/statmodel-server-visualization.R
  • tests/testthat/test-module-statmodel-server.R
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/testthat/test-module-statmodel-server.R

Copy link

@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

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

133-193: Centralize error handling for the full download pipeline.

merge(), prepare_dose_response_fit(), visualizeResponseProtein(), ggsave(), and utils::zip() failures can still bubble without consistent notification. Wrapping the full content body in one tryCatch would make failure UX predictable.

🔧 Proposed refactor
     content = function(file) {
-      if (input[[NAMESPACE_STATMODEL$visualization_plot_type]] ==
-        CONSTANTS_STATMODEL$plot_type_response_curve) {
+      tryCatch({
+        if (input[[NAMESPACE_STATMODEL$visualization_plot_type]] ==
+          CONSTANTS_STATMODEL$plot_type_response_curve) {
         # Generate response curve plot
         matrix <- contrast$matrix
         if (is.null(matrix)) {
           showNotification("Please build a contrast matrix first.", type = "error")
           return(NULL)
         }
         protein_level_data <- merge(preprocess_data()$ProteinLevelData, matrix, by = "GROUP")
         dia_prepared <- prepare_dose_response_fit(data = protein_level_data)
@@
-      } else {
+        } else {
         # Existing behavior for other plot types (TODO: refactor in future issue)
         files <- list.files(getwd(), pattern = "^Ex_", full.names = TRUE)
@@
-      }
+        }
+      }, error = function(e) {
+        showNotification(conditionMessage(e), type = "error")
+        return(NULL)
+      })
     }
🤖 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 133 - 193, Wrap the entire
content = function(file) { ... } body in a single tryCatch so any errors from
merge, prepare_dose_response_fit, visualizeResponseProtein, ggplot2::ggsave,
utils::zip, file.copy, etc. are caught; in the error handler call
showNotification with the error message and return(NULL). Ensure the existing
on.exit(unlink(...), add = TRUE) calls still execute for cleanup (preserve them
inside the try block or set up a top-level on.exit) and rethrow or return NULL
consistently on error so the download pipeline (including branches that use
list.files/getwd and summary zip creation) has a predictable failure path;
reference the content function, contrast$matrix, preprocess_data(),
prepare_dose_response_fit(), visualizeResponseProtein(), ggplot2::ggsave(),
utils::zip(), and file.copy() when making the changes.
🤖 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`:
- Around line 174-180: The code currently uses getwd() to discover files (files
<- list.files(getwd(), pattern = "^Ex_", ...)), which can leak other sessions'
artifacts; change the handler to use a session-scoped artifact directory passed
into the handler (e.g., an argument like artifact_dir or a session-specific
value such as session$userData$artifact_dir) instead of getwd(), validate the
directory exists, then call list.files(artifact_dir, pattern="^Ex_",
full.names=TRUE) and continue using file.info(files) / latest_file as before;
update the handler signature and any call sites to supply the session-scoped
path.

---

Nitpick comments:
In `@R/statmodel-server-visualization.R`:
- Around line 133-193: Wrap the entire content = function(file) { ... } body in
a single tryCatch so any errors from merge, prepare_dose_response_fit,
visualizeResponseProtein, ggplot2::ggsave, utils::zip, file.copy, etc. are
caught; in the error handler call showNotification with the error message and
return(NULL). Ensure the existing on.exit(unlink(...), add = TRUE) calls still
execute for cleanup (preserve them inside the try block or set up a top-level
on.exit) and rethrow or return NULL consistently on error so the download
pipeline (including branches that use list.files/getwd and summary zip creation)
has a predictable failure path; reference the content function, contrast$matrix,
preprocess_data(), prepare_dose_response_fit(), visualizeResponseProtein(),
ggplot2::ggsave(), utils::zip(), and file.copy() when making the changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f3391295-9a00-4b63-9a49-9d6d96a3a91b

📥 Commits

Reviewing files that changed from the base of the PR and between 12a971a and b8354c5.

📒 Files selected for processing (1)
  • R/statmodel-server-visualization.R

Copy link

@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.

♻️ Duplicate comments (1)
R/statmodel-server-visualization.R (1)

175-181: ⚠️ Potential issue | 🟠 Major

Shared working directory scan risks cross-session artifact leakage.

The getwd() scan for Ex_* files remains unaddressed from previous review. In multi-user Shiny deployments, this can return another session's artifact (wrong download + privacy risk).

Consider passing a session-scoped artifact directory into the handler:

💡 Suggested direction
-create_download_plot_handler <- function(output, input, contrast, preprocess_data) {
+create_download_plot_handler <- function(output, input, contrast, preprocess_data, artifact_dir) {
...
-        files <- list.files(getwd(), pattern = "^Ex_", full.names = TRUE)
+        files <- list.files(artifact_dir(), pattern = "^Ex_", full.names = TRUE)

At the call site, pass a session-scoped reactive like session$userData$artifact_dir or a reactiveVal initialized per session.

🤖 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 175 - 181, Replace the
global getwd() scan with a session-scoped artifact directory: change the
list.files call that builds files (and subsequent file_info/which.max logic) to
use a provided per-session path (e.g. a parameter or reactive such as
session$userData$artifact_dir or a reactiveVal passed into the handler) instead
of getwd(); ensure the handler validates the directory exists and is within
allowed bounds before calling list.files(pattern = "^Ex_"), then proceed with
the existing file_info and latest_file logic using that session-scoped
directory.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@R/statmodel-server-visualization.R`:
- Around line 175-181: Replace the global getwd() scan with a session-scoped
artifact directory: change the list.files call that builds files (and subsequent
file_info/which.max logic) to use a provided per-session path (e.g. a parameter
or reactive such as session$userData$artifact_dir or a reactiveVal passed into
the handler) instead of getwd(); ensure the handler validates the directory
exists and is within allowed bounds before calling list.files(pattern = "^Ex_"),
then proceed with the existing file_info and latest_file logic using that
session-scoped directory.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 836c9e06-d988-4c97-b02b-d7a5978fdeb2

📥 Commits

Reviewing files that changed from the base of the PR and between b8354c5 and 47e21bd.

📒 Files selected for processing (1)
  • R/statmodel-server-visualization.R

@swaraj-neu
Copy link
Contributor Author

The getwd() scan is pre-existing code, not introduced by this PR. It's tracked for replacement in the follow-up issue for on-demand group comparison plot downloads.

@swaraj-neu swaraj-neu changed the title Merge the download response curve plot feature from statmodel into Development Merge the feature to download all the plots (including group comparison) from statmodel into Development Mar 5, 2026
} else {
# Generate group comparison plot using a session-scoped temp directory
plot_type <- input[[NAMESPACE_STATMODEL$visualization_plot_type]]
fold_change_cutoff <- ifelse(
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should be zero if NULL.

Comment on lines +160 to +173
pdf_path <- tempfile("Ex_ResponseCurvePlot-", fileext = ".pdf")
ggplot2::ggsave(pdf_path,
plot = response_plot, device = "pdf",
width = 10, height = 8
)

zip_path <- file.path(temp_dir, "Ex_ResponseCurvePlot.zip")
utils::zip(zip_path, files = pdf_path, flags = "-j")
file.copy(zip_path, file)
unlink(c(pdf_path, zip_path))
} else {
# Existing behavior for other plot types (TODO: refactor in future issue)
files <- list.files(getwd(), pattern = "^Ex_", full.names = TRUE)
if (length(files) == 0) {
showNotification("No plot files found to download.", type = "error")
zip_path <- tempfile("Ex_ResponseCurvePlot-", fileext = ".zip")
on.exit(unlink(c(pdf_path, zip_path), force = TRUE), add = TRUE)
utils::zip(zipfile = zip_path, files = pdf_path, flags = "-j")
copied <- file.copy(zip_path, file, overwrite = TRUE)
if (!isTRUE(copied)) {
showNotification("Failed to copy response curve ZIP for download.", type = "error")
return(NULL)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This code + the temp directory creation seems to be duplicated between both plot types. Can this be consolidated into one code block? I don't think the zipfile names need to be necessarily different here (or a helper function that helps zipping files based on ggplot outputs).

Comment on lines +676 to +734
test_that("download handler filename returns ResponseCurvePlot for response curves", {
# Test the filename logic directly
plot_type <- CONSTANTS_STATMODEL$plot_type_response_curve
filename <- if (plot_type == CONSTANTS_STATMODEL$plot_type_response_curve) {
paste("ResponseCurvePlot-", Sys.Date(), ".zip", sep = "")
} else {
paste("SummaryPlot-", Sys.Date(), ".zip", sep = "")
}
expect_true(grepl("ResponseCurvePlot", filename))
expect_true(grepl("\\.zip$", filename))
})

test_that("download handler filename returns SummaryPlot for other plot types", {
plot_type <- CONSTANTS_STATMODEL$plot_type_volcano_plot
filename <- if (plot_type == CONSTANTS_STATMODEL$plot_type_response_curve) {
paste("ResponseCurvePlot-", Sys.Date(), ".zip", sep = "")
} else {
paste("SummaryPlot-", Sys.Date(), ".zip", sep = "")
}
expect_true(grepl("SummaryPlot", filename))
expect_true(grepl("\\.zip$", filename))
})

# ============================================================================
# GROUP COMPARISON PLOT DOWNLOAD TESTS
# ============================================================================

test_that("download handler filename returns SummaryPlot for Volcano plot", {
plot_type <- CONSTANTS_STATMODEL$plot_type_volcano_plot
filename <- if (plot_type == CONSTANTS_STATMODEL$plot_type_response_curve) {
paste("ResponseCurvePlot-", Sys.Date(), ".zip", sep = "")
} else {
paste("SummaryPlot-", Sys.Date(), ".zip", sep = "")
}
expect_true(grepl("SummaryPlot", filename))
expect_true(grepl("\\.zip$", filename))
})

test_that("download handler filename returns SummaryPlot for Heatmap", {
plot_type <- CONSTANTS_STATMODEL$plot_type_heatmap
filename <- if (plot_type == CONSTANTS_STATMODEL$plot_type_response_curve) {
paste("ResponseCurvePlot-", Sys.Date(), ".zip", sep = "")
} else {
paste("SummaryPlot-", Sys.Date(), ".zip", sep = "")
}
expect_true(grepl("SummaryPlot", filename))
expect_true(grepl("\\.zip$", filename))
})

test_that("download handler filename returns SummaryPlot for ComparisonPlot", {
plot_type <- CONSTANTS_STATMODEL$plot_type_comparison_plot
filename <- if (plot_type == CONSTANTS_STATMODEL$plot_type_response_curve) {
paste("ResponseCurvePlot-", Sys.Date(), ".zip", sep = "")
} else {
paste("SummaryPlot-", Sys.Date(), ".zip", sep = "")
}
expect_true(grepl("SummaryPlot", filename))
expect_true(grepl("\\.zip$", filename))
})
Copy link
Contributor

Choose a reason for hiding this comment

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

You're not testing any helper functions here. Could you adjust these tests to use test a helper function?

)
})

test_that("all plot type constants are defined for download handler branching", {
Copy link
Contributor

Choose a reason for hiding this comment

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

Similarly, this test only tests the constants are different, but what would be more useful is verifying the logic flow of the helper function create_download_plot_handler

{
# Server initialized: create_download_plot_handler was called internally
# Verify the server doesn't crash with the new 4-argument signature
expect_null(contrast$matrix)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure if this is a valid test that would validate create_download_plot_handler was even called. You'd need to verify the mock for create_download_plot_handler was invoked.

{
# Verify server initializes without error with 6-argument download handler
# (output, input, contrast, preprocess_data, data_comparison, loadpage_input)
expect_null(contrast$matrix)
Copy link
Contributor

Choose a reason for hiding this comment

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

Similarly, I'm not sure if this is a valid test that would validate create_download_plot_handler was even called. You'd need to verify the mock for create_download_plot_handler was invoked with the 6 arguments

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

Labels

enhancement New feature or request Review effort 3/5

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Statmodel] Enable user to download group comparison plots (Volcano, Heatmap, Comparison) [Statmodel] Enable user to download response curve plot

2 participants