diff --git a/R/writer0.R b/R/writer0.R index b32ce3c..f7c086b 100644 --- a/R/writer0.R +++ b/R/writer0.R @@ -167,8 +167,9 @@ wrShFunc <- function() { ' xlab(paste0(inpdr,"1")) + ylab(paste0(inpdr,"2")) + \n', ' sctheme(base_size = inpfsz, XYval = inptxt) \n', ' if(ggCont){{ \n', - ' ggOut = ggOut + scale_color_gradientn(paste0(inp1, " "), colours = inpcol) + \n', - ' guides(color = guide_colorbar(barwidth = 15)) \n', + ' ggOut = ggOut + scale_color_gradientn(paste0(inp1, " "), colours = inpcol) +\n', + ' guides(color = guide_colorbar(barwidth = 15)) +\n', + ' theme(legend.text = element_text(angle = 45, hjust = 1)) \n', ' }} else {{ \n', ' legfsz = min(nchar(paste0(levels(ggData$val), collapse = "")), 200) \n', ' legfsz = 0.75 * (inpfsz - (1.5 * floor(legfsz/50))) \n', @@ -322,7 +323,7 @@ wrShFunc <- function() { ' if(ggCont1 == TRUE & ggCont2 == TRUE){{ \n', ' # Both cont: Scatter plot \n', ' ggOut <- ggplot(ggData, aes(val1, val2)) + \n', - ' geom_point() + sctheme(base_size = inpfsz) + xlab(inp1) + ylab(inp2) \n', + ' geom_point() + sctheme(base_size = inpfsz, Xang = 45, XjusH = 1) + xlab(inp1) + ylab(inp2) \n', ' }} else if(ggCont1 == FALSE & ggCont2 == FALSE){{ \n', ' # Both cate: Confusion matrix \n', ' allCombi <- CJ(val1 = unique(ggData$val1), val2 = unique(ggData$val2)) \n', @@ -332,8 +333,9 @@ wrShFunc <- function() { ' ggOut <- ggplot(ggData, aes(val1, val2, fill = N, label = N)) + \n', ' geom_tile(color = "white") + geom_text(size = inpfsz/5) + \n', ' scale_fill_gradient(low = "white", high = "steelblue") + \n', - ' theme_minimal(base_size = inpfsz) + xlab(inp1) + ylab(inp2) + \n', - ' theme(axis.text.x = element_text(angle = 45, hjust = 1)) + \n', + ' theme_minimal(base_size = inpfsz) + xlab(inp1) + ylab(inp2) +\n', + ' theme(axis.text.x = element_text(angle = 45, hjust = 1),\n', + ' legend.text = element_text(angle = 45, hjust = 1)) +\n', ' ggtitle("Confusion matrix") \n', ' }} else {{ \n', ' # Cate-cont: Ask user to use VlnBoxP directly! \n', @@ -347,7 +349,7 @@ wrShFunc <- function() { '# Plot X/Y relationship (either confusion matrix or scatter plot) \n', 'sc2Dcnum <- function(inpConf, inpMeta, inp1, inp2, inpH5, inpGene, \n', ' inpDtyp1, inpDtyp2, inpsub1, inpsub2, \n', - ' inpmin1, inpmax1, inpmin2, inpmax2, inpcut = 0){{ \n', + ' inpmin1, inpmax1, inpmin2, inpmax2){{ \n', ' # Tidy inputs \n', ' inpDtyp1 = gsub("^Assay: ", "", inpDtyp1) \n', ' inpDtyp2 = gsub("^Assay: ", "", inpDtyp2) \n', @@ -416,15 +418,17 @@ wrShFunc <- function() { ' # Both cont: Pearson / Spearman corr \n', ' ggP <- round(cor(ggData$val1, ggData$val2, method = "pearson"), 6) \n', ' ggS <- round(cor(ggData$val1, ggData$val2, method = "spearman"), 6) \n', - ' ggK <- round(cor(ggData$val1, ggData$val2, method = "kendall"), 6) \n', + ' # Kendall correlation is O(n^2) and extremely slow on large datasets\n', + ' # (~150s for ~95k cells); disabled for performance.\n', + ' # ggK <- round(cor(ggData$val1, ggData$val2, method = "kendall"), 6) \n', ' ggModel <- lm(val2 ~ val1, data = ggData)\n', ' ggCoeff <- signif(coef(ggModel), 6)\n', ' ggData = data.table(measures = c("Pearson Corr.",\n', ' "Spearman Corr.",\n', - ' "Kendall Corr.",\n', + ' # "Kendall Corr.",\n', ' "LM (Y/RHS~X/LHS) Gradient",\n', ' "LM (Y/RHS~X/LHS) Intercept"),\n', - ' value = c(ggP, ggS, ggK, ggCoeff[2], ggCoeff[1]))\n', + ' value = c(ggP, ggS, ggCoeff[2], ggCoeff[1]))\n', ' \n', ' }} else if(ggCont1 == FALSE & ggCont2 == FALSE){{ \n', ' # Both cate: Confusion matrix \n', @@ -436,16 +440,8 @@ wrShFunc <- function() { ' if(ggCont1 == TRUE){{\n', ' colnames(ggData) <- c("sub", "val2", "val1")\n', ' }} \n', - ' ggData$express = FALSE \n', - ' ggData[val2 > inpcut]$express = TRUE \n', - ' ggData1 = ggData[express == TRUE, .(nExpress = .N), by = "val1"] \n', ' ggData = ggData[, .(nCells = .N), by = "val1"] \n', - ' ggData = ggData1[ggData, on = "val1"] \n', - ' ggData = ggData[, c("val1", "nCells", "nExpress"), with = FALSE] \n', - ' ggData[is.na(nExpress)]$nExpress = 0 \n', - ' ggData$pctExpress = round(100 * ggData$nExpress / ggData$nCells, 2)\n', ' colnames(ggData)[1] = "group"\n', - ' colnames(ggData)[3] = paste0(colnames(ggData)[3], "_", inp2) \n', ' ggData = ggData[order(group)] \n', ' }} \n', ' return(ggData) \n', @@ -855,9 +851,10 @@ wrShFunc <- function() { ' scale_y_discrete(expand = c(0, 0.5)) + \n', ' scale_size_continuous("proportion", range = c(0, 8), \n', ' limits = c(0, 1), breaks = c(0.00,0.25,0.50,0.75,1.00)) + \n', - ' scale_color_gradientn("expression", limits = colRange, colours = inpcols) + \n', - ' guides(color = guide_colorbar(barwidth = 15)) + \n', - ' theme(axis.title = element_blank(), legend.box = "vertical") \n', + ' scale_color_gradientn("expression", limits = colRange, colours = inpcols) +\n', + ' guides(color = guide_colorbar(barwidth = 15)) +\n', + ' theme(axis.title = element_blank(), legend.box = "vertical",\n', + ' legend.text = element_text(angle = 45, hjust = 1)) \n', ' }} else {{ \n', ' # Heatmap \n', ' ggOut = ggplot(ggData, aes(grpBy, geneName, fill = val)) + \n', @@ -865,9 +862,10 @@ wrShFunc <- function() { ' sctheme(base_size = inpfsz, Xang = 45, XjusH = 1) + \n', ' scale_x_discrete(expand = c(0.05, 0)) + \n', ' scale_y_discrete(expand = c(0, 0.5)) + \n', - ' scale_fill_gradientn("expression", limits = colRange, colours = inpcols) + \n', - ' guides(fill = guide_colorbar(barwidth = 15)) + \n', - ' theme(axis.title = element_blank()) \n', + ' scale_fill_gradientn("expression", limits = colRange, colours = inpcols) +\n', + ' guides(fill = guide_colorbar(barwidth = 15)) +\n', + ' theme(axis.title = element_blank(),\n', + ' legend.text = element_text(angle = 45, hjust = 1)) \n', ' }} \n', ' \n', ' # Final tidy \n', @@ -950,6 +948,12 @@ wrShFunc <- function() { ' ggData$X = ggData$X * coord_scale\n', ' ggData$Y = ggData$Y * coord_scale\n', ' }}\n', + ' # Flip Y into ggplot\'s bottom-up axis so the spots line up with the tissue\n', + ' # image. We deliberately do NOT use scale_y_reverse() to achieve this:\n', + ' # annotation_custom()/rasterGrob ignore reversed continuous scales and the\n', + ' # background image collapses to an invisible sliver. Flipping the data and\n', + ' # keeping a normal axis lets the raster render correctly.\n', + ' ggData$Y = img_h - ggData$Y\n', ' # Extract metadata columns - handle different scenarios for backwards compatibility\n', ' # Check if we need to match spatial cells to metadata\n', ' spatial_cells <- rownames(inpImg$coord)\n', @@ -1084,11 +1088,10 @@ wrShFunc <- function() { ' size = (inpfsz/4), seed = 42) \n', ' }} \n', ' }} \n', - ' ggOut = ggOut + \n', - ' scale_y_reverse() + # Flip Y-axis to match image coordinates\n', - ' coord_fixed(ratio = 1, \n', - ' xlim = c(0, img_w), \n', - ' ylim = c(img_h, 0), # Reversed for scale_y_reverse\n', + ' ggOut = ggOut +\n', + ' coord_fixed(ratio = 1,\n', + ' xlim = c(0, img_w),\n', + ' ylim = c(0, img_h), # Normal axis; Y already flipped in ggData\n', ' expand = FALSE) + \n', ' theme_void(base_size = inpfsz) + theme(\n', ' axis.text = element_blank(), axis.line = element_blank(), \n', @@ -1148,7 +1151,8 @@ wrShFunc <- function() { ' geom_point(size = input_size, shape = 16) +\n', ' facet_wrap(ggData$split, ncol = ncolumn) +\n', ' scale_color_gradientn(ggData$gene, colours = color_pal, name = gene_exp) +\n', - ' guides(color = guide_colorbar(barwidth = 15), fill = guide_legend(title = gene_exp))\n', + ' guides(color = guide_colorbar(barwidth = 15), fill = guide_legend(title = gene_exp)) +\n', + ' theme(legend.text = element_text(angle = 45, hjust = 1))\n', ' calc_ratio <- (max(ggData$X) - min(ggData$X)) / (max(ggData$Y) - min(ggData$Y))\n', ' if (aspect_ratio == "Square") {{\n', ' ggOut <- ggOut + coord_fixed(ratio = calc_ratio)\n', diff --git a/R/writer1.R b/R/writer1.R index 54f23e2..1a2bff5 100644 --- a/R/writer1.R +++ b/R/writer1.R @@ -100,8 +100,8 @@ wrSVpre <- function() { 'shinyServer(function(input, output, session) {{ \n', ' ### For all tags and Server-side selectize \n', ' observe_helpers() \n', - ' optCrt="{{ option_create: function(data,escape) {{return(\'
\' + \'
\');}} }}" \n', - ' observe({{ \n', + ' optCrt="{{ option_create: function(data,escape) {{return(\'
\' + \'
\');}} }}" \n', + ' observe({{ \n', ' invalidateLater(30000) # ping every 30 seconds to keep connection alive \n', ' cat(".") \n', ' }}) \n', @@ -391,6 +391,29 @@ wrSVmainS3 <- function(prefix) { wrSVmainA2 <- function(prefix) { glue::glue( ' ### Functions for tab A2 \n', + ' # Selectize render that colours each option by the data type used in the table\n', + ' # calculations: categorical (green), continuous (blue), or undetermined (orange).\n', + ' # - Categorical = Cell Information with a defined factor colour list (fCL).\n', + ' # - Continuous = Cell Information without an fCL whose values are numeric, plus\n', + ' # all gene/feature expression (always numeric). These take the\n', + ' # ggCont == TRUE path in sc2Dcnum / sc2Dcomp.\n', + ' # - Undetermined = anything that is neither - no fCL and not numeric (e.g.\n', + ' # logical QC flags). Type cannot be exclusively determined.\n', + ' # Genes are not listed in either set; they fall through to the continuous\n', + ' # default below, which is correct.\n', + ' optColCat = jsonlite::toJSON({prefix}conf[!is.na(fCL)]$UI)\n', + ' optColUnd = jsonlite::toJSON({prefix}conf[is.na(fCL) &\n', + ' !sapply(ID, function(i) i %in% colnames({prefix}meta) &&\n', + ' is.numeric({prefix}meta[[i]]))]$UI)\n', + ' optColFn = paste0(\n', + ' "function(item, escape) {{",\n', + ' " var cat = ", optColCat, "; var und = ", optColUnd, ";",\n', + ' " var c = cat.indexOf(item.value) > -1 ? \'#1B7837\' :",\n', + ' " (und.indexOf(item.value) > -1 ? \'#E66101\' : \'#2166AC\');",\n', + ' " return \'
\' + escape(item.label) + \'
\';",\n', + ' "}}")\n', + ' optColor = paste0("{{ option: ", optColFn, ", item: ", optColFn,\n', + ' ", option_create: function(data,escape) {{return(\'
\' + \'
\');}} }}")\n', ' getG{prefix}a2inp1 <- reactive({{ \n', ' req(gsub("^Assay: ", "", input${prefix}a2ass1)) \n', ' if(gsub("^Assay: ", "", input${prefix}a2ass1) == "Cell Information"){{ \n', @@ -408,7 +431,7 @@ wrSVmainA2 <- function(prefix) { ' updateSelectizeInput(session, "{prefix}a2inp1", choices = getG{prefix}a2inp1()[[1]], \n', ' server = TRUE, selected = getG{prefix}a2inp1()[[2]], options = list( \n', ' maxOptions = getG{prefix}a2inp1()[[3]], create = TRUE, \n', - ' persist = TRUE, render = I(optCrt))) \n', + ' persist = TRUE, render = I(optColor))) \n', ' }})\n', ' getG{prefix}a2inp2 <- reactive({{ \n', ' req(gsub("^Assay: ", "", input${prefix}a2ass2)) \n', @@ -427,7 +450,7 @@ wrSVmainA2 <- function(prefix) { ' updateSelectizeInput(session, "{prefix}a2inp2", choices = getG{prefix}a2inp2()[[1]], \n', ' server = TRUE, selected = getG{prefix}a2inp2()[[2]], options = list( \n', ' maxOptions = getG{prefix}a2inp2()[[3]], create = TRUE, \n', - ' persist = TRUE, render = I(optCrt))) \n', + ' persist = TRUE, render = I(optColor))) \n', ' }})\n', ' output${prefix}a2sub1.ui <- renderUI({{ \n', ' sub = strsplit({prefix}conf[UI == input${prefix}a2sub1]$fID, "\\\\|")[[1]] \n', @@ -510,7 +533,7 @@ wrSVmainA2 <- function(prefix) { ' output${prefix}a2.dt <- renderDataTable({{\n', ' ggData = sc2Dcnum({prefix}conf, {prefix}meta, input${prefix}a2inp1, input${prefix}a2inp2,\n', ' "{prefix}assay_", {prefix}gene, input${prefix}a2ass1, input${prefix}a2ass2, input${prefix}a2sub1, input${prefix}a2sub2,\n', - ' input${prefix}a2min1, input${prefix}a2max1, input${prefix}a2min2, input${prefix}a2max2, input${prefix}a2cut)\n', + ' input${prefix}a2min1, input${prefix}a2max1, input${prefix}a2min2, input${prefix}a2max2)\n', ' datatable(ggData, rownames = FALSE, extensions = "Buttons",\n', ' options = list(pageLength = -1, dom = "tB", buttons = c("copy", "csv", "excel")))\n', ' }})\n', @@ -1119,10 +1142,10 @@ wrSVmainS1 <- function(prefix) { ' zoom_factor_y <- img_h / diff({prefix}s1oup1xy$y)\n', ' zoom_factor <- min(zoom_factor_x, zoom_factor_y)\n', ' adjSiz <- input${prefix}s1siz * zoom_factor\n', - ' {prefix}s1oup1() + theme(legend.position = "none") + \n', - ' coord_fixed(ratio = 1, \n', - ' xlim = {prefix}s1oup1xy$x, \n', - ' ylim = rev({prefix}s1oup1xy$y), # Reverse for scale_y_reverse\n', + ' {prefix}s1oup1() + theme(legend.position = "none") +\n', + ' coord_fixed(ratio = 1,\n', + ' xlim = {prefix}s1oup1xy$x,\n', + ' ylim = {prefix}s1oup1xy$y, # Normal axis; brush coords already in flipped data space\n', ' expand = FALSE) + \n', ' scale_size_continuous(range = c(0, adjSiz), limits = c(0,1))\n', ' }}\n', @@ -1143,10 +1166,10 @@ wrSVmainS1 <- function(prefix) { ' zoom_factor <- min(zoom_factor_x, zoom_factor_y)\n', ' adjSiz <- input${prefix}s1siz * zoom_factor\n', ' ggsav(file, height = input${prefix}s1oup1.h, width = input${prefix}s1oup1.w, \n', - ' plot = {prefix}s1oup1() + theme(legend.position = "none") + \n', - ' coord_fixed(ratio = 1, \n', - ' xlim = {prefix}s1oup1xy$x, \n', - ' ylim = rev({prefix}s1oup1xy$y), \n', + ' plot = {prefix}s1oup1() + theme(legend.position = "none") +\n', + ' coord_fixed(ratio = 1,\n', + ' xlim = {prefix}s1oup1xy$x,\n', + ' ylim = {prefix}s1oup1xy$y,\n', ' expand = FALSE) + \n', ' scale_size_continuous(range = c(0, adjSiz), limits = c(0,1)))\n', ' }}\n', diff --git a/R/writer2.R b/R/writer2.R index 9e35630..d6dad0d 100644 --- a/R/writer2.R +++ b/R/writer2.R @@ -76,6 +76,11 @@ wrUIpre <- function(title, ganalytics) { ' {ga} \n', ' tags$head(tags$style(HTML(".shiny-output-error-validation {{color: red; font-weight: bold;}}"))), \n', ' list(tags$style(HTML(".navbar-default .navbar-nav {{ font-weight: bold; font-size: 16px; }}"))), \n', + ' ### Center-align the text inside the colour-coded feature selectors (tab A2)\n', + ' tags$head(tags$style(HTML(paste0(\n', + ' ".centerSelect .selectize-input, ",\n', + ' ".centerSelect .selectize-input input, ",\n', + ' ".centerSelect .selectize-dropdown .option {{ text-align: center; }}")))),\n', ' \n', ' ### Page title \n', ' titlePanel("{title}"), \n', @@ -515,6 +520,13 @@ wrUImainA2 <- function(prefix, ptsiz) { ' "In this tab, users can visualise both cell information and gene ", \n', ' "expression side-by-side on low-dimensional representions.", \n', ' br(),br(), \n', + ' HTML(paste0(\n', + ' "
",\n', + ' "Selector colour key:  ",\n', + ' "■ Categorical  ",\n', + ' "■ Continuous  ",\n', + ' "■ Undetermined",\n', + ' "
")),\n', ' fluidRow( \n', ' column( \n', ' 3, fluidRow(\n', @@ -568,6 +580,7 @@ wrUImainA2 <- function(prefix, ptsiz) { ' 6, selectInput("{prefix}a2ass1", "Data type to colour plot:",\n', ' choices = c("Cell Information", paste0("Assay: ", {prefix}def$assay)), \n', ' selected = "Cell Information"), \n', + ' div(class = "centerSelect",\n', ' selectInput("{prefix}a2inp1", "Cell Info / Feature Name:", choices = NULL) %>% \n', ' helper(type = "inline", size = "m", fade = TRUE,\n', ' title = "Cell Info / Gene to colour cells by",\n', @@ -575,7 +588,7 @@ wrUImainA2 <- function(prefix, ptsiz) { ' "- Categorical covariates have a fixed colour palette",\n', ' paste0("- Continuous covariates / gene expression are coloured ",\n', ' "in a Blue-Yellow-Red colour scheme, which can be ",\n', - ' "changed in the plot controls")))\n', + ' "changed in the plot controls"))))\n', ' ),\n', ' column(\n', ' 6, actionButton("{prefix}a2tog1", "Toggle plot controls"),\n', @@ -620,6 +633,7 @@ wrUImainA2 <- function(prefix, ptsiz) { ' 6, selectInput("{prefix}a2ass2", "Data type to colour plot:",\n', ' choices = c("Cell Information", paste0("Assay: ", {prefix}def$assay)), \n', ' selected = paste0("Assay: ", {prefix}def$assay[1])), \n', + ' div(class = "centerSelect",\n', ' selectInput("{prefix}a2inp2", "Cell Info / Feature Name:", choices = NULL) %>% \n', ' helper(type = "inline", size = "m", fade = TRUE,\n', ' title = "Cell Info / Gene to colour cells by",\n', @@ -627,7 +641,7 @@ wrUImainA2 <- function(prefix, ptsiz) { ' "- Categorical covariates have a fixed colour palette",\n', ' paste0("- Continuous covariates / gene expression are coloured ",\n', ' "in a Blue-Yellow-Red colour scheme, which can be ",\n', - ' "changed in the plot controls")))\n', + ' "changed in the plot controls"))))\n', ' ),\n', ' column(\n', ' 6, actionButton("{prefix}a2tog2", "Toggle plot controls"),\n', @@ -682,7 +696,6 @@ wrUImainA2 <- function(prefix, ptsiz) { ' ),\n', ' column(\n', ' 6, h4("Cell numbers / statistics"), \n', - ' numericInput("{prefix}a2cut", "Cutoff for Expression:", value = 0), \n', ' dataTableOutput("{prefix}a2.dt") \n', ' ) # End of column (6 space) \n', ' ) # End of fluidRow (4 space) \n',