From 0456584d2d0d6b59823440d7622583fed7a46116 Mon Sep 17 00:00:00 2001 From: Sabila Bernard <60706407+sabila-bernard@users.noreply.github.com> Date: Tue, 19 May 2026 13:26:22 -0400 Subject: [PATCH 1/6] "Claude PR Assistant workflow" --- .github/workflows/claude.yml | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/claude.yml diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000..6b15fac --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,50 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + # claude_args: '--allowed-tools Bash(gh pr *)' + From 88e54468ccd0fbe95924df53528ffbae26b31cbb Mon Sep 17 00:00:00 2001 From: Sabila Bernard <60706407+sabila-bernard@users.noreply.github.com> Date: Tue, 19 May 2026 13:26:23 -0400 Subject: [PATCH 2/6] "Claude Code Review workflow" --- .github/workflows/claude-code-review.yml | 44 ++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/claude-code-review.yml diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 0000000..b5e8cfd --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,44 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize, ready_for_review, reopened] + # Optional: Only run on specific file changes + # paths: + # - "src/**/*.ts" + # - "src/**/*.tsx" + # - "src/**/*.js" + # - "src/**/*.jsx" + +jobs: + claude-review: + # Optional: Filter by PR author + # if: | + # github.event.pull_request.user.login == 'external-contributor' || + # github.event.pull_request.user.login == 'new-developer' || + # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' + plugins: 'code-review@claude-code-plugins' + prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + From 68f5bbcc97b6ff3b972c99bd64451556a22d27b6 Mon Sep 17 00:00:00 2001 From: Sabila Bernard <60706407+sabila-bernard@users.noreply.github.com> Date: Tue, 19 May 2026 14:34:03 -0400 Subject: [PATCH 3/6] Create .gitattributes --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dd5478e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ + * text=auto + *.r text eol=lf + *.R text eol=lf From 6ea333fd96cd59cedf9f9ae28f673d92c28fdc9f Mon Sep 17 00:00:00 2001 From: Sabila Bernard Date: Wed, 27 May 2026 16:08:01 -0400 Subject: [PATCH 4/6] Custom Upload and Assay Updates with bug fixes Features: - Select and use a specific assay after uploading Seurat data - Upload a custom drug signature matrix (Custom Upload option) - Upload a pre-calculated Cell-Drug Connectivity correlation matrix Bug fixes: - corrMat double-transpose removed so drug names appear in dropdown - referenceCompound_ui now evaluates inside hidden conditionalPanel - assay_to_use() guarded against pre-upload navigation crash - Gene name case normalised so lowercase uploads don't silently fail - seq_len() replaces 1:length() in combination analysis loops --- .gitattributes | 7 +- .github/workflows/shinytest2.yml | 44 ++ CLAUDE.md | 69 +++ DESCRIPTION | 5 +- NAMESPACE | 1 + README.md | 1 + inst/shiny/server.R | 437 +++++++++++++++--- .../fixtures/downsampled_seuratObj.RDS | 3 + inst/shiny/tests/testthat/setup-chromote.R | 3 + inst/shiny/tests/testthat/test-edge-cases.R | 156 +++++++ inst/shiny/tests/testthat/test-upload.R | 45 ++ inst/shiny/ui.R | 121 +++-- 12 files changed, 774 insertions(+), 118 deletions(-) create mode 100644 .github/workflows/shinytest2.yml create mode 100644 CLAUDE.md create mode 100644 inst/shiny/tests/testthat/fixtures/downsampled_seuratObj.RDS create mode 100644 inst/shiny/tests/testthat/setup-chromote.R create mode 100644 inst/shiny/tests/testthat/test-edge-cases.R create mode 100644 inst/shiny/tests/testthat/test-upload.R diff --git a/.gitattributes b/.gitattributes index dd5478e..bdfebea 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ - * text=auto - *.r text eol=lf - *.R text eol=lf +* text=auto +*.r text eol=lf +*.R text eol=lf +*.RDS filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/shinytest2.yml b/.github/workflows/shinytest2.yml new file mode 100644 index 0000000..f9d7bf9 --- /dev/null +++ b/.github/workflows/shinytest2.yml @@ -0,0 +1,44 @@ +on: + pull_request: + branches: [main, master, drug_signature_update] + +name: shinytest2 + +jobs: + shinytest2: + runs-on: ubuntu-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + + steps: + - uses: actions/checkout@v4 + with: + lfs: true + + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::shinytest2 + needs: shinytest2 + + - uses: browser-actions/setup-chrome@latest + with: + chrome-version: stable + + - name: Install scFOCAL package + run: R CMD INSTALL --no-multiarch . + + - name: Verify scFOCAL installed + shell: Rscript {0} + run: | + path <- system.file("shiny", package = "scFOCAL") + if (!nzchar(path)) stop("scFOCAL not installed: system.file() returned ''") + cat("scFOCAL shiny dir:", path, "\n") + + - name: Run shinytest2 tests + shell: Rscript {0} + run: | + shinytest2::test_app("inst/shiny") diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a47553c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,69 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build & Test Commands + +```r +# Rebuild package after any change to inst/shiny/ or R/ +devtools::install() # or Build → Clean and Rebuild in RStudio + +# Launch app manually +scFOCAL::runscFOCAL() + +# Run all shinytest2 tests +shinytest2::test_app("inst/shiny") + +# Run a single test file +testthat::test_file("inst/shiny/tests/testthat/test-upload.R") +testthat::test_file("inst/shiny/tests/testthat/test-edge-cases.R") +``` + +**Critical**: Tests run against the *installed* package via `system.file("shiny", package = "scFOCAL")`. Always do a Clean and Rebuild before running tests after editing anything in `inst/shiny/` or `R/`. + +## Architecture + +**Entry point**: `R/runApp.R` — `runscFOCAL()` calls `shiny::shinyAppDir(system.file('shiny', package = "scFOCAL"))`. + +The Shiny app lives in `inst/shiny/server.R` + `inst/shiny/ui.R`. + +### Key server.R reactives + +| Reactive | Purpose | +|---|---| +| `rdsSeurat()` | Loads uploaded Seurat RDS; handles Seurat v5 layer detection | +| `seuratLoaded` | Boolean output; used by conditional panels and tests as upload health-check | +| `assay_to_use()` | Selects RNA vs RNA_ortho assay; guards with `req(input$seurobjRDS)` | +| `drugSignatures()` | Accepts custom drugs×genes CSV; auto-detects orientation via gene-name overlap; falls back to built-in LINCS | +| `corrMatUpload()` | Reads uploaded drugs×cells correlation matrix CSV | +| `LINCS.ResponseSigs()` / `L1000_genes` | Built-in L1000 signature data from package namespace | +| `diseaseSigs` | Triggered by `CalcDiseaseSig` action button | +| `debugRelease` | Outputs current `L1000_Release` input value; used as app alive-check in tests | + +### UI tab structure + +Overview → **Run scFOCAL-dev** (tabsetPanel): +1. Data Upload — Seurat RDS, assay selection +2. Exploration — dim plots, cell highlights +3. Disease Signatures — cell subsetting, heatmap/table/reversal tabs +4. Drug Response / Synergy Analysis — corrMat upload, compound selection + +**Duplicate ID note**: `corrMatUpload` (input) and `referenceCompound_ui` (output) intentionally appear in two tabs — this is existing architecture. Shiny handles duplicate `uiOutput` IDs correctly at runtime. In shinytest2 tests, navigate to the correct tab *before* uploading or reading these elements to avoid "Multiple HTML elements found" warnings. + +## Testing Infrastructure + +Tests live in `inst/shiny/tests/testthat/`: + +- `setup-chromote.R` — raises Chromote's `default_timeout` to 120s; required because the app takes ~12s to load packages on a cold start, which exceeds Chrome's default 10s `Page.navigate` timeout +- `test-upload.R` — app launch and Seurat upload health checks +- `test-edge-cases.R` — regression tests for corrMat orientation, gene case normalization, pre-upload navigation crash +- `fixtures/downsampled_seuratObj.RDS` — 134MB Seurat fixture; requires **Git LFS** before pushing to GitHub + +CI: `.github/workflows/shinytest2.yml` runs on every PR to `drug_signature_update`. + +## Open Bugs (as of drug_signature_update branch) + +- **Bug 1** — `corrMatUpload()` in server.R double-transposes the matrix, swapping drug names and cell barcodes +- **Bug 2** — Shiny warns about duplicate `corrMatUpload`/`referenceCompound_ui` IDs in tests; workaround by navigating to the correct tab first +- **Bug 4** — `assay_to_use()` may be called before Seurat upload; needs `req(rdsSeurat())` guard +- **Bug 5** — Uploaded drug signature CSVs with lowercase gene names produce 0-gene overlap with the Seurat object (which uses uppercase), causing a silent crash; needs `toupper()` normalization diff --git a/DESCRIPTION b/DESCRIPTION index 5d69bfa..b489d41 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -9,7 +9,7 @@ Description: scFOCAL integrates small molecules transcriptional consensus signat License: Custom Encoding: UTF-8 LazyData: true -RoxygenNote: 7.3.2 +RoxygenNote: 7.3.3 Depends: R (>= 3.5) Imports: @@ -29,6 +29,7 @@ Imports: grid, htmlwidgets, lme4, + limma, pheatmap, plotly, reshape2, @@ -41,3 +42,5 @@ Imports: viridis, ggpubr, ComplexHeatmap +Suggests: + shinytest2 diff --git a/NAMESPACE b/NAMESPACE index dd8a91a..8a90e50 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -18,6 +18,7 @@ import(ggrepel) import(ggsci) import(grid) import(htmlwidgets) +importFrom(limma, lmFit) import(lme4) import(pheatmap) import(plotly) diff --git a/README.md b/README.md index ee7a626..dc27e6b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +## Branch to add in new signature files and option for custom drug signature upload (i.e. from TCSgen, in progress) diff --git a/inst/shiny/server.R b/inst/shiny/server.R index e2a2250..4a8d09c 100644 --- a/inst/shiny/server.R +++ b/inst/shiny/server.R @@ -23,13 +23,53 @@ library(ggforce) library(EnhancedVolcano) library(DT) library(lme4) +library(limma) library(edgeR) library(ggpubr) library(stringr) library(viridis) library(ComplexHeatmap) +library(scFOCAL) -server <- function(input, output) { + +#Format: +# seurat: gene x cells +# drug signature: drugs x genes +# corrmat: drugs x cells +# metadata: cells x drugs + + +server <- function(input, output, session) { + + assay_to_use <- reactive({ + if (!is.null(input$assayChoice) && input$assayChoice != "") { + input$assayChoice + } else { + if (is.null(isOrthogonAL())) { + "RNA" + } else { + "RNA_ortho" + } + } + }) + + + layer_to_use <- reactive({ + req(assay_to_use(), rdsSeurat()) + + assay <- assay_to_use() + layers <- names(rdsSeurat()@assays[[assay]]) + + if ("scale.data" %in% layers) { + "scale.data" + } else { + "data" + } + }) + + output$debugRelease <- renderPrint({ + input$L1000_Release + }) # L1000 data as reactive L1000_genes <- reactive({ @@ -40,6 +80,82 @@ server <- function(input, output) { get0("LINCS.ResponseSigs", envir = asNamespace("scFOCAL")) }) + drugSignatures <- reactive({ + + # if user uploaded custom signatures + if (!is.null(input$customL1000Upload)) { + + df <- read.csv(input$customL1000Upload$datapath, + row.names = 1, + check.names = FALSE) + + # Detect orientation using gene overlap + # df needs to be drugs x genes + seurat_genes <- rownames(rdsSeurat()) # gene x cells + + overlap_rows <- sum(toupper(rownames(df)) %in% toupper(seurat_genes)) + overlap_cols <- sum(toupper(colnames(df)) %in% toupper(seurat_genes)) + + # If genes are in rows -> transpose + if (overlap_rows > overlap_cols) { + df <- t(df) + message("Transposed drug signature matrix (genes were in rows).") + + } + + if (overlap_cols == 0 && overlap_rows == 0) { + stop("No gene names overlap between drug file and Seurat object.") + } + + return(as.data.frame(df)) + } + + # otherwise use built-in + + df <- LINCS.ResponseSigs() + # remove "Genes" in column + if ("Genes" %in% colnames(df)) { + rownames(df) <- df$Genes + df$Genes <- NULL + } + + return(df) + + + }) + + compounds <- reactive({ + + req(RDS_Final_CorrMat()) + rownames(RDS_Final_CorrMat()) + + + }) + + output$referenceCompound_ui <- renderUI({ + req(input$Reference_L1000_or_Custom == "L1000 Derived") + req(compounds()) + + print("Rendering dropdown") + print(compounds()) + + selectizeInput( + "referenceCompound", + "Select reference compound", + choices = compounds(), + selected = compounds()[1] + ) + }) + outputOptions(output, 'referenceCompound_ui', suspendWhenHidden = FALSE) + + perturbationSuccess <- reactiveVal(FALSE) + + output$perturbationSuccess <- reactive({ + perturbationSuccess() + }) + outputOptions(output, "perturbationSuccess", suspendWhenHidden = FALSE) + + ################################################################################ # ################################################################################ @@ -105,6 +221,18 @@ server <- function(input, output) { outputOptions(output, 'seuratNotLoaded', suspendWhenHidden = FALSE) + observe({ + req(rdsSeurat()) + assays <- names(rdsSeurat()@assays) + + updateSelectInput( + session, + "assayChoice", + choices = assays, + selected = assays[1] + ) + }) + # Custom TCS Upload ############################################################################## # handle upload of Seurat object @@ -143,7 +271,7 @@ server <- function(input, output) { tags$head(tags$style(".modal-dialog{ width:85% !important;}")), DT::dataTableOutput("customTCS_table"), br(), - "plotSomethingHere", + #"plotSomethingHere", easyClose = TRUE, footer = modalButton("Close") )) @@ -153,28 +281,29 @@ server <- function(input, output) { # Handle upload of pre-calculated correlation matrix corrMatUpload <- reactive({ req(input$corrMatUpload) - read.csv(input$corrMatUpload$datapath, row.names = 1, header = T) + df <- read.csv(input$corrMatUpload$datapath, row.names = 1, header = T) + return(as.data.frame(df)) }) - cm <- reactive({ - if (class(corrMatUpload()) == "data.frame"){ - return(NULL) - } + observeEvent(input$corrMatUpload, { + print("=== UPLOADED CORR MATRIX DETECTED ===") + RDS_Final_CorrMat(corrMatUpload()) }) + output$corrMatUploaded <- reactive({ - return(is.null(cm())) + !is.null(input$corrMatUpload) }) outputOptions(output, 'corrMatUploaded', suspendWhenHidden = FALSE) output$corrMatNotUploaded <- reactive({ - return(is.null(input$corrMatUpload$datapath)) + is.null(input$corrMatUpload) }) outputOptions(output, 'corrMatNotUploaded', suspendWhenHidden = FALSE) output$L1000_release_InSilico <- renderUI({ # select from dropdown which L1000 release to use for in silico perturbation: selectInput('L1000_Release', #input$groupByRDS - choices = c("2015", "2017", "2021"), + choices = c( "2017","Custom Upload"), label = "Select L1000 Release", selected = "2017", multiple = F) @@ -376,6 +505,7 @@ server <- function(input, output) { RDSseurat <- rdsSeurat() RDSseurat <- SetIdent(RDSseurat, value = input$groupByRDS) + #if not orthogonal print "non-orthogonal" else set default assay as RNA_ortho if (is.null(isOrthogonAL()) == TRUE){ # is this backwards? ############################################################################################# < print("Is null = F, non-orthogonal") } else { @@ -954,40 +1084,80 @@ server <- function(input, output) { # Calculate correlation matrix between L1000 compounds and single-cells in RDS upload. RDS_Final_CorrMat <- reactiveVal() + observeEvent(eventExpr = input$CalculateRDS_L1000_Spearman_Mat, { # Here, we should add dropdown menu to select data slot to pull scale.data from... + if (!is.null(input$corrMatUpload)) { + + # check if we are using uploaded correlation matrix first + print("=== USING UPLOADED CORRELATION MATRIX ===") + return() + } + + # else continue correlation calculation with selected or uploaded drug signatures RDSseurat <- rdsSeurat() + assay <- assay_to_use() + layer <- layer_to_use() + + print(paste("Using assay:", assay)) + print(paste("Using layer:", layer)) + + progress5 <- shiny::Progress$new() + progress5$set(message = "Transposing scale.data slot") + # Handle scaling if needed if (unlist(SeuratObject::Version(RDSseurat))[1] < 5) { - if (dim(RDSseurat@assays$RNA@scale.data)[1] == 0){ + #if (dim(RDSseurat@assays$RNA@scale.data)[1] == 0){ + if (dim(RDSseurat@assays[[assay]]@scale.data)[1] == 0){ print("scale.data slot is empty - scaling data") + DefaultAssay(RDSseurat) <- assay RDSseurat <- ScaleData(RDSseurat, do.center = T) } + + total.transpose <- t(RDSseurat@assays[[assay]]@scale.data) + } else { print("V5 Seurat Object Detected: server.R #853") - if (is.null(isOrthogonAL()) == TRUE){ # is this backwards? ############################################################################################# < - print("Is null = F, non-orthogonal") - # if (dim(RDSseurat@assays$RNA@layers$scale.data)[1] == 0){ - # print("scale.data slot is empty - scaling data") - # RDSseurat <- ScaleData(RDSseurat, do.center = T) - # } - } else { - print("Is.null = T, OrthogonalObject") - DefaultAssay(RDSseurat) <- c("RNA_ortho") - # if (dim(RDSseurat@assays$RNA_ortho@layers$scale.data)[1] == 0){ - # print("scale.data slot is empty - scaling data") - # RDSseurat <- ScaleData(RDSseurat, do.center = T) - # } + + mat <- tryCatch({ + GetAssayData(RDSseurat, assay = assay, layer = "scale.data") + }, error = function(e) NULL) + + if (is.null(mat) || nrow(mat) == 0){ + print("scale.data missing - scaling") + DefaultAssay(RDSseurat) <- assay + RDSseurat <- ScaleData(RDSseurat, do.center = TRUE) } - # DefaultAssay(RDSseurat) <- "RNA_ortho" - # if (dim(RDSseurat@assays$RNA_ortho@layers$scale.data)[1] == 0){ - # print("scale.data slot is empty - scaling data") - # RDSseurat <- ScaleData(RDSseurat, do.center = T) - # } + + total.transpose <- t(GetAssayData( + RDSseurat, + assay = assay, + layer = layer + )) } - a <- LINCS.ResponseSigs() - a$compound <- rownames(a) - progress5 <- shiny::Progress$new() - progress5$set(message = "Transposing scale.data slot") + total.transpose <- as.matrix(total.transpose) + + + # if (is.null(isOrthogonAL()) == TRUE){ # is this backwards? ############################################################################################# < + # print("Is null = F, non-orthogonal") + # # if (dim(RDSseurat@assays$RNA@layers$scale.data)[1] == 0){ + # # print("scale.data slot is empty - scaling data") + # # RDSseurat <- ScaleData(RDSseurat, do.center = T) + # # } + # } else { + # print("Is.null = T, OrthogonalObject") + # DefaultAssay(RDSseurat) <- c("RNA_ortho") + # # if (dim(RDSseurat@assays$RNA_ortho@layers$scale.data)[1] == 0){ + # # print("scale.data slot is empty - scaling data") + # # RDSseurat <- ScaleData(RDSseurat, do.center = T) + # # } + # } + # # DefaultAssay(RDSseurat) <- "RNA_ortho" + # # if (dim(RDSseurat@assays$RNA_ortho@layers$scale.data)[1] == 0){ + # # print("scale.data slot is empty - scaling data") + # # RDSseurat <- ScaleData(RDSseurat, do.center = T) + # # } + # } + # if (is.null(isOrthogonAL()) == FALSE){ # is this backwards? # print("Is null = F, orthogonal") @@ -996,35 +1166,48 @@ server <- function(input, output) { # DefaultAssay(RDSseurat) <- c("RNA_ortho") # } - if (is.null(isOrthogonAL()) == TRUE){ # is this backwards? ############################################################################################# < - print("Is null = F, non-orthogonal") - } else { - print("Is.null = T, OrthogonalObject") - DefaultAssay(RDSseurat) <- c("RNA_ortho") - } + # if (is.null(isOrthogonAL()) == TRUE){ # is this backwards? ############################################################################################# < + # print("Is null = F, non-orthogonal") + # } else { + # print("Is.null = T, OrthogonalObject") + # #DefaultAssay(RDSseurat) <- c("RNA_ortho") + # } + ########################################################################################################################################## - if (unlist(SeuratObject::Version(RDSseurat))[1] < 5){ - print("line 909: detected as less than v5") - # if (is.null(isOrthogonAL()) == FALSE){ # is this backwards? - # print("Is null = F, non-orthogonal") - # } else { - # print("Is.null = T, OrthogonalObject") - # DefaultAssay(RDSseurat) <- c("RNA_ortho") - # } - total.transpose <- t(RDSseurat@assays$RNA@scale.data) # which assay to integrate with? - } else { - print("We're here 927") - if (is.null(isOrthogonAL()) == TRUE){ # is this backwards? - print("Is.null = T, non-OrthogonalObject") - # DefaultAssay(RDSseurat) <- c("RNA_ortho") - # total.transpose <- t(RDSseurat@assays$RNA_ortho@layers$scale.data) # which assay to integrate with? - total.transpose <- t(GetAssayData(RDSseurat, assay = "RNA", layer = "scale.data")) - } else { - print("Is null = F, orthogonal") - # total.transpose <- t(RDSseurat@assays$RNA@layers$scale.data) # which assay to integrate with? - total.transpose <- t(GetAssayData(RDSseurat, assay = "RNA_ortho", layer = "scale.data")) - } - } + # if (unlist(SeuratObject::Version(RDSseurat))[1] < 5){ + # print("line 909: detected as less than v5") + # + # # if (is.null(isOrthogonAL()) == FALSE){ # is this backwards? + # # print("Is null = F, non-orthogonal") + # # } else { + # # print("Is.null = T, OrthogonalObject") + # # DefaultAssay(RDSseurat) <- c("RNA_ortho") + # # } + # + # #total.transpose <- t(RDSseurat@assays$RNA@scale.data) # which assay to integrate with? + # total.transpose <- t(RDSseurat@assays[[assay]]@scale.data) + # } else { + # + # print("We're here 927") + # + # # if (is.null(isOrthogonAL()) == TRUE){ # is this backwards? + # # print("Is.null = T, non-OrthogonalObject") + # # # DefaultAssay(RDSseurat) <- c("RNA_ortho") + # # # total.transpose <- t(RDSseurat@assays$RNA_ortho@layers$scale.data) # which assay to integrate with? + # # total.transpose <- t(GetAssayData(RDSseurat, assay = "RNA", layer = "scale.data")) + # # } else { + # # print("Is null = F, orthogonal") + # # # total.transpose <- t(RDSseurat@assays$RNA@layers$scale.data) # which assay to integrate with? + # # total.transpose <- t(GetAssayData(RDSseurat, assay = "RNA_ortho", layer = "scale.data")) + # # } + # + # total.transpose <- t(GetAssayData( + # RDSseurat, + # assay = assay, + # layer = layer + # )) + # + # } ################################################################################################################################################## progress5$close() @@ -1032,16 +1215,34 @@ server <- function(input, output) { on.exit(progress6$close()) progress6$set(message = "Calculating Drug-Cell Connectivity", value = 0) + a <- drugSignatures() + a$compound <- rownames(a) + + print(class(total.transpose)) + print(mode(total.transpose)) + # loop through compounds counter <- 1 Final_Matrix <- data.frame() - for (i in 1:length(rownames(LINCS.ResponseSigs()))){ - cmpdToOverlay <- rownames(LINCS.ResponseSigs())[i] - progress6$inc(1/length(rownames(LINCS.ResponseSigs())), detail = paste("Calculating Correlations Against ", cmpdToOverlay)) + for (i in seq_len(nrow(a))) { + + + + cmpdToOverlay <- rownames(a)[i] + + progress6$inc( + 1 / nrow(a), + detail = paste("Calculating Correlations Against", cmpdToOverlay) + ) cmpd <- subset(a, a$compound == cmpdToOverlay) cmpd$Genes <- NULL + + # colnames(total.transpose) <- toupper(gsub("-", ".", colnames(total.transpose))) + # colnames(cmpd) <- toupper(gsub("-", ".", colnames(cmpd))) + cmpdGenes <- colnames(cmpd) - cmpd_overlap <- colnames(total.transpose)[which(colnames(total.transpose) %in% cmpdGenes)] + #cmpd_overlap <- colnames(total.transpose)[which(colnames(total.transpose) %in% cmpdGenes)] + cmpd_overlap <- intersect(colnames(total.transpose), cmpdGenes) total.transpose.cmpd <- total.transpose[,cmpd_overlap] # FileA cmpd_ordered <- as.numeric(as.vector(t(cmpd))) names(cmpd_ordered) <- colnames(cmpd) @@ -1049,11 +1250,12 @@ server <- function(input, output) { # head(colnames(total.transpose.cmpd) == names(cmpd_ordered2)) SC <- cor(cmpd_ordered2, t(total.transpose.cmpd), method = "spearman") Final_Matrix <- rbind(Final_Matrix, SC) - rownames(Final_Matrix) <- rownames(LINCS.ResponseSigs())[1:counter] + rownames(Final_Matrix) <- rownames(a)[1:counter] counter <- counter + 1 } Final_Matrix <- na.omit(Final_Matrix) RDS_Final_CorrMat(Final_Matrix) + }) ## Custom TCS Scoring @@ -1090,7 +1292,16 @@ server <- function(input, output) { # } } - a <- LINCS.ResponseSigs() + a <- drugSignatures() + + #DEBUG ORIENTATION + print("=== CHECKING drugSignatures() ===") + print(dim(a)) + print("Row names (should be compounds):") + print(head(rownames(a))) + print("Col names (should be genes):") + print(head(colnames(a))) + a$compound <- rownames(a) progress5 <- shiny::Progress$new() progress5$set(message = "Transposing scale.data slot") @@ -1228,16 +1439,35 @@ server <- function(input, output) { # }) seurat_corradded <- reactive({ + if (input$uploadCorrelationMatrix == TRUE){ req(input$uploadCorrelationMatrix == TRUE) + print("Am here uploadedcorrmatrix") obj <- rdsSeurat() + Final_Matrix <- as.data.frame(t(corrMatUpload())) + + print("rownames of Final_Matrix:") + print(head(rownames(Final_Matrix))) + print("colnames of obj") + print(head(colnames(obj))) + obj <- AddMetaData(obj, metadata = Final_Matrix) + obj } else { req(input$CalculateRDS_L1000_Spearman_Mat) + print(input$CalculateRDS_L1000_Spearman_Mat) + print("Am here after spearman calc") obj <- rdsSeurat() + + Final_Matrix <- as.data.frame(t(RDS_Final_CorrMat())) + print("rownames of Final_Matrix:") + print(head(rownames(Final_Matrix))) + print("colnames of obj") + print(head(colnames(obj))) + obj <- AddMetaData(obj, metadata = Final_Matrix) obj } @@ -1280,7 +1510,7 @@ server <- function(input, output) { compoundSignature <- as.data.frame(t(compoundSignature)) # set-up compoundSignature$Genes <- rownames(compoundSignature) # set-up compoundSignature <- subset(compoundSignature, compoundSignature[input$referenceCompound] != 0) # remove zero values - for (i in 1:length(rownames(compoundSignature))){ # Assign positive or negative values so we can color by that. + for (i in seq_len(nrow(compoundSignature))){ # Assign positive or negative values so we can color by that. if (compoundSignature[i, input$referenceCompound] > 0){ compoundSignature$PosNeg[i] <- "Pos" } @@ -1603,16 +1833,41 @@ server <- function(input, output) { RDSseurat <- SetIdent(RDSseurat, value = input$groupByRDS) tumorCells <- WhichCells(RDSseurat, idents = input$cancerCellIdentsRDS) - compoundSpearmans <- RDSseurat@meta.data[input$referenceCompound] # How to do this for the RDS upload... where is the spearman matrix and do we add it as metadata to the seurat object? + # compoundSpearmans <- RDSseurat@meta.data[input$referenceCompound] # How to do this for the RDS upload... where is the spearman matrix and do we add it as metadata to the seurat object? + # + # living <- subset(compoundSpearmans, subset = compoundSpearmans[input$referenceCompound] > input$correlationCutoff_RDS) + # dead <- subset(compoundSpearmans, subset = compoundSpearmans[input$referenceCompound] < input$correlationCutoff_RDS) + # livingCells <- rownames(living) + # deadCells <- rownames(dead) + # - living <- subset(compoundSpearmans, subset = compoundSpearmans[input$referenceCompound] > input$correlationCutoff_RDS) - dead <- subset(compoundSpearmans, subset = compoundSpearmans[input$referenceCompound] < input$correlationCutoff_RDS) - livingCells <- rownames(living) - deadCells <- rownames(dead) + compoundSpearmans <- RDSseurat@meta.data[input$referenceCompound] + vals <- compoundSpearmans[[1]] + cut <- input$correlationCutoff_RDS + + deadCells <- rownames(compoundSpearmans)[vals <= cut[1]] # Sensitive + livingCells <- rownames(compoundSpearmans)[vals >= cut[2]] # Resistant + + print(input$correlationCutoff_RDS) + + vals <- RDSseurat@meta.data[[input$referenceCompound]] + + print("Slider value:") + print(input$correlationCutoff_RDS) + + print("Correlation summary:") + print(summary(vals)) resistantCells(livingCells) sensitiveCells(deadCells) + print(paste("Sensitive:", length(deadCells))) + print(paste("Resistant:", length(livingCells))) + + groupsValid <- reactive({ + length(deadCells) >= 10 && length(livingCells) >= 10 + }) + p1 <- DimPlot(RDSseurat, reduction = input$reductionUseRDS, cells.highlight = intersect(deadCells, tumorCells), sizes.highlight = 0.001, cols.highlight = c("blue")) + theme_void() @@ -1651,6 +1906,8 @@ server <- function(input, output) { dead <- subset(compoundSpearmans, subset = compoundSpearmans[input$Custom_TCS_nameInput] < input$correlationCutoff_customTCS) livingCells <- rownames(living) deadCells <- rownames(dead) + + resistantCells_customTCS(livingCells) sensitiveCells_customTCS(deadCells) @@ -1744,7 +2001,7 @@ server <- function(input, output) { # # Should just do this now because will want to include this in deltaMean part. - for (i in 1:length(rownames(resistantMeans))){ # Assign positive or negative values so we can color by that. + for (i in seq_len(nrow(resistantMeans))){ # Assign positive or negative values so we can color by that. if (resistantMeans$mean[i] > 0){ resistantMeans$PosNeg[i] <- "Pos" } @@ -1753,7 +2010,7 @@ server <- function(input, output) { } } - for (i in 1:length(rownames(sensitiveMeans))){ + for (i in seq_len(nrow(sensitiveMeans))){ if (sensitiveMeans$mean[i] > 0){ sensitiveMeans$PosNeg[i] <- "Pos" } @@ -1762,7 +2019,7 @@ server <- function(input, output) { } } - for (i in 1:length(rownames(deltaMeans))){ + for (i in seq_len(nrow(deltaMeans))){ if (deltaMeans$deltaMean[i] > 0){ deltaMeans$PosNeg[i] <- "Pos" } @@ -1928,6 +2185,7 @@ server <- function(input, output) { zmat_custTCS <- reactiveVal() seurRDS_customTCSsensitivity <- reactiveVal() seurRDS_customTCS_sensitivityAssigner <- reactiveVal() + observeEvent(eventExpr = input$perturbationButton_customTCS, { if (input$perturbationButton_customTCS >= 1){ RDSseurat <- seurat_custom_corradded() @@ -1985,9 +2243,14 @@ server <- function(input, output) { zmat <- reactiveVal() seurRDSsensitivity <- reactiveVal() seurRDS_sensitivityAssigner <- reactiveVal() + observeEvent(eventExpr = input$perturbationButton, { + perturbationSuccess(FALSE) if (input$perturbationButton >= 1){ + RDSseurat <- seurat_corradded() + + RDSseurat <- SetIdent(RDSseurat, value = input$groupByRDS) if (input$uploadCorrelationMatrix == TRUE){ testSpearman <- as.data.frame(t(corrMatUpload())) @@ -2008,6 +2271,20 @@ server <- function(input, output) { rownames(sensitivityAssigner) <- sensitivityAssigner$cells sensitivityAssigner$cells <- NULL + print(table(sensitivityAssigner$sensitivity)) + + if (length(unique(sensitivityAssigner$sensitivity)) < 2) { + showNotification( + "You must select a slider range that produces BOTH sensitive and resistant cells.", + type = "error", + duration = 5 + ) + perturbationSuccess(FALSE) + return(NULL) + } + + + RDSseurat <- AddMetaData(RDSseurat, metadata = sensitivityAssigner) seurRDSsensitivity(RDSseurat) sensitivityAssigner <- subset(sensitivityAssigner, rownames(sensitivityAssigner) %in% tumorCells) @@ -2022,8 +2299,12 @@ server <- function(input, output) { # sensitivity <- subset(sensitivity, rownames(sensitivity) %in% tumorCells) sensitivity <- sensitivity[which(rownames(sensitivity) %in% rownames(testSpearman)),] # subject_group <- interaction(sensitivity$sensitivity, subjectID$subjectID) + sensitivity <- factor(sensitivity) subjectID <- factor(subjectID) + + + design <- model.matrix(~ subjectID + sensitivity) z_matrix <- t(z_matrix) zmat(z_matrix) @@ -2033,6 +2314,7 @@ server <- function(input, output) { limmaRes(results) print(head(results)) print(head(colnames(z_matrix))) + perturbationSuccess(TRUE) } }) @@ -2065,7 +2347,7 @@ server <- function(input, output) { y = "adj.P.Val", FCcutoff = 0.01, title = NULL, subtitle = NULL, xlim = c(min(res[["logFC"]], na.rm = TRUE) - 0.1, max(res[["logFC"]], na.rm = TRUE) + 0.1) # ylim = c(0, max(-log10(res[["adj.P.Val"]]), na.rm = TRUE) + 5) - ) + ) volc_facet <- volc + ggforce::facet_zoom(xlim = c(min(res[["logFC"]], na.rm = TRUE) - 0.05,0)) volc_facet <- volc_facet + theme(panel.spacing.y = unit(0.1, "cm")) volc_facet @@ -3409,6 +3691,7 @@ server <- function(input, output) { axis.text.x = element_text(angle = 45, hjust = 1), legend.position = "none") + # --- COMBINE PLOTS AND RETURN --- combined_row <- cowplot::plot_grid(h, p, d, ncol = 3, rel_widths = c(1, 5, 1.2)) return(combined_row) diff --git a/inst/shiny/tests/testthat/fixtures/downsampled_seuratObj.RDS b/inst/shiny/tests/testthat/fixtures/downsampled_seuratObj.RDS new file mode 100644 index 0000000..45504d4 --- /dev/null +++ b/inst/shiny/tests/testthat/fixtures/downsampled_seuratObj.RDS @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c193a70be32f485ea78aca07b2ec113fb9ece9254a63418f3ff6d944c1223a7e +size 134353296 diff --git a/inst/shiny/tests/testthat/setup-chromote.R b/inst/shiny/tests/testthat/setup-chromote.R new file mode 100644 index 0000000..948c327 --- /dev/null +++ b/inst/shiny/tests/testthat/setup-chromote.R @@ -0,0 +1,3 @@ +cr <- chromote::Chromote$new() +cr$default_timeout <- 120 +chromote::set_default_chromote_object(cr) diff --git a/inst/shiny/tests/testthat/test-edge-cases.R b/inst/shiny/tests/testthat/test-edge-cases.R new file mode 100644 index 0000000..39d47cd --- /dev/null +++ b/inst/shiny/tests/testthat/test-edge-cases.R @@ -0,0 +1,156 @@ +library(shinytest2) + +# ── Helpers ────────────────────────────────────────────────────────────────── + +make_app <- function(name) { + AppDriver$new( + app_dir = system.file("shiny", package = "scFOCAL"), + name = name, + load_timeout = 120000, + timeout = 120000 + ) +} + +# Build a drugs x cells corrMat CSV compatible with the fixture Seurat object +make_corr_mat_csv <- function(drug_names = c("DrugA", "DrugB", "DrugC")) { + seurat_obj <- readRDS(test_path("fixtures/downsampled_seuratObj.RDS")) + cells <- colnames(seurat_obj) + mat <- matrix( + runif(length(drug_names) * length(cells)), + nrow = length(drug_names), + dimnames = list(drug_names, cells) + ) + path <- tempfile(fileext = ".csv") + write.csv(as.data.frame(mat), path) + path +} + +# Build a custom drug signature CSV with lowercase gene names (mismatch test) +make_lowercase_drug_sig_csv <- function() { + seurat_obj <- readRDS(test_path("fixtures/downsampled_seuratObj.RDS")) + genes <- rownames(seurat_obj) + drugs <- c("Drug1", "Drug2") + mat <- matrix( + rnorm(length(drugs) * length(genes)), + nrow = length(drugs), + dimnames = list(drugs, tolower(genes)) + ) + path <- tempfile(fileext = ".csv") + write.csv(as.data.frame(mat), path) + path +} + +# ── Bug 4: app navigated before Seurat upload should not crash ─────────────── + +test_that("app does not crash when accessed before uploading Seurat file", { + # assay_to_use() calls isOrthogonAL() which calls req(input$seurobjRDS) + # before a file is uploaded. The req() should silently pause, not crash. + app <- make_app("pre-upload-nav") + + expect_no_error(app$get_value(output = "debugRelease")) + + app$stop() +}) + +# ── Bug 1: corrMat double transpose — compound dropdown must show drug names ── + +test_that("corrMat upload shows drug names in compound dropdown, not cell barcodes", { + fixture <- normalizePath(test_path("fixtures/downsampled_seuratObj.RDS"), mustWork = FALSE) + skip_if_not(file.exists(fixture), "Seurat fixture not available (requires Git LFS)") + drug_names <- c("DrugA", "DrugB", "DrugC") + corr_mat_path <- make_corr_mat_csv(drug_names) + + app <- make_app("corr-mat-orientation") + + app$upload_file(seurobjRDS = fixture) + app$wait_for_value(output = "seuratLoaded", timeout = 30000) + + app$set_inputs(uploadCorrelationMatrix = TRUE) + app$upload_file(corrMatUpload = corr_mat_path) + # Wait for the server to confirm the upload registered + app$wait_for_value(output = "corrMatUploaded", timeout = 10000) + + # The Reference_L1000_or_Custom radio button lives inside a conditionalPanel + # that is hidden until corrMat is uploaded, so set it explicitly after upload. + app$set_inputs(Reference_L1000_or_Custom = "L1000 Derived") + + # wait_for_value blocks until the output is actually sent by Shiny, + # avoiding the race condition with suspendWhenHidden output evaluation. + compound_ui <- app$wait_for_value(output = "referenceCompound_ui", timeout = 15000) + + # The duplicate uiOutput ID returns a length-2 vector; collapse to one string. + compound_html <- paste(compound_ui, collapse = "\n") + + for (drug in drug_names) { + expect_true( + grepl(drug, compound_html, fixed = TRUE), + label = paste("Compound dropdown should contain", drug, "not cell barcodes") + ) + } + + app$stop() +}) + +# ── Bug 2: duplicate uiOutput — compound dropdown must not be empty ─────────── + +test_that("reference compound dropdown is populated after corrMat upload", { + fixture <- normalizePath(test_path("fixtures/downsampled_seuratObj.RDS"), mustWork = FALSE) + skip_if_not(file.exists(fixture), "Seurat fixture not available (requires Git LFS)") + # uiOutput('referenceCompound_ui') appears in two tabs in ui.R. + # When the same output ID is bound twice, the second binding can + # overwrite the first, leaving one tab's dropdown empty. + drug_names <- c("DrugX", "DrugY") + corr_mat_path <- make_corr_mat_csv(drug_names) + + app <- make_app("duplicate-ui-output") + + app$upload_file(seurobjRDS = fixture) + app$wait_for_value(output = "seuratLoaded", timeout = 30000) + + app$set_inputs(uploadCorrelationMatrix = TRUE) + app$upload_file(corrMatUpload = corr_mat_path) + # Wait for the server to confirm the upload registered + app$wait_for_value(output = "corrMatUploaded", timeout = 10000) + + # The Reference_L1000_or_Custom radio button lives inside a conditionalPanel + # that is hidden until corrMat is uploaded, so set it explicitly after upload. + app$set_inputs(Reference_L1000_or_Custom = "L1000 Derived") + + # wait_for_value blocks until the output is actually sent by Shiny, + # avoiding the race condition with suspendWhenHidden output evaluation. + compound_ui <- app$wait_for_value(output = "referenceCompound_ui", timeout = 15000) + + # The duplicate uiOutput ID returns a length-2 vector; at least one must be non-empty. + expect_true( + any(nchar(trimws(compound_ui)) > 0), + label = "referenceCompound_ui should not be empty after corrMat upload" + ) + + app$stop() +}) + +# ── Bug 5: lowercase gene names in custom drug signature should not crash ───── + +test_that("custom drug signature with lowercase gene names does not crash app", { + fixture <- normalizePath(test_path("fixtures/downsampled_seuratObj.RDS"), mustWork = FALSE) + skip_if_not(file.exists(fixture), "Seurat fixture not available (requires Git LFS)") + # If gene names in the uploaded CSV are lowercase (e.g. tp53) but the + # Seurat object uses uppercase (TP53), overlap detection returns 0 and + # the server calls stop(), crashing the reactive chain silently. + # The app should survive — either by normalising case or showing an error. + sig_path <- make_lowercase_drug_sig_csv() + + app <- make_app("gene-case-mismatch") + + app$upload_file(seurobjRDS = fixture) + app$wait_for_value(output = "seuratLoaded", timeout = 30000) + + app$set_inputs(L1000_Release = "Custom Upload") + app$upload_file(customL1000Upload = sig_path) + app$wait_for_idle(timeout = 15000) + + # If the app crashed, reading any output would throw — this proves it's alive + expect_no_error(app$get_value(output = "debugRelease")) + + app$stop() +}) diff --git a/inst/shiny/tests/testthat/test-upload.R b/inst/shiny/tests/testthat/test-upload.R new file mode 100644 index 0000000..8c56083 --- /dev/null +++ b/inst/shiny/tests/testthat/test-upload.R @@ -0,0 +1,45 @@ +library(shinytest2) + +test_that("app loads without errors", { + app <- AppDriver$new( + app_dir = system.file("shiny", package = "scFOCAL"), + name = "app-loads", + load_timeout = 120000, + timeout = 120000 + ) + + # Confirm the app started — reading any output proves it didn't crash + expect_no_error(app$get_value(output = "debugRelease")) + + app$stop() +}) + +test_that("uploading a valid Seurat RDS shows success message", { + # Resolve to absolute path NOW, before AppDriver can change the working dir + fixture <- normalizePath( + test_path("fixtures/downsampled_seuratObj.RDS"), + mustWork = FALSE + ) + skip_if_not(file.exists(fixture), "Seurat fixture not available (requires Git LFS)") + + app <- AppDriver$new( + app_dir = system.file("shiny", package = "scFOCAL"), + name = "seurat-upload", + load_timeout = 120000, + timeout = 120000 + ) + + app$upload_file(seurobjRDS = fixture) + + # Wait for seuratLoaded to be TRUE. Ignore NULL (pre-upload) and any + # transient FALSE that may arrive while the 134 MB file is being read. + loaded <- app$wait_for_value( + output = "seuratLoaded", + ignore = list(NULL, FALSE), + timeout = 30000 + ) + + expect_true(isTRUE(loaded)) + + app$stop() +}) diff --git a/inst/shiny/ui.R b/inst/shiny/ui.R index d1f4f63..0acd3c6 100644 --- a/inst/shiny/ui.R +++ b/inst/shiny/ui.R @@ -22,22 +22,24 @@ library(scales) library(ggforce) library(EnhancedVolcano) library(DT) +library(scFOCAL) + + options(shiny.maxRequestSize = 30000*1024^2) # increase limit to 15gb? -options(shiny.maxRequestSize = 30000*1024^2) # increase limit to 15gb? ################################################################################ # ################################################################################ -# # LINCS Response Signature Data -# LINCS.ResponseSigs <- read.delim( -# file = "matPH3_2_1_0.2_0.3_L1000_Batch2017_Regina_removed.txt", header = T) -# row.names(LINCS.ResponseSigs) <- LINCS.ResponseSigs$Genes -# LINCS.ResponseSigs <- na.omit(LINCS.ResponseSigs) -# newNames <- gsub("-", ".", rownames(LINCS.ResponseSigs)) -# rownames(LINCS.ResponseSigs) <- newNames -# L1000_genes <- colnames(LINCS.ResponseSigs) -# L1000_compounds <- rownames(LINCS.ResponseSigs) + # # LINCS Response Signature Data + # LINCS.ResponseSigs <- read.delim( + # file = "matPH3_2_1_0.2_0.3_L1000_Batch2017_Regina_removed.txt", header = T) + # row.names(LINCS.ResponseSigs) <- LINCS.ResponseSigs$Genes + # LINCS.ResponseSigs <- na.omit(LINCS.ResponseSigs) + # newNames <- gsub("-", ".", rownames(LINCS.ResponseSigs)) + # rownames(LINCS.ResponseSigs) <- newNames + # L1000_genes <- colnames(LINCS.ResponseSigs) + # L1000_compounds <- rownames(LINCS.ResponseSigs) ui <- fluidPage( tags$script(src = "https://kit.fontawesome.com/070e476711.js"), @@ -77,7 +79,7 @@ ui <- fluidPage( tags$i(class="fa-sharp fa-solid fa-magnifying-glass-chart"), tags$span("- Run scFOCAL - dev") ), #put contents for actual application here - +##### 1. FILE UPLOAD #################################################################################################################################### tabsetPanel( tabPanel(tags$div( tags$i(class = "fa-sharp fa-solid fa-upload"), @@ -106,7 +108,18 @@ ui <- fluidPage( hr(), p(em("Depending on file size, after the file upload above is complete, it will still take some additional time for the data to be loaded into the processing environment. Please be patient.")), br(), + + conditionalPanel(condition = 'output.seuratLoaded', + selectInput( + inputId = "assayChoice", + label = "Select Assay", + choices = NULL + ) + + ) + ), +#### 2. PRE- PROCESSING ############################################################################################################################################################################################ tabPanel(tags$div( tags$i(class = "fa-sharp fa-solid fa-gears"), tags$span("2. Pre-processing"), @@ -166,6 +179,7 @@ ui <- fluidPage( ) ), +#### 3. DISEASE SIGNATURE ##################################################################################################################### tabPanel(tags$div( tags$i(class = "fa-solid fa-signature"), tags$span("3. Disease Signatures"), @@ -319,12 +333,14 @@ ui <- fluidPage( ) ), +##### 4. CELL DRUG CONNECTIVITY ########################################################################################################################################################################### tabPanel(tags$div( tags$i(class = "fa-sharp fa-solid fa-pills"), tags$span("4. Cell-Drug Connectivity"), tags$style(type = "text/css", "li a{color:#000000; font-samily: 'sans-serif', Arial Rounded MT Bold;}") ), + conditionalPanel(condition = "output.seuratNotLoaded", wellPanel( h4("No data detected."), @@ -361,6 +377,19 @@ ui <- fluidPage( conditionalPanel(condition = "output.uploadCorrelationMatrix", hr(), uiOutput("L1000_release_InSilico"), + #verbatimTextOutput("debugRelease"), + conditionalPanel( + condition = "input.L1000_Release == 'Custom Upload'", + fileInput( + inputId = "customL1000Upload", + label = "Upload custom drug signature matrix", + buttonLabel = "Browse...", + placeholder = "No file selected", + width = NULL, + multiple = F, + accept = c(".csv", ".CSV")) + ), + actionButton(inputId = "CalculateRDS_L1000_Spearman_Mat", label = "Calculate single-cell compound discordance") ), checkboxInput(inputId = "uploadCorrelationMatrix", label = "Upload a previously calculated Cell-Drug Connectivity file."), @@ -375,7 +404,7 @@ ui <- fluidPage( ) ), wellPanel( - conditionalPanel(condition = "output.corrMatrixCalculated", + conditionalPanel(condition = "output.corrMatrixCalculated && !input.uploadCorrelationMatrix", hr(), # this part needs fixing... should align with left side h4("Discordance calculations complete. Download to avoid recalculation"), downloadButton(outputId = "RDScorrMatDownload", label = "Download single-cell vs small molecule correlations") @@ -388,6 +417,9 @@ ui <- fluidPage( ) ) ), + + + h5("Step 1.2 (Optional): Calculate cell-drug connectivities for custom TCS input."), # conditionalPanel(condition = "output.TCSuploaded", checkboxInput(inputId = "customTCSasRef", label = "Utilize custom TCS signature?"), @@ -449,13 +481,13 @@ ui <- fluidPage( wellPanel( splitLayout( wellPanel( - selectizeInput(inputId = "referenceCompound", - label = "Select reference compound", - choices = L1000_compounds, - selected = "alisertib", - multiple = F, - width = NULL, - size = NULL), + # selectizeInput(inputId = "referenceCompound", + # label = "Select reference compound", + # choices = NULL, + # multiple = F, + # width = NULL, + # size = NULL), + uiOutput("referenceCompound_ui"), # wellPanel( sliderInput(inputId = "sigMinCutoffRDS", label = "Set visualization cutoff minimum:", @@ -472,7 +504,7 @@ ui <- fluidPage( ) ), # wellPanel( - plotOutput(outputId = "scSynergySeq1RDS"), + plotOutput(outputId = "scSynergySeq1RDS"), # ), hr(), br(), @@ -481,7 +513,7 @@ ui <- fluidPage( textOutput("referenceCompound", ), actionButton("perturbationButton", label = "Run Perturbation Analysis", icon = icon("redo")) ), - conditionalPanel(condition = "input.perturbationButton", + conditionalPanel(condition = "output.perturbationSuccess == true", hr(), h4("Success! Please navigate to the results tab."), hr()) @@ -604,6 +636,18 @@ ui <- fluidPage( conditionalPanel(condition = "output.uploadCorrelationMatrix", hr(), uiOutput("L1000_release_InSilico"), + #verbatimTextOutput("debugRelease"), + conditionalPanel( + condition = "input.L1000_Release == 'Custom Upload'", + fileInput( + inputId = "customL1000Upload", + label = "Upload custom drug signature matrix", + buttonLabel = "Browse...", + placeholder = "No file selected", + width = NULL, + multiple = F, + accept = c(".csv", ".CSV")) + ), actionButton(inputId = "CalculateRDS_L1000_Spearman_Mat", label = "Calculate single-cell compound discordance") ), checkboxInput(inputId = "uploadCorrelationMatrix", label = "Upload a previously calculated Cell-Drug Connectivity file"), @@ -614,11 +658,12 @@ ui <- fluidPage( placeholder = "No file selected", width = NULL, multiple = F, - accept = c(".csv", ".CSV")) + accept = c(".csv", ".CSV")), + ) ), wellPanel( - conditionalPanel(condition = "output.corrMatrixCalculated", + conditionalPanel(condition = "output.corrMatrixCalculated && !input.uploadCorrelationMatrix", hr(), # this part needs fixing... should align with left side h4("Discordance calculations complete. Download to avoid recalculation"), downloadButton(outputId = "RDScorrMatDownload", label = "Download single-cell vs small molecule correlations") @@ -838,6 +883,7 @@ ui <- fluidPage( ) ################################################################################################################################################################### < End of controls uploaded conditional Panel... ) ), +##### IN SILICO PERTUBATION ############################################################################################################### tabPanel(tags$div( tags$i(class = "fa-sharp fa-solid fa-magnifying-glass-chart"), tags$span("5. In Silico Perturbation"), # NEED TO UPDATE CONDITIONS SO THAT RESULTS ARE ONLY PLOTTED UNDER L1000 or CUSTOM from radio buttons... @@ -885,13 +931,14 @@ ui <- fluidPage( wellPanel( splitLayout( wellPanel( - selectizeInput(inputId = "referenceCompound", - label = "Select reference compound", - choices = L1000_compounds, - selected = "alisertib", - multiple = F, - width = NULL, - size = NULL), + # selectizeInput(inputId = "referenceCompound", + # label = "Select reference compound", + # choices = NULL, + # selected = NULL, + # multiple = F, + # width = NULL, + # size = NULL), + uiOutput("referenceCompound_ui"), # wellPanel( sliderInput(inputId = "sigMinCutoffRDS", label = "Set visualization cutoff minimum:", @@ -923,7 +970,7 @@ ui <- fluidPage( actionButton("perturbationButton", label = "Run Perturbation Analysis", icon = icon("redo")) ) ), - conditionalPanel(condition = "input.perturbationButton", + conditionalPanel(condition ="output.perturbationSuccess == true", hr(), h4("Success! Please navigate to the results tab."), hr()) @@ -1021,12 +1068,12 @@ ui <- fluidPage( ), conditionalPanel(condition = "input.CalculateCustomTCSConnectivities == 0", "custom TCS connectivities not yet calculated, please return to the 'Cell-Drug Connectivity' Tab.") - )# , + )# , - ) + ) ), tabPanel(tags$div( @@ -1127,7 +1174,7 @@ ui <- fluidPage( ) ) ) - ), + ), conditionalPanel(condition = "input.perturbationButton_customTCS", hr(), h4("Custom TCS Result"), @@ -1175,7 +1222,7 @@ ui <- fluidPage( # "What goes here?" # , # plotOutput("main_plot_userSet_customTCS", height = "100%") ) - ) + ) ), wellPanel( # "Plot of normalized shift here", @@ -1192,8 +1239,8 @@ ui <- fluidPage( style = "height: 1000px;", plotOutput("main_plot_customTCS", height = "100%") ) - ) - ), + ) + ), tabPanel("Combination Scoring", hr("Rank L1000 Small Molecules for combination with your reference drug using the scFOCAL combination index."), splitLayout( From ad07fcf5960e41394fec3427a746e595f94c67e6 Mon Sep 17 00:00:00 2001 From: Sabila Bernard <60706407+sabila-bernard@users.noreply.github.com> Date: Thu, 28 May 2026 13:02:20 -0400 Subject: [PATCH 5/6] chore: remove Claude workflow from AyadLab PR branch --- .github/workflows/claude-code-review.yml | 44 ------------------------ 1 file changed, 44 deletions(-) delete mode 100644 .github/workflows/claude-code-review.yml diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml deleted file mode 100644 index b5e8cfd..0000000 --- a/.github/workflows/claude-code-review.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Claude Code Review - -on: - pull_request: - types: [opened, synchronize, ready_for_review, reopened] - # Optional: Only run on specific file changes - # paths: - # - "src/**/*.ts" - # - "src/**/*.tsx" - # - "src/**/*.js" - # - "src/**/*.jsx" - -jobs: - claude-review: - # Optional: Filter by PR author - # if: | - # github.event.pull_request.user.login == 'external-contributor' || - # github.event.pull_request.user.login == 'new-developer' || - # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Run Claude Code Review - id: claude-review - uses: anthropics/claude-code-action@v1 - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' - plugins: 'code-review@claude-code-plugins' - prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://code.claude.com/docs/en/cli-reference for available options - From 54dbb4dbe926b59afd5717eba27a2efa1a37697c Mon Sep 17 00:00:00 2001 From: Sabila Bernard <60706407+sabila-bernard@users.noreply.github.com> Date: Thu, 28 May 2026 13:02:20 -0400 Subject: [PATCH 6/6] chore: remove Claude workflow from AyadLab PR branch --- .github/workflows/claude.yml | 50 ------------------------------------ 1 file changed, 50 deletions(-) delete mode 100644 .github/workflows/claude.yml diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml deleted file mode 100644 index 6b15fac..0000000 --- a/.github/workflows/claude.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Claude Code - -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - issues: - types: [opened, assigned] - pull_request_review: - types: [submitted] - -jobs: - claude: - if: | - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - actions: read # Required for Claude to read CI results on PRs - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Run Claude Code - id: claude - uses: anthropics/claude-code-action@v1 - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - - # This is an optional setting that allows Claude to read CI results on PRs - additional_permissions: | - actions: read - - # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. - # prompt: 'Update the pull request description to include a summary of changes.' - - # Optional: Add claude_args to customize behavior and configuration - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://code.claude.com/docs/en/cli-reference for available options - # claude_args: '--allowed-tools Bash(gh pr *)' -