OSCR

Multi-omics reveals heterogeneity and functional populations of oligodendrocyte progenitor cells induced by human neural stem cells.

Code ↔ Paper

5 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 5 matches
  1. [1] § Materials and methods › Single-cell RNA sequencing (scRNA‑seq) of hOPCs ↔ R/visualization.R, lines 1035–1123 · score 0.76 · percent mito, genes detected, cells expressing, dimension reduction, mitochondrial, gene expression
  2. [2] § Materials and methods › Data collection and analysis ↔ R/preprocessing.R, lines 2175–2251 · score 0.70 · Nuclear segmentation, DAPI staining, cytoplasmic, channel, intensity, exported
  3. [3] § Materials and methods › Single-cell RNA sequencing (scRNA‑seq) of hOPCs ↔ R/doubletFinder.R, lines 1–66 · score 0.67 · Doublet Finder, principal components, Seurat, predicted, PCA, matrix
  4. [4] § Materials and methods › GO and KEGG ↔ R/differential_expression.R, lines 455–538 · score 0.61 · DESeq2, fold change, v4, predict, threshold, gene
  5. [5] § Materials and methods › GO and KEGG ↔ R/generics.R, lines 109–170 · score 0.57 · DESeq2, fold change, ClusterProfiler, PRE, gene

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

R · 6,161 lines · 195 KB · other · 1 match

  1. #' @importFrom utils globalVariables
  2. #' @importFrom ggplot2 fortify GeomViolin ggproto
  3. #' @importFrom SeuratObject DefaultDimReduc
  4. #'
  5. NULL
  6. #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  7. # Generics
  8. #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  9. #' @importFrom methods setGeneric
  10. #'
  11. setGeneric(
  12. name = '.PrepImageData',
  13. def = function(data, cells, ...) {
  14. standardGeneric(f = '.PrepImageData')
  15. }
  16. )
  17. #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  18. # Heatmaps
  19. #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  20. #' Dimensional reduction heatmap
  21. #'
  22. #' Draws a heatmap focusing on a principal component. Both cells and genes are sorted by their
  23. #' principal component scores. Allows for nice visualization of sources of heterogeneity in the dataset.
  24. #'
  25. #' @inheritParams DoHeatmap
  26. #' @param dims Dimensions to plot
  27. #' @param nfeatures Number of genes to plot
  28. #' @param cells A list of cells to plot. If numeric, just plots the top cells.
  29. #' @param reduction Which dimensional reduction to use
  30. #' @param balanced Plot an equal number of genes with both + and - scores.
  31. #' @param projected Use the full projected dimensional reduction
  32. #' @param ncol Number of columns to plot
  33. #' @param fast If true, use \code{image} to generate plots; faster than using ggplot2,
  34. #' but not customizable and excludes figure legend in output
  35. #' @param assays A vector of assays to pull data from
  36. #' @param combine Combine plots into a single \code{\link[patchwork]{patchwork}ed} ggplot object with
  37. #' single shared figure legend when \code{fast=FALSE}. If \code{FALSE}, return a list of ggplot objects
  38. #' @param legend.position When \code{combine=TRUE}, allows legend position to be adjusted
  39. #' for \code{\link[patchwork]{patchwork}ed} output (default "right"). See \link[ggplot2]{theme}
  40. #'
  41. #' @return No return value by default. If using fast = FALSE, will return a
  42. #' \code{\link[patchwork]{patchwork}ed} ggplot object if combine = TRUE, otherwise
  43. #' returns a list of ggplot objects
  44. #'
  45. #' @importFrom patchwork wrap_plots plot_layout
  46. #' @export
  47. #' @concept visualization
  48. #'
  49. #' @seealso \code{\link[graphics]{image}} \code{\link[ggplot2]{geom_raster}}
  50. #'
  51. #' @examples
  52. #' data("pbmc_small")
  53. #' DimHeatmap(object = pbmc_small)
  54. #'
  55. DimHeatmap <- function(
  56. object,
  57. dims = 1,
  58. nfeatures = 30,
  59. cells = NULL,
  60. reduction = 'pca',
  61. disp.min = -2.5,
  62. disp.max = NULL,
  63. balanced = TRUE,
  64. projected = FALSE,
  65. ncol = NULL,
  66. fast = TRUE,
  67. raster = TRUE,
  68. slot = 'scale.data',
  69. assays = NULL,
  70. combine = TRUE,
  71. legend.position = "right"
  72. ) {
  73. ncol <- ncol %||% ifelse(test = length(x = dims) > 2, yes = 3, no = length(x = dims))
  74. plots <- vector(mode = 'list', length = length(x = dims))
  75. assays <- assays %||% DefaultAssay(object = object)
  76. disp.max <- disp.max %||% ifelse(
  77. test = slot == 'scale.data',
  78. yes = 2.5,
  79. no = 6
  80. )
  81. if (!DefaultAssay(object = object[[reduction]]) %in% assays) {
  82. warning("The original assay that the reduction was computed on is different than the assay specified")
  83. }
  84. cells <- cells %||% ncol(x = object)
  85. if (is.numeric(x = cells)) {
  86. cells <- lapply(
  87. X = dims,
  88. FUN = function(x) {
  89. cells <- TopCells(
  90. object = object[[reduction]],
  91. dim = x,
  92. ncells = cells,
  93. balanced = balanced
  94. )
  95. if (balanced) {
  96. cells$negative <- rev(x = cells$negative)
  97. }
  98. cells <- unlist(x = unname(obj = cells))
  99. return(cells)
  100. }
  101. )
  102. }
  103. if (!is.list(x = cells)) {
  104. cells <- lapply(X = 1:length(x = dims), FUN = function(x) {return(cells)})
  105. }
  106. features <- lapply(
  107. X = dims,
  108. FUN = TopFeatures,
  109. object = object[[reduction]],
  110. nfeatures = nfeatures,
  111. balanced = balanced,
  112. projected = projected
  113. )
  114. features.all <- unique(x = unlist(x = features))
  115. if (length(x = assays) > 1) {
  116. features.keyed <- lapply(
  117. X = assays,
  118. FUN = function(assay) {
  119. features <- features.all[features.all %in% rownames(x = object[[assay]])]
  120. if (length(x = features) > 0) {
  121. return(paste0(Key(object = object[[assay]]), features))
  122. }
  123. }
  124. )
  125. features.keyed <- Filter(f = Negate(f = is.null), x = features.keyed)
  126. features.keyed <- unlist(x = features.keyed)
  127. } else {
  128. features.keyed <- features.all
  129. DefaultAssay(object = object) <- assays
  130. }
  131. data.all <- FetchData(
  132. object = object,
  133. vars = features.keyed,
  134. cells = unique(x = unlist(x = cells)),
  135. layer = slot
  136. )
  137. data.all <- MinMax(data = data.all, min = disp.min, max = disp.max)
  138. data.limits <- c(min(data.all), max(data.all))
  139. # if (check.plot && any(c(length(x = features.keyed), length(x = cells[[1]])) > 700)) {
  140. # choice <- menu(c("Continue with plotting", "Quit"), title = "Plot(s) requested will likely take a while to plot.")
  141. # if (choice != 1) {
  142. # return(invisible(x = NULL))
  143. # }
  144. # }
  145. if (fast) {
  146. nrow <- floor(x = length(x = dims) / 3.01) + 1
  147. orig.par <- par()$mfrow
  148. par(mfrow = c(nrow, ncol))
  149. }
  150. for (i in 1:length(x = dims)) {
  151. dim.features <- c(features[[i]][[2]], rev(x = features[[i]][[1]]))
  152. dim.features <- rev(x = unlist(x = lapply(
  153. X = dim.features,
  154. FUN = function(feat) {
  155. return(grep(pattern = paste0(feat, '$'), x = features.keyed, value = TRUE))
  156. }
  157. )))
  158. dim.cells <- cells[[i]]
  159. data.plot <- data.all[dim.cells, dim.features]
  160. if (fast) {
  161. SingleImageMap(
  162. data = data.plot,
  163. title = paste0(Key(object = object[[reduction]]), dims[i]),
  164. order = dim.cells
  165. )
  166. } else {
  167. plots[[i]] <- SingleRasterMap(
  168. data = data.plot,
  169. raster = raster,
  170. limits = data.limits,
  171. cell.order = dim.cells,
  172. feature.order = dim.features
  173. )
  174. plots[[i]] <- plots[[i]] +
  175. ggtitle(paste0(Key(object = object[[reduction]]), dims[i])) +
  176. theme(plot.title = element_text(hjust = 0.5, face = "bold"))
  177. }
  178. }
  179. if (fast) {
  180. par(mfrow = orig.par)
  181. return(invisible(x = NULL))
  182. }
  183. if (combine) {
  184. plots <- wrap_plots(plots, ncol = ncol, guides = "collect") +
  185. plot_layout(guides = "collect") &
  186. theme(legend.position = legend.position)
  187. }
  188. return(plots)
  189. }
  190. #' Feature expression heatmap
  191. #'
  192. #' Draws a heatmap of single cell feature expression.
  193. #'
  194. #' @param object Seurat object
  195. #' @param features A vector of features to plot, defaults to \code{VariableFeatures(object = object)}
  196. #' @param cells A vector of cells to plot
  197. #' @param disp.min Minimum display value (all values below are clipped)
  198. #' @param disp.max Maximum display value (all values above are clipped); defaults to 2.5
  199. #' if \code{slot} is 'scale.data', 6 otherwise
  200. #' @param group.by A vector of variables to group cells by; pass 'ident' to group by cell identity classes
  201. #' @param group.bar Add a color bar showing group status for cells
  202. #' @param group.colors Colors to use for the color bar
  203. #' @param slot Data slot to use, choose from 'raw.data', 'data', or 'scale.data'
  204. #' @param assay Assay to pull from
  205. # @param check.plot Check that plotting will finish in a reasonable amount of time
  206. #' @param label Label the cell identies above the color bar
  207. #' @param size Size of text above color bar
  208. #' @param hjust Horizontal justification of text above color bar
  209. #' @param vjust Vertical justification of text above color bar
  210. #' @param angle Angle of text above color bar
  211. #' @param raster If true, plot with geom_raster, else use geom_tile. geom_raster may look blurry on
  212. #' some viewing applications such as Preview due to how the raster is interpolated. Set this to FALSE
  213. #' if you are encountering that issue (note that plots may take longer to produce/render).
  214. #' @param draw.lines Include white lines to separate the groups
  215. #' @param lines.width Integer number to adjust the width of the separating white lines.
  216. #' Corresponds to the number of "cells" between each group.
  217. #' @param group.bar.height Scale the height of the color bar
  218. #' @param combine Combine plots into a single \code{\link[patchwork]{patchwork}ed}
  219. #' ggplot object. If \code{FALSE}, return a list of ggplot objects
  220. #'
  221. #' @return A \code{\link[patchwork]{patchwork}ed} ggplot object if
  222. #' \code{combine = TRUE}; otherwise, a list of ggplot objects
  223. #'
  224. #' @importFrom stats median
  225. #' @importFrom scales hue_pal
  226. #' @importFrom ggplot2 annotation_raster coord_cartesian scale_color_manual
  227. #' ggplot_build geom_text
  228. #' @importFrom patchwork wrap_plots
  229. #' @export
  230. #' @concept visualization
  231. #'
  232. #' @examples
  233. #' data("pbmc_small")
  234. #' DoHeatmap(object = pbmc_small)
  235. #'
  236. DoHeatmap <- function(
  237. object,
  238. features = NULL,
  239. cells = NULL,
  240. group.by = 'ident',
  241. group.bar = TRUE,
  242. group.colors = NULL,
  243. disp.min = -2.5,
  244. disp.max = NULL,
  245. slot = 'scale.data',
  246. assay = NULL,
  247. label = TRUE,
  248. size = 5.5,
  249. hjust = 0,
  250. vjust = 0,
  251. angle = 45,
  252. raster = TRUE,
  253. draw.lines = TRUE,
  254. lines.width = NULL,
  255. group.bar.height = 0.02,
  256. combine = TRUE
  257. ) {
  258. assay <- assay %||% DefaultAssay(object = object)
  259. DefaultAssay(object = object) <- assay
  260. cells <- cells %||% colnames(x = object[[assay]])
  261. if (is.numeric(x = cells)) {
  262. cells <- colnames(x = object)[cells]
  263. }
  264. features <- features %||% VariableFeatures(object = object)
  265. features <- rev(x = unique(x = features))
  266. disp.max <- disp.max %||% ifelse(
  267. test = slot == 'scale.data',
  268. yes = 2.5,
  269. no = 6
  270. )
  271. # make sure features are present
  272. possible.features <- Features(object,layer = slot)
  273. if (any(!features %in% possible.features)) {
  274. bad.features <- features[!features %in% possible.features]
  275. features <- features[features %in% possible.features]
  276. if(length(x = features) == 0) {
  277. stop("No requested features found in the ", slot, " layer for the ", assay, " assay.")
  278. }
  279. warning("The following features were omitted as they were not found in the ", slot,
  280. " layer for the ", assay, " assay: ", paste(bad.features, collapse = ", "))
  281. }
  282. data <- as.data.frame(x = as.matrix(x = t(x = GetAssayData(
  283. object = object,
  284. layer = slot)[features, cells, drop = FALSE])))
  285. object <- suppressMessages(expr = StashIdent(object = object, save.name = 'ident'))
  286. group.by <- group.by %||% 'ident'
  287. groups.use <- object[[group.by]][cells, , drop = FALSE]
  288. # group.use <- switch(
  289. # EXPR = group.by,
  290. # 'ident' = Idents(object = object),
  291. # object[[group.by, drop = TRUE]]
  292. # )
  293. # group.use <- factor(x = group.use[cells])
  294. plots <- vector(mode = 'list', length = ncol(x = groups.use))
  295. for (i in 1:ncol(x = groups.use)) {
  296. data.group <- data
  297. group.use <- groups.use[, i, drop = TRUE]
  298. if (!is.factor(x = group.use)) {
  299. group.use <- factor(x = group.use)
  300. }
  301. names(x = group.use) <- cells
  302. if (draw.lines) {
  303. # create fake cells to serve as the white lines, fill with NAs
  304. lines.width <- lines.width %||% ceiling(x = nrow(x = data.group) * 0.0025)
  305. placeholder.cells <- sapply(
  306. X = 1:(length(x = levels(x = group.use)) * lines.width),
  307. FUN = function(x) {
  308. return(RandomName(length = 20))
  309. }
  310. )
  311. placeholder.groups <- rep(x = levels(x = group.use), times = lines.width)
  312. group.levels <- levels(x = group.use)
  313. names(x = placeholder.groups) <- placeholder.cells
  314. group.use <- as.vector(x = group.use)
  315. names(x = group.use) <- cells
  316. group.use <- factor(x = c(group.use, placeholder.groups), levels = group.levels)
  317. na.data.group <- matrix(
  318. data = NA,
  319. nrow = length(x = placeholder.cells),
  320. ncol = ncol(x = data.group),
  321. dimnames = list(placeholder.cells, colnames(x = data.group))
  322. )
  323. data.group <- rbind(data.group, na.data.group)
  324. }
  325. lgroup <- length(levels(group.use))
  326. plot <- SingleRasterMap(
  327. data = data.group,
  328. raster = raster,
  329. disp.min = disp.min,
  330. disp.max = disp.max,
  331. feature.order = features,
  332. cell.order = names(x = sort(x = group.use)),
  333. group.by = group.use
  334. )
  335. if (group.bar) {
  336. # TODO: Change group.bar to annotation.bar
  337. default.colors <- c(hue_pal()(length(x = levels(x = group.use))))
  338. if (!is.null(x = names(x = group.colors))) {
  339. cols <- unname(obj = group.colors[levels(x = group.use)])
  340. } else {
  341. cols <- group.colors[1:length(x = levels(x = group.use))] %||% default.colors
  342. }
  343. if (any(is.na(x = cols))) {
  344. cols[is.na(x = cols)] <- default.colors[is.na(x = cols)]
  345. cols <- Col2Hex(cols)
  346. col.dups <- sort(x = unique(x = which(x = duplicated(x = substr(
  347. x = cols,
  348. start = 1,
  349. stop = 7
  350. )))))
  351. through <- length(x = default.colors)
  352. while (length(x = col.dups) > 0) {
  353. pal.max <- length(x = col.dups) + through
  354. cols.extra <- hue_pal()(pal.max)[(through + 1):pal.max]
  355. cols[col.dups] <- cols.extra
  356. col.dups <- sort(x = unique(x = which(x = duplicated(x = substr(
  357. x = cols,
  358. start = 1,
  359. stop = 7
  360. )))))
  361. }
  362. }
  363. group.use2 <- sort(x = group.use)
  364. if (draw.lines) {
  365. na.group <- RandomName(length = 20)
  366. levels(x = group.use2) <- c(levels(x = group.use2), na.group)
  367. group.use2[placeholder.cells] <- na.group
  368. cols <- c(cols, "#FFFFFF")
  369. }
  370. pbuild <- ggplot_build(plot = plot)
  371. names(x = cols) <- levels(x = group.use2)
  372. # scale the height of the bar
  373. y.range <- diff(x = pbuild$layout$panel_params[[1]]$y.range)
  374. y.pos <- max(pbuild$layout$panel_params[[1]]$y.range) + y.range * 0.015
  375. y.max <- y.pos + group.bar.height * y.range
  376. x.min <- min(pbuild$layout$panel_params[[1]]$x.range) + 0.1
  377. x.max <- max(pbuild$layout$panel_params[[1]]$x.range) - 0.1
  378. plot <- plot +
  379. annotation_raster(
  380. raster = t(x = cols[group.use2]),
  381. xmin = x.min,
  382. xmax = x.max,
  383. ymin = y.pos,
  384. ymax = y.max
  385. ) +
  386. coord_cartesian(ylim = c(0, y.max), clip = 'off') +
  387. scale_color_manual(
  388. values = cols[-length(x = cols)],
  389. name = "Identity",
  390. na.translate = FALSE
  391. )
  392. if (label) {
  393. x.max <- max(pbuild$layout$panel_params[[1]]$x.range)
  394. # Attempt to pull xdivs from x.major in ggplot2 < 3.3.0; if NULL, pull from the >= 3.3.0 slot
  395. x.divs <- pbuild$layout$panel_params[[1]]$x.major %||% attr(x = pbuild$layout$panel_params[[1]]$x$get_breaks(), which = "pos")
  396. x <- data.frame(group = sort(x = group.use), x = x.divs)
  397. label.x.pos <- tapply(X = x$x, INDEX = x$group, FUN = function(y) {
  398. if (isTRUE(x = draw.lines)) {
  399. mean(x = y[-length(x = y)])
  400. } else {
  401. mean(x = y)
  402. }
  403. })
  404. label.x.pos <- data.frame(group = names(x = label.x.pos), label.x.pos)
  405. plot <- plot + geom_text(
  406. stat = "identity",
  407. data = label.x.pos,
  408. aes(label = .data[['group']], x = .data[['label.x.pos']]),
  409. y = y.max + y.max * 0.03 * 0.5 + vjust,
  410. angle = angle,
  411. hjust = hjust,
  412. size = size
  413. )
  414. plot <- suppressMessages(plot + coord_cartesian(
  415. ylim = c(0, y.max + y.max * 0.002 * max(nchar(x = levels(x = group.use))) * size),
  416. clip = 'off')
  417. )
  418. }
  419. }
  420. plot <- plot + theme(line = element_blank())
  421. plots[[i]] <- plot
  422. }
  423. if (combine) {
  424. plots <- wrap_plots(plots)
  425. }
  426. return(plots)
  427. }
  428. #' Hashtag oligo heatmap
  429. #'
  430. #' Draws a heatmap of hashtag oligo signals across singlets/doublets/negative cells. Allows for the visualization of HTO demultiplexing results.
  431. #'
  432. #' @param object Seurat object. Assumes that the hash tag oligo (HTO) data has been added and normalized, and demultiplexing has been run with HTODemux().
  433. #' @param classification The naming for metadata column with classification result from HTODemux().
  434. #' @param global.classification The slot for metadata column specifying a cell as singlet/doublet/negative.
  435. #' @param assay Hashtag assay name.
  436. #' @param ncells Number of cells to plot. Default is to choose 5000 cells by random subsampling, to avoid having to draw exceptionally large heatmaps.
  437. #' @param singlet.names Namings for the singlets. Default is to use the same names as HTOs.
  438. #' @param raster If true, plot with geom_raster, else use geom_tile. geom_raster may look blurry on
  439. #' some viewing applications such as Preview due to how the raster is interpolated. Set this to FALSE
  440. #' if you are encountering that issue (note that plots may take longer to produce/render).
  441. #' @return Returns a ggplot2 plot object.
  442. #'
  443. #' @importFrom ggplot2 guides
  444. #' @export
  445. #' @concept visualization
  446. #'
  447. #' @seealso \code{\link{HTODemux}}
  448. #'
  449. #' @examples
  450. #' \dontrun{
  451. #' object <- HTODemux(object)
  452. #' HTOHeatmap(object)
  453. #' }
  454. #'
  455. HTOHeatmap <- function(
  456. object,
  457. assay = 'HTO',
  458. classification = paste0(assay, '_classification'),
  459. global.classification = paste0(assay, '_classification.global'),
  460. ncells = 5000,
  461. singlet.names = NULL,
  462. raster = TRUE
  463. ) {
  464. DefaultAssay(object = object) <- assay
  465. Idents(object = object) <- object[[classification, drop = TRUE]]
  466. if (ncells > ncol(x = object)) {
  467. warning("ncells (", ncells, ") is larger than the number of cells present in the provided object (", ncol(x = object), "). Plotting heatmap for all cells.")
  468. } else {
  469. object <- subset(
  470. x = object,
  471. cells = sample(x = colnames(x = object), size = ncells)
  472. )
  473. }
  474. classification <- object[[classification]]
  475. singlets <- which(x = object[[global.classification]] == 'Singlet')
  476. singlet.ids <- sort(x = unique(x = as.character(x = classification[singlets, ])))
  477. doublets <- which(object[[global.classification]] == 'Doublet')
  478. doublet.ids <- sort(x = unique(x = as.character(x = classification[doublets, ])))
  479. heatmap.levels <- c(singlet.ids, doublet.ids, 'Negative')
  480. object <- ScaleData(object = object, assay = assay, verbose = FALSE)
  481. data <- FetchData(object = object, vars = singlet.ids)
  482. Idents(object = object) <- factor(x = classification[, 1], levels = heatmap.levels)
  483. plot <- SingleRasterMap(
  484. data = data,
  485. raster = raster,
  486. feature.order = rev(x = singlet.ids),
  487. cell.order = names(x = sort(x = Idents(object = object))),
  488. group.by = Idents(object = object)
  489. ) + guides(color = "none")
  490. return(plot)
  491. }
  492. #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  493. # Expression by identity plots
  494. #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  495. #' Single cell ridge plot
  496. #'
  497. #' Draws a ridge plot of single cell data (gene expression, metrics, PC
  498. #' scores, etc.)
  499. #'
  500. #' @param object Seurat object
  501. #' @param features Features to plot (gene expression, metrics, PC scores,
  502. #' anything that can be retreived by FetchData)
  503. #' @param cols Colors to use for plotting
  504. #' @param idents Which classes to include in the plot (default is all)
  505. #' @param sort Sort identity classes (on the x-axis) by the average
  506. #' expression of the attribute being potted, can also pass 'increasing' or 'decreasing' to change sort direction
  507. #' @param assay Name of assay to use, defaults to the active assay
  508. #' @param group.by Group (color) cells in different ways (for example, orig.ident)
  509. #' @param y.max Maximum y axis value
  510. #' @param same.y.lims Set all the y-axis limits to the same values
  511. #' @param log plot the feature axis on log scale
  512. #' @param ncol Number of columns if multiple plots are displayed
  513. #' @param slot Slot to pull expression data from (e.g. "counts" or "data")
  514. #' @param layer Layer to pull expression data from (e.g. "counts" or "data")
  515. #' @param stack Horizontally stack plots for each feature
  516. #' @param combine Combine plots into a single \code{\link[patchwork]{patchwork}ed}
  517. #' ggplot object. If \code{FALSE}, return a list of ggplot
  518. #' @param fill.by Color violins/ridges based on either 'feature' or 'ident'
  519. #'
  520. #' @return A \code{\link[patchwork]{patchwork}ed} ggplot object if
  521. #' \code{combine = TRUE}; otherwise, a list of ggplot objects
  522. #'
  523. #' @export
  524. #' @concept visualization
  525. #'
  526. #' @examples
  527. #' data("pbmc_small")
  528. #' RidgePlot(object = pbmc_small, features = 'PC_1')
  529. #'
  530. RidgePlot <- function(
  531. object,
  532. features,
  533. cols = NULL,
  534. idents = NULL,
  535. sort = FALSE,
  536. assay = NULL,
  537. group.by = NULL,
  538. y.max = NULL,
  539. same.y.lims = FALSE,
  540. log = FALSE,
  541. ncol = NULL,
  542. slot = deprecated(),
  543. layer = 'data',
  544. stack = FALSE,
  545. combine = TRUE,
  546. fill.by = NULL
  547. ) {
  548. if (is_present(arg = slot)) {
  549. deprecate_soft(
  550. when = '5.0.0',
  551. what = 'RidgePlot(slot = )',
  552. with = 'RidgePlot(layer = )'
  553. )
  554. layer <- slot %||% layer
  555. }
  556. fill.by <- fill.by %||% if (stack) 'feature' else 'ident'
  557. return(ExIPlot(
  558. object = object,
  559. type = 'ridge',
  560. features = features,
  561. idents = idents,
  562. ncol = ncol,
  563. sort = sort,
  564. assay = assay,
  565. y.max = y.max,
  566. same.y.lims = same.y.lims,
  567. cols = cols,
  568. group.by = group.by,
  569. log = log,
  570. layer = layer,
  571. stack = stack,
  572. combine = combine,
  573. fill.by = fill.by
  574. ))
  575. }
  576. #' Single cell violin plot
  577. #'
  578. #' Draws a violin plot of single cell data (gene expression, metrics, PC
  579. #' scores, etc.)
  580. #'
  581. #' @inheritParams RidgePlot
  582. #' @param pt.size Point size for points
  583. #' @param alpha Alpha value for points
  584. #' @param split.by A factor in object metadata to split the plot by, pass 'ident'
  585. #' to split by cell identity
  586. #' @param split.plot plot each group of the split violin plots by multiple or
  587. #' single violin shapes.
  588. #' @param adjust Adjust parameter for geom_violin
  589. #' @param flip flip plot orientation (identities on x-axis)
  590. #' @param add.noise determine if adding a small noise for plotting
  591. #' @param raster Convert points to raster format. Requires 'ggrastr' to be installed.
  592. # default is \code{NULL} which automatically rasterizes if ggrastr is installed and
  593. # number of points exceed 100,000.
  594. #' @param raster.dpi the dpi for raster layer, default is 300.
  595. #' See \code{\link[ggrastr]{rasterize}} for more info.
  596. #'
  597. #' @return A \code{\link[patchwork]{patchwork}ed} ggplot object if
  598. #' \code{combine = TRUE}; otherwise, a list of ggplot objects
  599. #'
  600. #' @export
  601. #' @concept visualization
  602. #'
  603. #' @seealso \code{\link{FetchData}}
  604. #'
  605. #' @examples
  606. #' data("pbmc_small")
  607. #' VlnPlot(object = pbmc_small, features = 'PC_1')
  608. #' VlnPlot(object = pbmc_small, features = 'LYZ', split.by = 'groups')
  609. #'
  610. VlnPlot <- function(
  611. object,
  612. features,
  613. cols = NULL,
  614. pt.size = NULL,
  615. alpha = 1,
  616. idents = NULL,
  617. sort = FALSE,
  618. assay = NULL,
  619. group.by = NULL,
  620. split.by = NULL,
  621. adjust = 1,
  622. y.max = NULL,
  623. same.y.lims = FALSE,
  624. log = FALSE,
  625. ncol = NULL,
  626. slot = deprecated(),
  627. layer = NULL,
  628. split.plot = FALSE,
  629. stack = FALSE,
  630. combine = TRUE,
  631. fill.by = 'feature',
  632. flip = FALSE,
  633. add.noise = TRUE,
  634. raster = NULL,
  635. raster.dpi = 300
  636. ) {
  637. if (is_present(arg = slot)) {
  638. deprecate_soft(
  639. when = '5.0.0',
  640. what = 'VlnPlot(slot = )',
  641. with = 'VlnPlot(layer = )'
  642. )
  643. layer <- slot %||% layer
  644. }
  645. layer.set <- suppressWarnings(
  646. Layers(
  647. object = object,
  648. search = layer %||% 'data'
  649. )
  650. )
  651. if (is.null(layer) && length(layer.set) == 1 && layer.set == 'scale.data'){
  652. warning('Default search for "data" layer yielded no results; utilizing "scale.data" layer instead.')
  653. }
  654. assay.name <- assay %||% DefaultAssay(object = object)
  655. if (is.null(layer.set) & is.null(layer) ) {
  656. warning('Default search for "data" layer in "', assay.name, '" assay yielded no results; utilizing "counts" layer instead.',
  657. call. = FALSE, immediate. = TRUE)
  658. layer.set <- Layers(
  659. object = object,
  660. search = 'counts'
  661. )
  662. }
  663. if (is.null(layer.set)) {
  664. stop('layer "', layer,'" is not found in assay: "', assay.name, '"')
  665. } else {
  666. layer <- layer.set
  667. }
  668. if (
  669. !is.null(x = split.by) &
  670. getOption(x = 'Seurat.warn.vlnplot.split', default = TRUE)
  671. ) {
  672. message(
  673. "The default behaviour of split.by has changed.\n",
  674. "Separate violin plots are now plotted side-by-side.\n",
  675. "To restore the old behaviour of a single split violin,\n",
  676. "set split.plot = TRUE.
  677. \nThis message will be shown once per session."
  678. )
  679. options(Seurat.warn.vlnplot.split = FALSE)
  680. }
  681. return(ExIPlot(
  682. object = object,
  683. type = ifelse(test = split.plot, yes = 'splitViolin', no = 'violin'),
  684. features = features,
  685. idents = idents,
  686. ncol = ncol,
  687. sort = sort,
  688. assay = assay,
  689. y.max = y.max,
  690. same.y.lims = same.y.lims,
  691. adjust = adjust,
  692. pt.size = pt.size,
  693. alpha = alpha,
  694. cols = cols,
  695. group.by = group.by,
  696. split.by = split.by,
  697. log = log,
  698. layer = layer,
  699. stack = stack,
  700. combine = combine,
  701. fill.by = fill.by,
  702. flip = flip,
  703. add.noise = add.noise,
  704. raster = raster,
  705. raster.dpi = raster.dpi
  706. ))
  707. }
  708. #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  709. # Dimensional reduction plots
  710. #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  711. #' Color dimensional reduction plot by tree split
  712. #'
  713. #' Returns a DimPlot colored based on whether the cells fall in clusters
  714. #' to the left or to the right of a node split in the cluster tree.
  715. #'
  716. #' @param object Seurat object
  717. #' @param node Node in cluster tree on which to base the split
  718. #' @param left.color Color for the left side of the split
  719. #' @param right.color Color for the right side of the split
  720. #' @param other.color Color for all other cells
  721. #' @inheritDotParams DimPlot -object
  722. #'
  723. #' @return Returns a DimPlot
  724. #'
  725. #' @export
  726. #' @concept visualization
  727. #'
  728. #' @seealso \code{\link{DimPlot}}
  729. #'
  730. #' @examples
  731. #' \dontrun{
  732. #' if (requireNamespace("ape", quietly = TRUE)) {
  733. #' data("pbmc_small")
  734. #' pbmc_small <- BuildClusterTree(object = pbmc_small, verbose = FALSE)
  735. #' PlotClusterTree(pbmc_small)
  736. #' ColorDimSplit(pbmc_small, node = 5)
  737. #' }
  738. #' }
  739. #'
  740. ColorDimSplit <- function(
  741. object,
  742. node,
  743. left.color = 'red',
  744. right.color = 'blue',
  745. other.color = 'grey50',
  746. ...
  747. ) {
  748. CheckDots(..., fxns = 'DimPlot')
  749. tree <- Tool(object = object, slot = "BuildClusterTree")
  750. split <- tree$edge[which(x = tree$edge[, 1] == node), ][, 2]
  751. all.children <- sort(x = tree$edge[, 2][!tree$edge[, 2] %in% tree$edge[, 1]])
  752. left.group <- DFT(tree = tree, node = split[1], only.children = TRUE)
  753. right.group <- DFT(tree = tree, node = split[2], only.children = TRUE)
  754. if (any(is.na(x = left.group))) {
  755. left.group <- split[1]
  756. }
  757. if (any(is.na(x = right.group))) {
  758. right.group <- split[2]
  759. }
  760. left.group <- MapVals(v = left.group, from = all.children, to = tree$tip.label)
  761. right.group <- MapVals(v = right.group, from = all.children, to = tree$tip.label)
  762. remaining.group <- setdiff(x = tree$tip.label, y = c(left.group, right.group))
  763. left.cells <- WhichCells(object = object, ident = left.group)
  764. right.cells <- WhichCells(object = object, ident = right.group)
  765. remaining.cells <- WhichCells(object = object, ident = remaining.group)
  766. object <- SetIdent(
  767. object = object,
  768. cells = left.cells,
  769. value = "Left Split"
  770. )
  771. object <- SetIdent(
  772. object = object,
  773. cells = right.cells,
  774. value = "Right Split"
  775. )
  776. object <- SetIdent(
  777. object = object,
  778. cells = remaining.cells,
  779. value = "Not in Split"
  780. )
  781. levels(x = object) <- c("Left Split", "Right Split", "Not in Split")
  782. colors.use = c(left.color, right.color, other.color)
  783. return(DimPlot(object = object, cols = colors.use, ...))
  784. }
  785. #' Dimensional reduction plot
  786. #'
  787. #' Graphs the output of a dimensional reduction technique on a 2D scatter plot where each point is a
  788. #' cell and it's positioned based on the cell embeddings determined by the reduction technique. By
  789. #' default, cells are colored by their identity class (can be changed with the group.by parameter).
  790. #'
  791. #' @param object Seurat object
  792. #' @param dims Dimensions to plot, must be a two-length numeric vector specifying x- and y-dimensions
  793. #' @param cells Vector of cells to plot (default is all cells)
  794. #' @param cols Vector of colors, each color corresponds to an identity class. This may also be a single character
  795. #' or numeric value corresponding to a palette as specified by \code{\link[RColorBrewer]{brewer.pal.info}}.
  796. #' By default, ggplot2 assigns colors. We also include a number of palettes from the pals package.
  797. #' See \code{\link{DiscretePalette}} for details.
  798. #' @param pt.size Adjust point size for plotting
  799. #' @param reduction Which dimensionality reduction to use. If not specified, first searches for umap, then tsne, then pca
  800. #' @param group.by Name of one or more metadata columns to group (color) cells by
  801. #' (for example, orig.ident); pass 'ident' to group by identity class
  802. #' @param split.by A factor in object metadata to split the plot by, pass 'ident'
  803. #' to split by cell identity
  804. #' @param shape.by If NULL, all points are circles (default). You can specify any
  805. #' cell attribute (that can be pulled with FetchData) allowing for both
  806. #' different colors and different shapes on cells. Only applicable if \code{raster = FALSE}.
  807. #' @param order Specify the order of plotting for the idents. This can be
  808. #' useful for crowded plots if points of interest are being buried. Provide
  809. #' either a full list of valid idents or a subset to be plotted last (on top)
  810. #' @param shuffle Whether to randomly shuffle the order of points. This can be
  811. #' useful for crowded plots if points of interest are being buried. (default is FALSE)
  812. #' @param seed Sets the seed if randomly shuffling the order of points.
  813. #' @param label Whether to label the clusters
  814. #' @param label.size Sets size of labels
  815. #' @param label.color Sets the color of the label text
  816. #' @param label.box Whether to put a box around the label text (geom_text vs
  817. #' geom_label)
  818. #' @param alpha Alpha value for plotting (default is 1)
  819. #' @param repel Repel labels
  820. #' @param stroke.size Adjust stroke (outline) size of points
  821. #' @param cells.highlight A list of character or numeric vectors of cells to
  822. #' highlight. If only one group of cells desired, can simply
  823. #' pass a vector instead of a list. If set, colors selected cells to the color(s)
  824. #' in \code{cols.highlight} and other cells black (white if dark.theme = TRUE);
  825. #' will also resize to the size(s) passed to \code{sizes.highlight}
  826. #' @param cols.highlight A vector of colors to highlight the cells as; will
  827. #' repeat to the length groups in cells.highlight
  828. #' @param sizes.highlight Size of highlighted cells; will repeat to the length
  829. #' groups in cells.highlight. If \code{sizes.highlight = TRUE} size of all
  830. #' points will be this value.
  831. #' @param na.value Color value for NA points when using custom scale
  832. #' @param ncol Number of columns for display when combining plots
  833. #' @param combine Combine plots into a single \code{\link[patchwork]{patchwork}ed}
  834. #' ggplot object. If \code{FALSE}, return a list of ggplot objects
  835. #' @param raster Convert points to raster format, default is \code{NULL} which
  836. #' automatically rasterizes if plotting more than 100,000 cells
  837. #' @param raster.dpi Pixel resolution for rasterized plots, passed to geom_scattermore(). Default is c(512, 512).
  838. #' @param label.size.cutoff Clusters with fewer cells than the cutoff are not labeled (replaced with ' ' label)
  839. #'
  840. #' @return A \code{\link[patchwork]{patchwork}ed} ggplot object if
  841. #' \code{combine = TRUE}; otherwise, a list of ggplot objects
  842. #'
  843. #' @importFrom rlang !!
  844. #' @importFrom ggplot2 facet_wrap vars sym labs
  845. #' @importFrom patchwork wrap_plots
  846. #'
  847. #' @export
  848. #' @concept visualization
  849. #'
  850. #' @note For the old \code{do.hover} and \code{do.identify} functionality, please see
  851. #' \code{HoverLocator} and \code{CellSelector}, respectively.
  852. #'
  853. #' @aliases TSNEPlot PCAPlot ICAPlot
  854. #' @seealso \code{\link{FeaturePlot}} \code{\link{HoverLocator}}
  855. #' \code{\link{CellSelector}} \code{\link{FetchData}}
  856. #'
  857. #' @examples
  858. #' data("pbmc_small")
  859. #' DimPlot(object = pbmc_small)
  860. #' DimPlot(object = pbmc_small, split.by = 'letter.idents')
  861. #'
  862. DimPlot <- function(
  863. object,
  864. dims = c(1, 2),
  865. cells = NULL,
  866. cols = NULL,
  867. pt.size = NULL,
  868. reduction = NULL,
  869. group.by = NULL,
  870. split.by = NULL,
  871. shape.by = NULL,
  872. order = NULL,
  873. shuffle = FALSE,
  874. seed = 1,
  875. label = FALSE,
  876. label.size = 4,
  877. label.color = 'black',
  878. label.box = FALSE,
  879. repel = FALSE,
  880. alpha = 1,
  881. stroke.size = NULL,
  882. cells.highlight = NULL,
  883. cols.highlight = '#DE2D26',
  884. sizes.highlight = 1,
  885. na.value = 'grey50',
  886. ncol = NULL,
  887. combine = TRUE,
  888. raster = NULL,
  889. raster.dpi = c(512, 512),
  890. label.size.cutoff = 0
  891. ) {
  892. if (!is_integerish(x = dims, n = 2L, finite = TRUE) || !all(dims > 0L)) {
  893. abort(message = "'dims' must be a two-length integer vector")
  894. }
  895. reduction <- reduction %||% DefaultDimReduc(object = object)
  896. # cells <- cells %||% colnames(x = object)
  897. ##### Cells for all cells in the assay.
  898. #### Cells function should not only get default layer
  899. cells <- cells %||% Cells(
  900. x = object,
  901. assay = DefaultAssay(object = object[[reduction]])
  902. )
  903. # data <- Embeddings(object = object[[reduction]])[cells, dims]
  904. # data <- as.data.frame(x = data)
  905. dims <- paste0(Key(object = object[[reduction]]), dims)
  906. orig.groups <- group.by
  907. group.by <- group.by %||% 'ident'
  908. if (label & (label.size.cutoff > 0)) {
  909. labels <- FetchData(object, group.by)
  910. for(i in seq_along(group.by)) {
  911. grouping_var <- group.by[i]
  912. label_table <- table(labels[,grouping_var])
  913. invalid_labels <- names(which(label_table < label.size.cutoff))
  914. labels[, i] <- as.character(labels[,i])
  915. labels[which(labels[,grouping_var] %in% invalid_labels),grouping_var] <- ' '
  916. colnames(labels)[i] <- paste0(colnames(labels)[i], "_filtered")
  917. }
  918. object <- AddMetaData(object,labels)
  919. group.by <- colnames(labels)
  920. }
  921. # check for overlap between colnames of dim reduc embeddings and metadata
  922. metadata_cols <- names(object[[]])
  923. colname_overlap <- intersect(dims, metadata_cols)
  924. if (length(colname_overlap) > 0) {
  925. warning("Found metadata columns with the same names as requested reduction columns: ",
  926. paste(colname_overlap, collapse = ", "),
  927. ". Consider renaming these metadata column(s) to avoid conflicts with dimensionality reduction embeddings.",
  928. call. = FALSE)
  929. }
  930. data <- FetchData(
  931. object = object,
  932. vars = c(dims, group.by),
  933. cells = cells,
  934. clean = 'project'
  935. )
  936. # cells <- rownames(x = object)
  937. # object[['ident']] <- Idents(object = object)
  938. # orig.groups <- group.by
  939. # group.by <- group.by %||% 'ident'
  940. # data <- cbind(data, object[[group.by]][cells, , drop = FALSE])
  941. group.by <- colnames(x = data)[3:ncol(x = data)]
  942. for (group in group.by) {
  943. if (!is.factor(x = data[, group])) {
  944. data[, group] <- factor(x = data[, group])
  945. }
  946. }
  947. if (!is.null(x = shape.by)) {
  948. data[, shape.by] <- object[[shape.by, drop = TRUE]]
  949. }
  950. if (!is.null(x = split.by)) {
  951. split <- FetchData(object = object, vars = split.by, clean=TRUE)[split.by]
  952. data <- data[rownames(split),]
  953. data[, split.by] <- split
  954. }
  955. if (isTRUE(x = shuffle)) {
  956. set.seed(seed = seed)
  957. data <- data[sample(x = 1:nrow(x = data)), ]
  958. }
  959. plots <- lapply(
  960. X = group.by,
  961. FUN = function(x) {
  962. plot <- SingleDimPlot(
  963. data = data[, c(dims, x, split.by, shape.by)],
  964. dims = dims,
  965. col.by = x,
  966. cols = cols,
  967. pt.size = pt.size,
  968. shape.by = shape.by,
  969. order = order,
  970. alpha = alpha,
  971. stroke.size = stroke.size,
  972. label = FALSE,
  973. cells.highlight = cells.highlight,
  974. cols.highlight = cols.highlight,
  975. sizes.highlight = sizes.highlight,
  976. na.value = na.value,
  977. raster = raster,
  978. raster.dpi = raster.dpi
  979. )
  980. if (label) {
  981. plot <- LabelClusters(
  982. plot = plot,
  983. id = x,
  984. repel = repel,
  985. size = label.size,
  986. split.by = split.by,
  987. box = label.box,
  988. color = label.color
  989. )
  990. }
  991. if (!is.null(x = split.by)) {
  992. plot <- plot + FacetTheme() +
  993. facet_wrap(
  994. facets = vars(!!sym(x = split.by)),
  995. ncol = if (length(x = group.by) > 1 || is.null(x = ncol)) {
  996. length(x = unique(x = data[, split.by]))
  997. } else {
  998. ncol
  999. }
  1000. )
  1001. }
  1002. plot <- if (is.null(x = orig.groups)) {
  1003. plot + labs(title = NULL)
  1004. } else {
  1005. plot + CenterTitle()
  1006. }
  1007. }
  1008. )
  1009. if (!is.null(x = split.by)) {
  1010. ncol <- 1
  1011. }
  1012. if (combine) {
  1013. plots <- wrap_plots(plots, ncol = orig.groups %iff% ncol)
  1014. }
  1015. return(plots)
  1016. }
  1017. #' Visualize 'features' on a dimensional reduction plot
  1018. #'
  1019. #' Colors single cells on a dimensional reduction plot according to a 'feature'
  1020. #' (i.e. gene expression, PC scores, number of genes detected, etc.)
  1021. #'
  1022. #' @inheritParams DimPlot
  1023. #' @param order Boolean determining whether to plot cells in order of expression. Can be useful if
  1024. #' cells expressing given feature are getting buried.
  1025. #' @param features Vector of features to plot. Features can come from:
  1026. #' \itemize{
  1027. #' \item An \code{Assay} feature (e.g. a gene name - "MS4A1")
  1028. #' \item A column name from meta.data (e.g. mitochondrial percentage -
  1029. #' "percent.mito")
  1030. #' \item A column name from a \code{DimReduc} object corresponding to the
  1031. #' cell embedding values (e.g. the PC 1 scores - "PC_1")
  1032. #' }
  1033. #' @param cols The two colors to form the gradient over. Provide as string vector with
  1034. #' the first color corresponding to low values, the second to high. Also accepts a Brewer
  1035. #' color scale or vector of colors. Note: this will bin the data into number of colors provided.
  1036. #' When blend is \code{TRUE}, takes anywhere from 1-3 colors:
  1037. #' \describe{
  1038. #' \item{1 color:}{Treated as color for double-negatives, will use default colors 2 and 3 for per-feature expression}
  1039. #' \item{2 colors:}{Treated as colors for per-feature expression, will use default color 1 for double-negatives}
  1040. #' \item{3+ colors:}{First color used for double-negatives, colors 2 and 3 used for per-feature expression, all others ignored}
  1041. #' }
  1042. #' @param min.cutoff,max.cutoff Vector of minimum and maximum cutoff values for each feature,
  1043. #' may specify quantile in the form of 'q##' where '##' is the quantile (eg, 'q1', 'q10')
  1044. #' @param stroke.size Adjust stroke (outline) size of points
  1045. #' @param split.by A factor in object metadata to split the plot by, pass 'ident'
  1046. #' to split by cell identity
  1047. #' @param keep.scale How to handle the color scale across multiple plots. Options are:
  1048. #' \itemize{
  1049. #' \item \dQuote{feature} (default; by row/feature scaling): The plots for
  1050. #' each individual feature are scaled to the maximum expression of the
  1051. #' feature across the conditions provided to \code{split.by}
  1052. #' \item \dQuote{all} (universal scaling): The plots for all features and
  1053. #' conditions are scaled to the maximum expression value for the feature
  1054. #' with the highest overall expression
  1055. #' \item \code{NULL} (no scaling): Each individual plot is scaled to the
  1056. #' maximum expression value of the feature in the condition provided to
  1057. #' \code{split.by}. Be aware setting \code{NULL} will result in color
  1058. #' scales that are not comparable between plots
  1059. #' }
  1060. #' @param slot Which slot to pull expression data from?
  1061. #' @param assay Primary assay to pull feature data from
  1062. #' @param blend Scale and blend expression values to visualize coexpression of two features
  1063. #' @param blend.threshold The color cutoff from weak signal to strong signal; ranges from 0 to 1.
  1064. #' @param ncol Number of columns to combine multiple feature plots to, ignored if \code{split.by} is not \code{NULL}
  1065. #' @param coord.fixed Plot cartesian coordinates with fixed aspect ratio
  1066. #' @param by.col If splitting by a factor, plot the splits per column with the features as rows; ignored if \code{blend = TRUE}
  1067. #' @param sort.cell Redundant with \code{order}. This argument is being
  1068. #' deprecated. Please use \code{order} instead.
  1069. #' @param interactive Launch an interactive \code{\link[Seurat:IFeaturePlot]{FeaturePlot}}
  1070. #' @param combine Combine plots into a single \code{\link[patchwork]{patchwork}ed}
  1071. #' ggplot object. If \code{FALSE}, return a list of ggplot objects
  1072. #'
  1073. #' @return A \code{\link[patchwork]{patchwork}ed} ggplot object if
  1074. #' \code{combine = TRUE}; otherwise, a list of ggplot objects
  1075. #'
  1076. #' @importFrom grDevices rgb
  1077. #' @importFrom patchwork wrap_plots
  1078. #' @importFrom cowplot theme_cowplot
  1079. #' @importFrom RColorBrewer brewer.pal.info
  1080. #' @importFrom ggplot2 labs scale_x_continuous scale_y_continuous theme element_rect
  1081. #' dup_axis guides element_blank element_text margin scale_color_brewer scale_color_gradientn
  1082. #' scale_color_manual coord_fixed ggtitle
  1083. #'
  1084. #' @export
  1085. #' @concept visualization
  1086. #'
  1087. #' @note For the old \code{do.hover} and \code{do.identify} functionality, please see
  1088. #' \code{HoverLocator} and \code{CellSelector}, respectively.
  1089. #'
  1090. #' @aliases FeatureHeatmap
  1091. #' @seealso \code{\link{DimPlot}} \code{\link{HoverLocator}}
  1092. #' \code{\link{CellSelector}}
  1093. #'
  1094. #' @examples
  1095. #' data("pbmc_small")
  1096. #' FeaturePlot(object = pbmc_small, features = 'PC_1')
  1097. #'
  1098. FeaturePlot <- function(
  1099. object,
  1100. features,
  1101. dims = c(1, 2),
  1102. cells = NULL,
  1103. cols = if (blend) {
  1104. c('lightgrey', '#ff0000', '#00ff00')
  1105. } else {
  1106. c('lightgrey', 'blue')
  1107. },
  1108. pt.size = NULL,
  1109. alpha = 1,
  1110. stroke.size = NULL,
  1111. order = FALSE,
  1112. min.cutoff = NA,
  1113. max.cutoff = NA,
  1114. reduction = NULL,
  1115. split.by = NULL,
  1116. keep.scale = "feature",
  1117. shape.by = NULL,
  1118. slot = 'data',
  1119. assay = NULL,
  1120. blend = FALSE,
  1121. blend.threshold = 0.5,
  1122. label = FALSE,
  1123. label.size = 4,
  1124. label.color = "black",
  1125. repel = FALSE,
  1126. ncol = NULL,
  1127. coord.fixed = FALSE,
  1128. by.col = TRUE,
  1129. sort.cell = deprecated(),
  1130. interactive = FALSE,
  1131. combine = TRUE,
  1132. raster = NULL,
  1133. raster.dpi = c(512, 512)
  1134. ) {
  1135. # TODO: deprecate fully on 3.2.0
  1136. if (is_present(arg = sort.cell)) {
  1137. deprecate_stop(
  1138. when = '4.9.0',
  1139. what = 'FeaturePlot(sort.cell = )',
  1140. with = 'FeaturePlot(order = )'
  1141. )
  1142. }
  1143. if (isTRUE(x = interactive)) {
  1144. return(IFeaturePlot(
  1145. object = object,
  1146. feature = features[1],
  1147. dims = dims,
  1148. reduction = reduction,
  1149. slot = slot
  1150. ))
  1151. }
  1152. # Check keep.scale param for valid entries
  1153. if (!is.null(x = keep.scale)) {
  1154. keep.scale <- arg_match0(arg = keep.scale, values = c('feature', 'all'))
  1155. }
  1156. # Set a theme to remove right-hand Y axis lines
  1157. # Also sets right-hand Y axis text label formatting
  1158. no.right <- theme(
  1159. axis.line.y.right = element_blank(),
  1160. axis.ticks.y.right = element_blank(),
  1161. axis.text.y.right = element_blank(),
  1162. axis.title.y.right = element_text(
  1163. face = "bold",
  1164. size = 14,
  1165. margin = margin(r = 7)
  1166. )
  1167. )
  1168. # Get the DimReduc to use
  1169. reduction <- reduction %||% DefaultDimReduc(object = object)
  1170. if (!is_integerish(x = dims, n = 2L, finite = TRUE) && !all(dims > 0L)) {
  1171. abort(message = "'dims' must be a two-length integer vector")
  1172. }
  1173. # Figure out blending stuff
  1174. if (isTRUE(x = blend) && length(x = features) != 2) {
  1175. abort(message = "Blending feature plots only works with two features")
  1176. }
  1177. # Set color scheme for blended FeaturePlots
  1178. if (isTRUE(x = blend)) {
  1179. default.colors <- eval(expr = formals(fun = FeaturePlot)$cols)
  1180. cols <- switch(
  1181. EXPR = as.character(x = length(x = cols)),
  1182. '0' = {
  1183. warn(message = "No colors provided, using default colors")
  1184. default.colors
  1185. },
  1186. '1' = {
  1187. warn(message = paste(
  1188. "Only one color provided, assuming",
  1189. sQuote(x = cols),
  1190. "is double-negative and augmenting with default colors"
  1191. ))
  1192. c(cols, default.colors[2:3])
  1193. },
  1194. '2' = {
  1195. warn(message = paste(
  1196. "Only two colors provided, assuming specified are for features and agumenting with",
  1197. sQuote(default.colors[1]),
  1198. "for double-negatives",
  1199. ))
  1200. c(default.colors[1], cols)
  1201. },
  1202. '3' = cols,
  1203. {
  1204. warn(message = "More than three colors provided, using only first three")
  1205. cols[1:3]
  1206. }
  1207. )
  1208. }
  1209. if (isTRUE(x = blend) && length(x = cols) != 3) {
  1210. abort("Blending feature plots only works with three colors; first one for negative cells")
  1211. }
  1212. # Name the reductions
  1213. dims <- paste0(Key(object = object[[reduction]]), dims)
  1214. cells <- cells %||% Cells(x = object[[reduction]])
  1215. # Get plotting data
  1216. data <- FetchData(
  1217. object = object,
  1218. vars = c(dims, 'ident', features),
  1219. cells = cells,
  1220. layer = slot,
  1221. assay = assay
  1222. )
  1223. # Check presence of features/dimensions
  1224. if (ncol(x = data) < 4) {
  1225. abort(message = paste(
  1226. "None of the requested features were found:",
  1227. paste(features, collapse = ', '),
  1228. "in slot ",
  1229. slot
  1230. ))
  1231. } else if (!all(dims %in% colnames(x = data))) {
  1232. abort(message = "The dimensions requested were not found")
  1233. }
  1234. features <- setdiff(x = names(x = data), y = c(dims, 'ident'))
  1235. # Determine cutoffs
  1236. min.cutoff <- mapply(
  1237. FUN = function(cutoff, feature) {
  1238. return(ifelse(
  1239. test = is.na(x = cutoff),
  1240. yes = min(data[, feature]),
  1241. no = cutoff
  1242. ))
  1243. },
  1244. cutoff = min.cutoff,
  1245. feature = features
  1246. )
  1247. max.cutoff <- mapply(
  1248. FUN = function(cutoff, feature) {
  1249. return(ifelse(
  1250. test = is.na(x = cutoff),
  1251. yes = max(data[, feature]),
  1252. no = cutoff
  1253. ))
  1254. },
  1255. cutoff = max.cutoff,
  1256. feature = features
  1257. )
  1258. check.lengths <- unique(x = vapply(
  1259. X = list(features, min.cutoff, max.cutoff),
  1260. FUN = length,
  1261. FUN.VALUE = numeric(length = 1)
  1262. ))
  1263. if (length(x = check.lengths) != 1) {
  1264. abort(
  1265. message = "There must be the same number of minimum and maximum cuttoffs as there are features"
  1266. )
  1267. }
  1268. names(x = min.cutoff) <- names(x = max.cutoff) <- features
  1269. brewer.gran <- ifelse(
  1270. test = length(x = cols) == 1,
  1271. yes = brewer.pal.info[cols, ]$maxcolors,
  1272. no = length(x = cols)
  1273. )
  1274. # Apply cutoffs
  1275. for (i in seq_along(along.with = features)) {
  1276. f <- features[i]
  1277. data.feature <- data[[f]]
  1278. min.use <- SetQuantile(cutoff = min.cutoff[f], data = data.feature)
  1279. max.use <- SetQuantile(cutoff = max.cutoff[f], data = data.feature)
  1280. data.feature[data.feature < min.use] <- min.use
  1281. data.feature[data.feature > max.use] <- max.use
  1282. if (brewer.gran != 2) {
  1283. data.feature <- if (all(data.feature == 0)) {
  1284. rep_len(x = 0, length.out = length(x = data.feature))
  1285. } else {
  1286. as.numeric(x = as.factor(x = cut(
  1287. x = as.numeric(x = data.feature),
  1288. breaks = 2
  1289. )))
  1290. }
  1291. }
  1292. data[[f]] <- data.feature
  1293. }
  1294. # Figure out splits (FeatureHeatmap)
  1295. data$split <- if (is.null(x = split.by)) {
  1296. RandomName()
  1297. } else {
  1298. switch(
  1299. EXPR = split.by,
  1300. ident = Idents(object = object)[cells, drop = TRUE],
  1301. object[[split.by, drop = TRUE]][cells, drop = TRUE]
  1302. )
  1303. }
  1304. if (!is.factor(x = data$split)) {
  1305. data$split <- factor(x = data$split)
  1306. }
  1307. # Set shaping variable
  1308. if (!is.null(x = shape.by)) {
  1309. data[, shape.by] <- object[[shape.by, drop = TRUE]]
  1310. }
  1311. # Make list of plots
  1312. plots <- vector(
  1313. mode = "list",
  1314. length = ifelse(
  1315. test = blend,
  1316. yes = 4,
  1317. no = length(x = features) * length(x = levels(x = data$split))
  1318. )
  1319. )
  1320. # Apply common limits
  1321. xlims <- c(floor(x = min(data[, dims[1]])), ceiling(x = max(data[, dims[1]])))
  1322. ylims <- c(floor(min(data[, dims[2]])), ceiling(x = max(data[, dims[2]])))
  1323. # Set blended colors
  1324. if (blend) {
  1325. ncol <- 4
  1326. color.matrix <- BlendMatrix(
  1327. two.colors = cols[2:3],
  1328. col.threshold = blend.threshold,
  1329. negative.color = cols[1]
  1330. )
  1331. cols <- cols[2:3]
  1332. colors <- list(
  1333. color.matrix[, 1],
  1334. color.matrix[1, ],
  1335. as.vector(x = color.matrix)
  1336. )
  1337. }
  1338. # Make the plots
  1339. for (i in 1:length(x = levels(x = data$split))) {
  1340. # Figure out which split we're working with
  1341. ident <- levels(x = data$split)[i]
  1342. data.plot <- data[as.character(x = data$split) == ident, , drop = FALSE]
  1343. # Blend expression values
  1344. if (isTRUE(x = blend)) {
  1345. features <- features[1:2]
  1346. no.expression <- features[colMeans(x = data.plot[, features]) == 0]
  1347. if (length(x = no.expression) != 0) {
  1348. abort(message = paste(
  1349. "The following features have no value:",
  1350. paste(no.expression, collapse = ', ')
  1351. ))
  1352. }
  1353. data.plot <- cbind(data.plot[, c(dims, 'ident')], BlendExpression(data = data.plot[, features[1:2]]))
  1354. features <- colnames(x = data.plot)[4:ncol(x = data.plot)]
  1355. }
  1356. # Make per-feature plots
  1357. for (j in 1:length(x = features)) {
  1358. feature <- features[j]
  1359. # Get blended colors
  1360. if (isTRUE(x = blend)) {
  1361. cols.use <- as.numeric(x = as.character(x = data.plot[, feature])) + 1
  1362. cols.use <- colors[[j]][sort(x = unique(x = cols.use))]
  1363. } else {
  1364. cols.use <- NULL
  1365. }
  1366. data.single <- data.plot[, c(dims, 'ident', feature, shape.by)]
  1367. # Make the plot
  1368. plot <- SingleDimPlot(
  1369. data = data.single,
  1370. dims = dims,
  1371. col.by = feature,
  1372. order = order,
  1373. pt.size = pt.size,
  1374. alpha = alpha,
  1375. stroke.size = stroke.size,
  1376. cols = cols.use,
  1377. shape.by = shape.by,
  1378. label = FALSE,
  1379. raster = raster,
  1380. raster.dpi = raster.dpi
  1381. ) +
  1382. scale_x_continuous(limits = xlims) +
  1383. scale_y_continuous(limits = ylims) +
  1384. theme_cowplot() +
  1385. CenterTitle()
  1386. # theme(plot.title = element_text(hjust = 0.5))
  1387. # Add labels
  1388. if (isTRUE(x = label)) {
  1389. plot <- LabelClusters(
  1390. plot = plot,
  1391. id = 'ident',
  1392. repel = repel,
  1393. size = label.size,
  1394. color = label.color
  1395. )
  1396. }
  1397. # Make FeatureHeatmaps look nice(ish)
  1398. if (length(x = levels(x = data$split)) > 1) {
  1399. plot <- plot + theme(panel.border = element_rect(fill = NA, colour = 'black'))
  1400. # Add title
  1401. plot <- plot + if (i == 1) {
  1402. labs(title = feature)
  1403. } else {
  1404. labs(title = NULL)
  1405. }
  1406. # Add second axis
  1407. if (j == length(x = features) && !blend) {
  1408. suppressMessages(
  1409. expr = plot <- plot +
  1410. scale_y_continuous(
  1411. sec.axis = dup_axis(name = ident),
  1412. limits = ylims
  1413. ) +
  1414. no.right
  1415. )
  1416. }
  1417. # Remove left Y axis
  1418. if (j != 1) {
  1419. plot <- plot + theme(
  1420. axis.line.y = element_blank(),
  1421. axis.ticks.y = element_blank(),
  1422. axis.text.y = element_blank(),
  1423. axis.title.y.left = element_blank()
  1424. )
  1425. }
  1426. # Remove bottom X axis
  1427. if (i != length(x = levels(x = data$split))) {
  1428. plot <- plot + theme(
  1429. axis.line.x = element_blank(),
  1430. axis.ticks.x = element_blank(),
  1431. axis.text.x = element_blank(),
  1432. axis.title.x = element_blank()
  1433. )
  1434. }
  1435. } else {
  1436. plot <- plot + labs(title = feature)
  1437. }
  1438. # Add colors scale for normal FeaturePlots
  1439. if (!blend) {
  1440. plot <- plot + guides(color = NULL)
  1441. cols.grad <- cols
  1442. if (length(x = cols) == 1) {
  1443. plot <- plot + scale_color_brewer(palette = cols)
  1444. } else if (length(x = cols) > 1) {
  1445. unique.feature.exp <- unique(data.plot[, feature])
  1446. if (length(unique.feature.exp) == 1) {
  1447. warn(message = paste0(
  1448. "All cells have the same value (",
  1449. unique.feature.exp,
  1450. ") of ",
  1451. dQuote(x = feature)
  1452. ))
  1453. if (unique.feature.exp == 0) {
  1454. cols.grad <- cols[1]
  1455. } else{
  1456. cols.grad <- cols
  1457. }
  1458. }
  1459. plot <- suppressMessages(
  1460. expr = plot + scale_color_gradientn(
  1461. colors = cols.grad,
  1462. guide = "colorbar"
  1463. )
  1464. )
  1465. }
  1466. }
  1467. if (!(is.null(x = keep.scale)) && keep.scale == "feature" && !blend) {
  1468. max.feature.value <- max(data[, feature])
  1469. min.feature.value <- min(data[, feature])
  1470. plot <- suppressMessages(plot & scale_color_gradientn(colors = cols, limits = c(min.feature.value, max.feature.value)))
  1471. }
  1472. # Add coord_fixed
  1473. if (coord.fixed) {
  1474. plot <- plot + coord_fixed()
  1475. }
  1476. # I'm not sure why, but sometimes the damn thing fails without this
  1477. # Thanks ggplot2
  1478. plot <- plot
  1479. # Place the plot
  1480. plots[[(length(x = features) * (i - 1)) + j]] <- plot
  1481. }
  1482. }
  1483. # Add blended color key
  1484. if (isTRUE(x = blend)) {
  1485. blend.legend <- BlendMap(color.matrix = color.matrix)
  1486. for (ii in 1:length(x = levels(x = data$split))) {
  1487. suppressMessages(expr = plots <- append(
  1488. x = plots,
  1489. values = list(
  1490. blend.legend +
  1491. scale_y_continuous(
  1492. sec.axis = dup_axis(name = ifelse(
  1493. test = length(x = levels(x = data$split)) > 1,
  1494. yes = levels(x = data$split)[ii],
  1495. no = ''
  1496. )),
  1497. expand = c(0, 0)
  1498. ) +
  1499. labs(
  1500. x = features[1],
  1501. y = features[2],
  1502. title = if (ii == 1) {
  1503. paste('Color threshold:', blend.threshold)
  1504. } else {
  1505. NULL
  1506. }
  1507. ) +
  1508. no.right
  1509. ),
  1510. after = 4 * ii - 1
  1511. ))
  1512. }
  1513. }
  1514. # Remove NULL plots
  1515. plots <- Filter(f = Negate(f = is.null), x = plots)
  1516. # Combine the plots
  1517. if (is.null(x = ncol)) {
  1518. ncol <- 2
  1519. if (length(x = features) == 1) {
  1520. ncol <- 1
  1521. }
  1522. if (length(x = features) > 6) {
  1523. ncol <- 3
  1524. }
  1525. if (length(x = features) > 9) {
  1526. ncol <- 4
  1527. }
  1528. }
  1529. ncol <- ifelse(
  1530. test = is.null(x = split.by) || isTRUE(x = blend),
  1531. yes = ncol,
  1532. no = length(x = features)
  1533. )
  1534. legend <- if (isTRUE(x = blend)) {
  1535. 'none'
  1536. } else {
  1537. split.by %iff% 'none'
  1538. }
  1539. # Transpose the FeatureHeatmap matrix (not applicable for blended FeaturePlots)
  1540. if (isTRUE(x = combine)) {
  1541. if (by.col && !is.null(x = split.by) && !blend) {
  1542. plots <- lapply(
  1543. X = plots,
  1544. FUN = function(x) {
  1545. return(suppressMessages(
  1546. expr = x +
  1547. theme_cowplot() +
  1548. ggtitle("") +
  1549. scale_y_continuous(sec.axis = dup_axis(name = ""), limits = ylims) +
  1550. no.right
  1551. ))
  1552. }
  1553. )
  1554. nsplits <- length(x = levels(x = data$split))
  1555. idx <- 1
  1556. for (i in (length(x = features) * (nsplits - 1) + 1):(length(x = features) * nsplits)) {
  1557. plots[[i]] <- suppressMessages(
  1558. expr = plots[[i]] +
  1559. scale_y_continuous(
  1560. sec.axis = dup_axis(name = features[[idx]]),
  1561. limits = ylims
  1562. ) +
  1563. no.right
  1564. )
  1565. idx <- idx + 1
  1566. }
  1567. idx <- 1
  1568. for (i in which(x = 1:length(x = plots) %% length(x = features) == 1)) {
  1569. plots[[i]] <- plots[[i]] +
  1570. ggtitle(levels(x = data$split)[[idx]]) +
  1571. theme(plot.title = element_text(hjust = 0.5))
  1572. idx <- idx + 1
  1573. }
  1574. idx <- 1
  1575. if (length(x = features) == 1) {
  1576. for (i in 1:length(x = plots)) {
  1577. plots[[i]] <- plots[[i]] +
  1578. ggtitle(levels(x = data$split)[[idx]]) +
  1579. theme(plot.title = element_text(hjust = 0.5))
  1580. idx <- idx + 1
  1581. }
  1582. ncol <- 1
  1583. nrow <- nsplits
  1584. } else {
  1585. nrow <- split.by %iff% length(x = levels(x = data$split))
  1586. }
  1587. plots <- plots[c(do.call(
  1588. what = rbind,
  1589. args = split(
  1590. x = 1:length(x = plots),
  1591. f = ceiling(x = seq_along(along.with = 1:length(x = plots)) / length(x = features))
  1592. )
  1593. ))]
  1594. # Set ncol to number of splits (nrow) and nrow to number of features (ncol)
  1595. plots <- wrap_plots(plots, ncol = nrow, nrow = ncol)
  1596. if (!is.null(x = legend) && legend == 'none') {
  1597. plots <- plots & NoLegend()
  1598. }
  1599. } else {
  1600. plots <- wrap_plots(plots, ncol = ncol, nrow = split.by %iff% length(x = levels(x = data$split)))
  1601. }
  1602. if (!is.null(x = legend) && legend == 'none') {
  1603. plots <- plots & NoLegend()
  1604. }
  1605. if (!(is.null(x = keep.scale)) && keep.scale == "all" && !blend) {
  1606. max.feature.value <- max(data[, features])
  1607. min.feature.value <- min(data[, features])
  1608. plots <- suppressMessages(plots & scale_color_gradientn(colors = cols, limits = c(min.feature.value, max.feature.value)))
  1609. }
  1610. }
  1611. return(plots)
  1612. }
  1613. #' Visualize features in dimensional reduction space interactively
  1614. #'
  1615. #' @inheritParams FeaturePlot
  1616. #' @param feature Feature to plot
  1617. #'
  1618. #' @return Returns the final plot as a ggplot object
  1619. #'
  1620. #' @importFrom cowplot theme_cowplot
  1621. #' @importFrom ggplot2 theme element_text guides scale_color_gradientn
  1622. #' @importFrom miniUI miniPage miniButtonBlock miniTitleBarButton miniContentPanel
  1623. #' @importFrom shiny fillRow sidebarPanel selectInput plotOutput reactiveValues
  1624. #' observeEvent stopApp observe updateSelectInput renderPlot runGadget
  1625. #'
  1626. #' @export
  1627. #' @concept visualization
  1628. #'
  1629. IFeaturePlot <- function(object, feature, dims = c(1, 2), reduction = NULL, slot = 'data') {
  1630. # Set initial data values
  1631. feature.label <- 'Feature to visualize'
  1632. assay.keys <- Key(object = object)[Assays(object = object)]
  1633. keyed <- sapply(X = assay.keys, FUN = grepl, x = feature)
  1634. assay <- if (any(keyed)) {
  1635. names(x = which(x = keyed))[1]
  1636. } else {
  1637. DefaultAssay(object = object)
  1638. }
  1639. features <- sort(x = rownames(x = GetAssayData(
  1640. object = object,
  1641. layer = slot,
  1642. assay = assay
  1643. )))
  1644. assays.use <- vapply(
  1645. X = Assays(object = object),
  1646. FUN = function(x) {
  1647. return(!IsMatrixEmpty(x = GetAssayData(
  1648. object = object,
  1649. layer = slot,
  1650. assay = x
  1651. )))
  1652. },
  1653. FUN.VALUE = logical(length = 1L)
  1654. )
  1655. assays.use <- sort(x = Assays(object = object)[assays.use])
  1656. reduction <- reduction %||% DefaultDimReduc(object = object)
  1657. dims.reduc <- gsub(
  1658. pattern = Key(object = object[[reduction]]),
  1659. replacement = '',
  1660. x = colnames(x = object[[reduction]])
  1661. )
  1662. # Set up the gadget UI
  1663. ui <- miniPage(
  1664. miniButtonBlock(miniTitleBarButton(
  1665. inputId = 'done',
  1666. label = 'Done',
  1667. primary = TRUE
  1668. )),
  1669. miniContentPanel(
  1670. fillRow(
  1671. sidebarPanel(
  1672. selectInput(
  1673. inputId = 'assay',
  1674. label = 'Assay',
  1675. choices = assays.use,
  1676. selected = assay,
  1677. selectize = FALSE,
  1678. width = '100%'
  1679. ),
  1680. selectInput(
  1681. inputId = 'feature',
  1682. label = feature.label,
  1683. choices = features,
  1684. selected = feature,
  1685. selectize = FALSE,
  1686. width = '100%'
  1687. ),
  1688. selectInput(
  1689. inputId = 'reduction',
  1690. label = 'Dimensional reduction',
  1691. choices = Reductions(object = object),
  1692. selected = reduction,
  1693. selectize = FALSE,
  1694. width = '100%'
  1695. ),
  1696. selectInput(
  1697. inputId = 'xdim',
  1698. label = 'X dimension',
  1699. choices = dims.reduc,
  1700. selected = as.character(x = dims[1]),
  1701. selectize = FALSE,
  1702. width = '100%'
  1703. ),
  1704. selectInput(
  1705. inputId = 'ydim',
  1706. label = 'Y dimension',
  1707. choices = dims.reduc,
  1708. selected = as.character(x = dims[2]),
  1709. selectize = FALSE,
  1710. width = '100%'
  1711. ),
  1712. selectInput(
  1713. inputId = 'palette',
  1714. label = 'Color scheme',
  1715. choices = names(x = FeaturePalettes),
  1716. selected = 'Seurat',
  1717. selectize = FALSE,
  1718. width = '100%'
  1719. ),
  1720. width = '100%'
  1721. ),
  1722. plotOutput(outputId = 'plot', height = '100%'),
  1723. flex = c(1, 4)
  1724. )
  1725. )
  1726. )
  1727. # Prepare plotting data
  1728. dims <- paste0(Key(object = object[[reduction]]), dims)
  1729. plot.data <- FetchData(object = object, vars = c(dims, feature), layer = slot)
  1730. # Shiny server
  1731. server <- function(input, output, session) {
  1732. plot.env <- reactiveValues(
  1733. data = plot.data,
  1734. dims = paste0(Key(object = object[[reduction]]), dims),
  1735. feature = feature,
  1736. palette = 'Seurat'
  1737. )
  1738. # Observe events
  1739. observeEvent(
  1740. eventExpr = input$done,
  1741. handlerExpr = stopApp(returnValue = plot.env$plot)
  1742. )
  1743. observe(x = {
  1744. assay <- input$assay
  1745. feature.use <- input$feature
  1746. features.assay <- sort(x = rownames(x = GetAssayData(
  1747. object = object,
  1748. layer = slot,
  1749. assay = assay
  1750. )))
  1751. feature.use <- ifelse(
  1752. test = feature.use %in% features.assay,
  1753. yes = feature.use,
  1754. no = features.assay[1]
  1755. )
  1756. reduc <- input$reduction
  1757. dims.reduc <- gsub(
  1758. pattern = Key(object = object[[reduc]]),
  1759. replacement = '',
  1760. x = colnames(x = object[[reduc]])
  1761. )
  1762. dims <- c(input$xdim, input$ydim)
  1763. for (i in seq_along(along.with = dims)) {
  1764. if (!dims[i] %in% dims.reduc) {
  1765. dims[i] <- dims.reduc[i]
  1766. }
  1767. }
  1768. updateSelectInput(
  1769. session = session,
  1770. inputId = 'xdim',
  1771. label = 'X dimension',
  1772. choices = dims.reduc,
  1773. selected = as.character(x = dims[1])
  1774. )
  1775. updateSelectInput(
  1776. session = session,
  1777. inputId = 'ydim',
  1778. label = 'Y dimension',
  1779. choices = dims.reduc,
  1780. selected = as.character(x = dims[2])
  1781. )
  1782. updateSelectInput(
  1783. session = session,
  1784. inputId = 'feature',
  1785. label = feature.label,
  1786. choices = features.assay,
  1787. selected = feature.use
  1788. )
  1789. })
  1790. observe(x = {
  1791. feature.use <- input$feature
  1792. feature.keyed <- paste0(Key(object = object[[input$assay]]), feature.use)
  1793. reduc <- input$reduction
  1794. dims <- c(input$xdim, input$ydim)
  1795. dims <- paste0(Key(object = object[[reduc]]), dims)
  1796. plot.data <- tryCatch(
  1797. expr = FetchData(
  1798. object = object,
  1799. vars = c(dims, feature.keyed),
  1800. layer = slot
  1801. ),
  1802. warning = function(...) {
  1803. return(plot.env$data)
  1804. },
  1805. error = function(...) {
  1806. return(plot.env$data)
  1807. }
  1808. )
  1809. dims <- colnames(x = plot.data)[1:2]
  1810. colnames(x = plot.data) <- c(dims, feature.use)
  1811. plot.env$data <- plot.data
  1812. plot.env$feature <- feature.use
  1813. plot.env$dims <- dims
  1814. })
  1815. observe(x = {
  1816. plot.env$palette <- input$palette
  1817. })
  1818. # Create the plot
  1819. output$plot <- renderPlot(expr = {
  1820. plot.env$plot <- SingleDimPlot(
  1821. data = plot.env$data,
  1822. dims = plot.env$dims,
  1823. col.by = plot.env$feature,
  1824. label = FALSE
  1825. ) +
  1826. theme_cowplot() +
  1827. theme(plot.title = element_text(hjust = 0.5)) +
  1828. guides(color = NULL) +
  1829. scale_color_gradientn(
  1830. colors = FeaturePalettes[[plot.env$palette]],
  1831. guide = 'colorbar'
  1832. )
  1833. plot.env$plot
  1834. })
  1835. }
  1836. runGadget(app = ui, server = server)
  1837. }
  1838. #' Highlight Neighbors in DimPlot
  1839. #'
  1840. #' It will color the query cells and the neighbors of the query cells in the
  1841. #' DimPlot
  1842. #'
  1843. #' @inheritParams DimPlot
  1844. #' @param nn.idx the neighbor index of all cells
  1845. #' @param query.cells cells used to find their neighbors
  1846. #' @param show.all.cells Show all cells or only query and neighbor cells
  1847. #'
  1848. #' @inherit DimPlot return
  1849. #'
  1850. #' @export
  1851. #' @concept visualization
  1852. #'
  1853. NNPlot <- function(
  1854. object,
  1855. reduction,
  1856. nn.idx,
  1857. query.cells,
  1858. dims = 1:2,
  1859. label = FALSE,
  1860. label.size = 4,
  1861. repel = FALSE,
  1862. sizes.highlight = 2,
  1863. pt.size = 1,
  1864. cols.highlight = c("#377eb8", "#e41a1c"),
  1865. na.value = "#bdbdbd",
  1866. order = c("self", "neighbors", "other"),
  1867. show.all.cells = TRUE,
  1868. ...
  1869. ) {
  1870. if (inherits(x = nn.idx, what = 'Neighbor')) {
  1871. rownames(x = slot(object = nn.idx, name = 'nn.idx')) <- Cells(x = nn.idx)
  1872. nn.idx <- Indices(object = nn.idx)
  1873. }
  1874. if (length(x = query.cells) > 1) {
  1875. neighbor.cells <- apply(
  1876. X = nn.idx[query.cells, -1],
  1877. MARGIN = 2,
  1878. FUN = function(x) {
  1879. return(Cells(x = object)[x])
  1880. }
  1881. )
  1882. } else {
  1883. neighbor.cells <- Cells(x = object)[nn.idx[query.cells , -1]]
  1884. }
  1885. neighbor.cells <- as.vector(x = neighbor.cells)
  1886. neighbor.cells <- neighbor.cells[!is.na(x = neighbor.cells)]
  1887. object[["nn.col"]] <- "other"
  1888. object[["nn.col"]][neighbor.cells, ] <- "neighbors"
  1889. object[["nn.col"]][query.cells, ] <- "self"
  1890. object$nn.col <- factor(
  1891. x = object$nn.col,
  1892. levels = c("self", "neighbors", "other")
  1893. )
  1894. if (!show.all.cells) {
  1895. object <- subset(
  1896. x = object,
  1897. cells = Cells(x = object)[which(x = object[["nn.col"]] != "other")]
  1898. )
  1899. nn.cols <- c(rev(x = cols.highlight))
  1900. nn.pt.size <- sizes.highlight
  1901. } else {
  1902. highlight.info <- SetHighlight(
  1903. cells.highlight = c(query.cells, neighbor.cells),
  1904. cells.all = Cells(x = object),
  1905. sizes.highlight = sizes.highlight,
  1906. pt.size = pt.size,
  1907. cols.highlight = "red"
  1908. )
  1909. nn.cols <- c(na.value, rev(x = cols.highlight))
  1910. nn.pt.size <- highlight.info$size
  1911. }
  1912. NN.plot <- DimPlot(
  1913. object = object,
  1914. reduction = reduction,
  1915. dims = dims,
  1916. group.by = "nn.col",
  1917. cols = nn.cols,
  1918. label = label,
  1919. order = order,
  1920. pt.size = nn.pt.size ,
  1921. label.size = label.size,
  1922. repel = repel
  1923. )
  1924. return(NN.plot)
  1925. }
  1926. #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  1927. # Scatter plots
  1928. #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  1929. #' Cell-cell scatter plot
  1930. #'
  1931. #' Creates a plot of scatter plot of features across two single cells. Pearson
  1932. #' correlation between the two cells is displayed above the plot.
  1933. #'
  1934. #' @inheritParams FeatureScatter
  1935. #' @inheritParams DimPlot
  1936. #' @param cell1 Cell 1 name
  1937. #' @param cell2 Cell 2 name
  1938. #' @param features Features to plot (default, all features)
  1939. #' @param highlight Features to highlight
  1940. #' @return A ggplot object
  1941. #'
  1942. #' @export
  1943. #' @concept visualization
  1944. #'
  1945. #' @aliases CellPlot
  1946. #'
  1947. #' @examples
  1948. #' data("pbmc_small")
  1949. #' CellScatter(object = pbmc_small, cell1 = 'ATAGGAGAAACAGA', cell2 = 'CATCAGGATGCACA')
  1950. #'
  1951. CellScatter <- function(
  1952. object,
  1953. cell1,
  1954. cell2,
  1955. features = NULL,
  1956. highlight = NULL,
  1957. cols = NULL,
  1958. pt.size = 1,
  1959. smooth = FALSE,
  1960. raster = NULL,
  1961. raster.dpi = c(512, 512)
  1962. ) {
  1963. features <- features %||% rownames(x = object)
  1964. data <- FetchData(
  1965. object = object,
  1966. vars = features,
  1967. cells = c(cell1, cell2)
  1968. )
  1969. data <- as.data.frame(x = t(x = data))
  1970. plot <- SingleCorPlot(
  1971. data = data,
  1972. cols = cols,
  1973. pt.size = pt.size,
  1974. rows.highlight = highlight,
  1975. smooth = smooth,
  1976. raster = raster,
  1977. raster.dpi = raster.dpi
  1978. )
  1979. return(plot)
  1980. }
  1981. #' Scatter plot of single cell data
  1982. #'
  1983. #' Creates a scatter plot of two features (typically feature expression), across a
  1984. #' set of single cells. Cells are colored by their identity class. Pearson
  1985. #' correlation between the two features is displayed above the plot.
  1986. #'
  1987. #' @param object Seurat object
  1988. #' @param feature1 First feature to plot. Typically feature expression but can also
  1989. #' be metrics, PC scores, etc. - anything that can be retreived with FetchData
  1990. #' @param feature2 Second feature to plot.
  1991. #' @param cells Cells to include on the scatter plot.
  1992. #' @param shuffle Whether to randomly shuffle the order of points. This can be
  1993. #' useful for crowded plots if points of interest are being buried. (default is FALSE)
  1994. #' @param seed Sets the seed if randomly shuffling the order of points.
  1995. #' @param group.by Name of one or more metadata columns to group (color) cells by
  1996. #' (for example, orig.ident); pass 'ident' to group by identity class
  1997. #' @param cols Colors to use for identity class plotting.
  1998. #' @param pt.size Size of the points on the plot
  1999. #' @param shape.by Ignored for now
  2000. #' @param split.by A factor in object metadata to split the feature plot by, pass 'ident'
  2001. #' to split by cell identity
  2002. #' @param span Spline span in loess function call, if \code{NULL}, no spline added
  2003. #' @param smooth Smooth the graph (similar to smoothScatter)
  2004. #' @param slot Slot to pull data from, should be one of 'counts', 'data', or 'scale.data'
  2005. #' @param combine Combine plots into a single \code{\link[patchwork]{patchwork}ed}
  2006. #' @param plot.cor Display correlation in plot title
  2007. #' @param ncol Number of columns if plotting multiple plots
  2008. #' @param raster Convert points to raster format, default is \code{NULL}
  2009. #' which will automatically use raster if the number of points plotted is greater than
  2010. #' 100,000
  2011. #' @param raster.dpi Pixel resolution for rasterized plots, passed to geom_scattermore().
  2012. #' Default is c(512, 512).
  2013. #' @param jitter Jitter for easier visualization of crowded points (default is FALSE)
  2014. #' @param log Plot features on the log scale (default is FALSE)
  2015. #'
  2016. #' @return A ggplot object
  2017. #'
  2018. #' @importFrom ggplot2 geom_smooth facet_wrap vars sym labs
  2019. #' @importFrom patchwork wrap_plots
  2020. #'
  2021. #' @export
  2022. #' @concept visualization
  2023. #'
  2024. #' @aliases GenePlot
  2025. #'
  2026. #' @examples
  2027. #' data("pbmc_small")
  2028. #' FeatureScatter(object = pbmc_small, feature1 = 'CD9', feature2 = 'CD3E')
  2029. #'
  2030. FeatureScatter <- function(
  2031. object,
  2032. feature1,
  2033. feature2,
  2034. cells = NULL,
  2035. shuffle = FALSE,
  2036. seed = 1,
  2037. group.by = NULL,
  2038. split.by = NULL,
  2039. cols = NULL,
  2040. pt.size = 1,
  2041. shape.by = NULL,
  2042. span = NULL,
  2043. smooth = FALSE,
  2044. combine = TRUE,
  2045. slot = 'data',
  2046. plot.cor = TRUE,
  2047. ncol = NULL,
  2048. raster = NULL,
  2049. raster.dpi = c(512, 512),
  2050. jitter = FALSE,
  2051. log = FALSE
  2052. ) {
  2053. cells <- cells %||% colnames(x = object)
  2054. if (isTRUE(x = shuffle)) {
  2055. set.seed(seed = seed)
  2056. cells <- sample(x = cells)
  2057. }
  2058. group.by <- group.by %||% 'ident'
  2059. data <- FetchData(
  2060. object = object,
  2061. vars = c(feature1, feature2, group.by),
  2062. cells = cells,
  2063. layer = slot
  2064. )
  2065. if (!grepl(pattern = feature1, x = names(x = data)[1], fixed = TRUE)) {
  2066. abort(message = paste("Feature 1", sQuote(x = feature1), "not found"))
  2067. }
  2068. if (!grepl(pattern = feature2, x = names(x = data)[2], fixed = TRUE)) {
  2069. abort(message = paste("Feature 2", sQuote(x = feature2), "not found"))
  2070. }
  2071. feature1 <- names(x = data)[1]
  2072. feature2 <- names(x = data)[2]
  2073. group.by <- intersect(x = group.by, y = names(x = data)[3:ncol(x = data)])
  2074. for (group in group.by) {
  2075. if (!is.factor(x = data[, group])) {
  2076. data[, group] <- factor(x = data[, group])
  2077. }
  2078. }
  2079. if (!is.null(x = split.by)) {
  2080. split <- FetchData(object = object, vars = split.by, clean=TRUE)[split.by]
  2081. data <- data[rownames(split),]
  2082. data[, split.by] <- split
  2083. }
  2084. plots <- lapply(
  2085. X = group.by,
  2086. FUN = function(x) {
  2087. plot <- SingleCorPlot(
  2088. data = data[,c(feature1, feature2, split.by)],
  2089. col.by = data[, x],
  2090. cols = cols,
  2091. pt.size = pt.size,
  2092. smooth = smooth,
  2093. legend.title = 'Identity',
  2094. span = span,
  2095. plot.cor = plot.cor,
  2096. raster = raster,
  2097. raster.dpi = raster.dpi,
  2098. jitter = jitter
  2099. )
  2100. if (!is.null(x = split.by)) {
  2101. plot <- plot + FacetTheme() +
  2102. facet_wrap(
  2103. facets = vars(!!sym(x = split.by)),
  2104. ncol = if (length(x = group.by) > 1 || is.null(x = ncol)) {
  2105. length(x = unique(x = data[, split.by]))
  2106. } else {
  2107. ncol
  2108. }
  2109. )
  2110. }
  2111. if (log) {
  2112. plot <- plot + scale_x_log10() + scale_y_log10()
  2113. }
  2114. plot
  2115. }
  2116. )
  2117. if (isTRUE(x = length(x = plots) == 1)) {
  2118. return(plots[[1]])
  2119. }
  2120. if (isTRUE(x = combine)) {
  2121. plots <- wrap_plots(plots, ncol = length(x = group.by))
  2122. }
  2123. return(plots)
  2124. }
  2125. #' View variable features
  2126. #'
  2127. #' @inheritParams FeatureScatter
  2128. #' @inheritParams SeuratObject::HVFInfo
  2129. #' @param cols Colors to specify non-variable/variable status
  2130. #' @param assay Assay to pull variable features from
  2131. #' @param log Plot the x-axis in log scale
  2132. #' @param raster Convert points to raster format, default is \code{NULL}
  2133. #' which will automatically use raster if the number of points plotted is greater than
  2134. #' 100,000
  2135. #'
  2136. #' @return A ggplot object
  2137. #'
  2138. #' @importFrom ggplot2 labs scale_color_manual scale_x_log10
  2139. #' @export
  2140. #' @concept visualization
  2141. #'
  2142. #' @aliases VariableGenePlot MeanVarPlot
  2143. #'
  2144. #' @seealso \code{\link{FindVariableFeatures}}
  2145. #'
  2146. #' @examples
  2147. #' data("pbmc_small")
  2148. #' VariableFeaturePlot(object = pbmc_small)
  2149. #'
  2150. VariableFeaturePlot <- function(
  2151. object,
  2152. cols = c('black', 'red'),
  2153. pt.size = 1,
  2154. log = NULL,
  2155. selection.method = NULL,
  2156. assay = NULL,
  2157. raster = NULL,
  2158. raster.dpi = c(512, 512)
  2159. ) {
  2160. if (length(x = cols) != 2) {
  2161. stop("'cols' must be of length 2")
  2162. }
  2163. hvf.info <- HVFInfo(
  2164. object = object,
  2165. assay = assay,
  2166. method = selection.method,
  2167. status = TRUE
  2168. )
  2169. status.col <- colnames(hvf.info)[grepl("variable", colnames(hvf.info))][[1]]
  2170. var.status <- c('no', 'yes')[unlist(hvf.info[[status.col]]) + 1]
  2171. if (colnames(x = hvf.info)[3] == 'dispersion.scaled') {
  2172. hvf.info <- hvf.info[, c(1, 2)]
  2173. } else if (colnames(x = hvf.info)[3] == 'variance.expected') {
  2174. hvf.info <- hvf.info[, c(1, 4)]
  2175. } else {
  2176. hvf.info <- hvf.info[, c(1, 3)]
  2177. }
  2178. axis.labels <- switch(
  2179. EXPR = colnames(x = hvf.info)[2],
  2180. 'variance.standardized' = c('Average Expression', 'Standardized Variance'),
  2181. 'dispersion' = c('Average Expression', 'Dispersion'),
  2182. 'residual_variance' = c('Geometric Mean of Expression', 'Residual Variance')
  2183. )
  2184. log <- log %||% (any(c('variance.standardized', 'residual_variance') %in% colnames(x = hvf.info)))
  2185. # var.features <- VariableFeatures(object = object, assay = assay)
  2186. # var.status <- ifelse(
  2187. # test = rownames(x = hvf.info) %in% var.features,
  2188. # yes = 'yes',
  2189. # no = 'no'
  2190. # )
  2191. plot <- SingleCorPlot(
  2192. data = hvf.info,
  2193. col.by = var.status,
  2194. pt.size = pt.size,
  2195. raster = raster,
  2196. raster.dpi = raster.dpi
  2197. )
  2198. if (length(x = unique(x = var.status)) == 1) {
  2199. switch(
  2200. EXPR = var.status[1],
  2201. 'yes' = {
  2202. cols <- cols[2]
  2203. labels.legend <- 'Variable'
  2204. },
  2205. 'no' = {
  2206. cols <- cols[1]
  2207. labels.legend <- 'Non-variable'
  2208. }
  2209. )
  2210. } else {
  2211. labels.legend <- c('Non-variable', 'Variable')
  2212. }
  2213. plot <- plot +
  2214. labs(title = NULL, x = axis.labels[1], y = axis.labels[2]) +
  2215. scale_color_manual(
  2216. labels = paste(labels.legend, 'count:', table(var.status)),
  2217. values = cols
  2218. )
  2219. if (log) {
  2220. plot <- plot + scale_x_log10()
  2221. }
  2222. return(plot)
  2223. }
  2224. #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  2225. # Polygon Plots
  2226. #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  2227. #' Polygon DimPlot
  2228. #'
  2229. #' Plot cells as polygons, rather than single points. Color cells by identity, or a categorical variable
  2230. #' in metadata
  2231. #'
  2232. #' @inheritParams PolyFeaturePlot
  2233. #' @param group.by A grouping variable present in the metadata. Default is to use the groupings present
  2234. #' in the current cell identities (\code{Idents(object = object)})
  2235. #'
  2236. #' @return Returns a ggplot object
  2237. #'
  2238. #' @export
  2239. #' @concept visualization
  2240. #'
  2241. PolyDimPlot <- function(
  2242. object,
  2243. group.by = NULL,
  2244. cells = NULL,
  2245. poly.data = 'spatial',
  2246. flip.coords = FALSE
  2247. ) {
  2248. polygons <- Misc(object = object, slot = poly.data)
  2249. if (is.null(x = polygons)) {
  2250. stop("Could not find polygon data in misc slot")
  2251. }
  2252. group.by <- group.by %||% 'ident'
  2253. group.data <- FetchData(
  2254. object = object,
  2255. vars = group.by,
  2256. cells = cells
  2257. )
  2258. group.data$cell <- rownames(x = group.data)
  2259. data <- merge(x = polygons, y = group.data, by = 'cell')
  2260. if (flip.coords) {
  2261. coord.x <- data$x
  2262. data$x <- data$y
  2263. data$y <- coord.x
  2264. }
  2265. plot <- SinglePolyPlot(data = data, group.by = group.by)
  2266. return(plot)
  2267. }
  2268. #' Polygon FeaturePlot
  2269. #'
  2270. #' Plot cells as polygons, rather than single points. Color cells by any value
  2271. #' accessible by \code{\link{FetchData}}.
  2272. #'
  2273. #' @inheritParams FeaturePlot
  2274. #' @param poly.data Name of the polygon dataframe in the misc slot
  2275. #' @param ncol Number of columns to split the plot into
  2276. #' @param common.scale ...
  2277. #' @param flip.coords Flip x and y coordinates
  2278. #'
  2279. #' @return Returns a ggplot object
  2280. #'
  2281. #' @importFrom ggplot2 scale_fill_viridis_c facet_wrap
  2282. #'
  2283. #' @export
  2284. #' @concept visualization
  2285. #' @concept spatial
  2286. #'
  2287. PolyFeaturePlot <- function(
  2288. object,
  2289. features,
  2290. cells = NULL,
  2291. poly.data = 'spatial',
  2292. ncol = ceiling(x = length(x = features) / 2),
  2293. min.cutoff = 0,
  2294. max.cutoff = NA,
  2295. common.scale = TRUE,
  2296. flip.coords = FALSE
  2297. ) {
  2298. polygons <- Misc(object = object, slot = poly.data)
  2299. if (is.null(x = polygons)) {
  2300. stop("Could not find polygon data in misc slot")
  2301. }
  2302. assay.data <- FetchData(
  2303. object = object,
  2304. vars = features,
  2305. cells = cells
  2306. )
  2307. features <- colnames(x = assay.data)
  2308. cells <- rownames(x = assay.data)
  2309. min.cutoff <- mapply(
  2310. FUN = function(cutoff, feature) {
  2311. return(ifelse(
  2312. test = is.na(x = cutoff),
  2313. yes = min(assay.data[, feature]),
  2314. no = cutoff
  2315. ))
  2316. },
  2317. cutoff = min.cutoff,
  2318. feature = features
  2319. )
  2320. max.cutoff <- mapply(
  2321. FUN = function(cutoff, feature) {
  2322. return(ifelse(
  2323. test = is.na(x = cutoff),
  2324. yes = max(assay.data[, feature]),
  2325. no = cutoff
  2326. ))
  2327. },
  2328. cutoff = max.cutoff,
  2329. feature = features
  2330. )
  2331. check.lengths <- unique(x = vapply(
  2332. X = list(features, min.cutoff, max.cutoff),
  2333. FUN = length,
  2334. FUN.VALUE = numeric(length = 1)
  2335. ))
  2336. if (length(x = check.lengths) != 1) {
  2337. stop("There must be the same number of minimum and maximum cuttoffs as there are features")
  2338. }
  2339. assay.data <- mapply(
  2340. FUN = function(feature, min, max) {
  2341. return(ScaleColumn(vec = assay.data[, feature], cutoffs = c(min, max)))
  2342. },
  2343. feature = features,
  2344. min = min.cutoff,
  2345. max = max.cutoff
  2346. )
  2347. if (common.scale) {
  2348. assay.data <- apply(
  2349. X = assay.data,
  2350. MARGIN = 2,
  2351. FUN = function(x) {
  2352. return(x - min(x))
  2353. }
  2354. )
  2355. assay.data <- t(
  2356. x = t(x = assay.data) / apply(X = assay.data, MARGIN = 2, FUN = max)
  2357. )
  2358. }
  2359. assay.data <- as.data.frame(x = assay.data)
  2360. assay.data <- data.frame(
  2361. cell = as.vector(x = replicate(n = length(x = features), expr = cells)),
  2362. feature = as.vector(x = t(x = replicate(n = length(x = cells), expr = features))),
  2363. expression = unlist(x = assay.data, use.names = FALSE)
  2364. )
  2365. data <- merge(x = polygons, y = assay.data, by = 'cell')
  2366. data$feature <- factor(x = data$feature, levels = features)
  2367. if (flip.coords) {
  2368. coord.x <- data$x
  2369. data$x <- data$y
  2370. data$y <- coord.x
  2371. }
  2372. plot <- SinglePolyPlot(data = data, group.by = 'expression', font_size = 8) +
  2373. scale_fill_viridis_c() +
  2374. facet_wrap(facets = 'feature', ncol = ncol)
  2375. return(plot)
  2376. }
  2377. #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  2378. # Spatial Plots
  2379. #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  2380. #' Spatial Cluster Plots
  2381. #'
  2382. #' Visualize clusters or other categorical groupings in a spatial context
  2383. #'
  2384. #' @inheritParams DimPlot
  2385. #' @inheritParams SingleImagePlot
  2386. #' @param object A \code{\link[SeuratObject]{Seurat}} object
  2387. #' @param fov Name of FOV to plot
  2388. #' @param boundaries A vector of segmentation boundaries per image to plot;
  2389. #' can be a character vector, a named character vector, or a named list.
  2390. #' Names should be the names of FOVs and values should be the names of
  2391. #' segmentation boundaries
  2392. #' @param molecules A vector of molecules to plot
  2393. #' @param nmols Max number of each molecule specified in `molecules` to plot
  2394. #' @param dark.background Set plot background to black
  2395. #' @param crop Crop the plots to area with cells only
  2396. #' @param overlap Overlay boundaries from a single image to create a single
  2397. #' plot; if \code{TRUE}, then boundaries are stacked in the order they're
  2398. #' given (first is lowest)
  2399. #' @param axes Keep axes and panel background
  2400. #' @param combine Combine plots into a single
  2401. #' \code{patchwork} ggplot object.If \code{FALSE},
  2402. #' return a list of ggplot objects
  2403. #' @param coord.fixed Plot cartesian coordinates with fixed aspect ratio
  2404. #' @param flip_xy Flag to flip X and Y axes. Default is FALSE.
  2405. #'
  2406. #' @return If \code{combine = TRUE}, a \code{patchwork}
  2407. #' ggplot object; otherwise, a list of ggplot objects
  2408. #'
  2409. #' @importFrom rlang !! is_na sym
  2410. #' @importFrom patchwork wrap_plots
  2411. #' @importFrom ggplot2 element_blank facet_wrap vars
  2412. #' @importFrom SeuratObject DefaultFOV Cells
  2413. #' DefaultBoundary FetchData Images Overlay
  2414. #'
  2415. #' @export
  2416. #' @concept visualization
  2417. #' @concept spatial
  2418. #'
  2419. ImageDimPlot <- function(
  2420. object,
  2421. fov = NULL,
  2422. boundaries = NULL,
  2423. group.by = NULL,
  2424. split.by = NULL,
  2425. cols = NULL,
  2426. shuffle.cols = FALSE,
  2427. size = 0.5,
  2428. molecules = NULL,
  2429. mols.size = 0.1,
  2430. mols.cols = NULL,
  2431. mols.alpha = 1.0,
  2432. nmols = 1000,
  2433. alpha = 1.0,
  2434. border.color = 'white',
  2435. border.size = NULL,
  2436. na.value = 'grey50',
  2437. dark.background = TRUE,
  2438. crop = FALSE,
  2439. cells = NULL,
  2440. overlap = FALSE,
  2441. axes = FALSE,
  2442. combine = TRUE,
  2443. coord.fixed = TRUE,
  2444. flip_xy = TRUE
  2445. ) {
  2446. cells <- cells %||% Cells(x = object)
  2447. # Determine FOV to use
  2448. fov <- fov %||% DefaultFOV(object = object)
  2449. fov <- Filter(
  2450. f = function(x) {
  2451. return(
  2452. x %in% Images(object = object) &&
  2453. inherits(x = object[[x]], what = 'FOV')
  2454. )
  2455. },
  2456. x = fov
  2457. )
  2458. if (!length(x = fov)) {
  2459. stop("No compatible spatial coordinates present")
  2460. }
  2461. # Identify boundaries to use
  2462. boundaries <- boundaries %||% sapply(
  2463. X = fov,
  2464. FUN = function(x) {
  2465. return(DefaultBoundary(object = object[[x]]))
  2466. },
  2467. simplify = FALSE,
  2468. USE.NAMES = TRUE
  2469. )
  2470. boundaries <- .BoundariesByImage(
  2471. object = object,
  2472. fov = fov,
  2473. boundaries = boundaries
  2474. )
  2475. fov <- names(x = boundaries)
  2476. overlap <- rep_len(x = overlap, length.out = length(x = fov))
  2477. crop <- rep_len(x = crop, length.out = length(x = fov))
  2478. names(x = crop) <- fov
  2479. # Prepare plotting data
  2480. group.by <- boundaries %!NA% group.by %||% 'ident'
  2481. vars <- c(group.by, split.by)
  2482. md <- if (!is_na(x = vars)) {
  2483. FetchData(
  2484. object = object,
  2485. vars = vars[!is.na(x = vars)],
  2486. cells = cells
  2487. )
  2488. } else {
  2489. NULL
  2490. }
  2491. pnames <- unlist(x = lapply(
  2492. X = seq_along(along.with = fov),
  2493. FUN = function(i) {
  2494. return(if (isTRUE(x = overlap[i])) {
  2495. fov[i]
  2496. } else {
  2497. paste(fov[i], boundaries[[i]], sep = '_')
  2498. })
  2499. }
  2500. ))
  2501. pdata <- vector(mode = 'list', length = length(x = pnames))
  2502. names(x = pdata) <- pnames
  2503. for (i in names(x = pdata)) {
  2504. ul <- unlist(x = strsplit(x = i, split = '_'))
  2505. img <- paste(ul[1:length(ul)-1], collapse = '_')
  2506. # Apply overlap
  2507. lyr <- ul[length(ul)]
  2508. if (is.na(x = lyr)) {
  2509. lyr <- boundaries[[img]]
  2510. }
  2511. # TODO: Apply crop
  2512. pdata[[i]] <- lapply(
  2513. X = lyr,
  2514. FUN = function(l) {
  2515. if (l == 'NA') {
  2516. return(NA)
  2517. }
  2518. df <- fortify(model = object[[img]][[l]])
  2519. df <- df[df$cell %in% cells, , drop = FALSE]
  2520. if (!is.null(x = md)) {
  2521. df <- merge(x = df, y = md, by.x = 'cell', by.y = 0, all.x = TRUE)
  2522. }
  2523. df$cell <- paste(l, df$cell, sep = '_')
  2524. df$boundary <- l
  2525. return(df)
  2526. }
  2527. )
  2528. pdata[[i]] <- if (!is_na(x = pdata[[i]])) {
  2529. do.call(what = 'rbind', args = pdata[[i]])
  2530. } else {
  2531. unlist(x = pdata[[i]])
  2532. }
  2533. }
  2534. # Fetch molecule information
  2535. if (!is.null(x = molecules)) {
  2536. molecules <- .MolsByFOV(
  2537. object = object,
  2538. fov = fov,
  2539. molecules = molecules
  2540. )
  2541. mdata <- vector(mode = 'list', length = length(x = fov))
  2542. names(x = mdata) <- fov
  2543. for (img in names(x = mdata)) {
  2544. idata <- object[[img]]
  2545. if (!img %in% names(x = molecules)) {
  2546. mdata[[img]] <- NULL
  2547. next
  2548. }
  2549. if (isTRUE(x = crop[img])) {
  2550. idata <- Overlay(x = idata, y = idata)
  2551. }
  2552. imols <- gsub(
  2553. pattern = paste0('^', Key(object = idata)),
  2554. replacement = '',
  2555. x = molecules[[img]]
  2556. )
  2557. mdata[[img]] <- FetchData(
  2558. object = idata,
  2559. vars = imols,
  2560. nmols = nmols
  2561. )
  2562. }
  2563. } else {
  2564. mdata <- NULL
  2565. }
  2566. # Build the plots
  2567. plots <- vector(
  2568. mode = 'list',
  2569. length = length(x = pdata) * ifelse(
  2570. test = length(x = group.by),
  2571. yes = length(x = group.by),
  2572. no = 1L
  2573. )
  2574. )
  2575. idx <- 1L
  2576. for (group in group.by) {
  2577. for (i in seq_along(along.with = pdata)) {
  2578. img <- unlist(x = strsplit(x = names(x = pdata)[i], split = '_'))[1L]
  2579. p <- SingleImagePlot(
  2580. data = pdata[[i]],
  2581. col.by = pdata[[i]] %!NA% group,
  2582. molecules = mdata[[img]],
  2583. cols = cols,
  2584. shuffle.cols = shuffle.cols,
  2585. size = size,
  2586. alpha = alpha,
  2587. mols.size = mols.size,
  2588. mols.cols = mols.cols,
  2589. mols.alpha = mols.alpha,
  2590. border.color = border.color,
  2591. border.size = border.size,
  2592. na.value = na.value,
  2593. dark.background = dark.background
  2594. )
  2595. if (!is.null(x = split.by)) {
  2596. p <- p + facet_wrap(
  2597. facets = vars(!!sym(x = split.by))
  2598. )
  2599. }
  2600. if (!isTRUE(x = axes)) {
  2601. p <- p + NoAxes(panel.background = element_blank())
  2602. }
  2603. if (!anyDuplicated(x = pdata[[i]]$cell)) {
  2604. p <- p + guides(fill = guide_legend(override.aes = list(size=4L, alpha=1)))
  2605. }
  2606. if (isTRUE(coord.fixed)) {
  2607. p <- p + coord_fixed()
  2608. }
  2609. if(!isTRUE(flip_xy) && isTRUE(coord.fixed)){
  2610. xy_ratio = (max(pdata[[i]]$x) - min(pdata[[i]]$x)) / (max(pdata[[i]]$y) - min(pdata[[i]]$y))
  2611. p = p + coord_flip() + theme(aspect.ratio = 1/xy_ratio)
  2612. }
  2613. plots[[idx]] <- p
  2614. idx <- idx + 1L
  2615. }
  2616. }
  2617. if (isTRUE(x = combine)) {
  2618. plots <- wrap_plots(plots)
  2619. }
  2620. return(plots)
  2621. }
  2622. #' Spatial Feature Plots
  2623. #'
  2624. #' Visualize expression in a spatial context
  2625. #'
  2626. #' @inheritParams FeaturePlot
  2627. #' @inheritParams ImageDimPlot
  2628. #' @param scale Set color scaling across multiple plots; choose from:
  2629. #' \itemize{
  2630. #' \item \dQuote{\code{feature}}: Plots per-feature are scaled across splits
  2631. #' \item \dQuote{\code{all}}: Plots per-feature are scaled across all features
  2632. #' \item \dQuote{\code{none}}: Plots are not scaled; \strong{note}: setting
  2633. #' \code{scale} to \dQuote{\code{none}} will result in color scales that are
  2634. #' \emph{not} comparable between plots
  2635. #' }
  2636. #' Ignored if \code{blend = TRUE}
  2637. #'
  2638. #' @inherit ImageDimPlot return
  2639. #'
  2640. #' @importFrom patchwork wrap_plots
  2641. #' @importFrom cowplot theme_cowplot
  2642. #' @importFrom ggplot2 dup_axis element_blank element_text facet_wrap guides
  2643. #' labs margin vars scale_y_continuous theme
  2644. #' @importFrom SeuratObject DefaultFOV Cells DefaultBoundary
  2645. #' FetchData Images Overlay
  2646. #'
  2647. #' @export
  2648. #' @concept visualization
  2649. #' @concept spatial
  2650. #'
  2651. ImageFeaturePlot <- function(
  2652. object,
  2653. features,
  2654. fov = NULL,
  2655. boundaries = NULL,
  2656. cols = if (isTRUE(x = blend)) {
  2657. c("lightgrey", "#ff0000", "#00ff00")
  2658. } else {
  2659. c("lightgrey", "firebrick1")
  2660. },
  2661. size = 0.5,
  2662. min.cutoff = NA,
  2663. max.cutoff = NA,
  2664. split.by = NULL,
  2665. molecules = NULL,
  2666. mols.size = 0.1,
  2667. mols.cols = NULL,
  2668. nmols = 1000,
  2669. alpha = 1.0,
  2670. border.color = 'white',
  2671. border.size = NULL,
  2672. dark.background = TRUE,
  2673. blend = FALSE,
  2674. blend.threshold = 0.5,
  2675. crop = FALSE,
  2676. cells = NULL,
  2677. scale = c('feature', 'all', 'none'),
  2678. overlap = FALSE,
  2679. axes = FALSE,
  2680. combine = TRUE,
  2681. coord.fixed = TRUE
  2682. ) {
  2683. cells <- cells %||% Cells(x = object)
  2684. scale <- scale[[1L]]
  2685. scale <- match.arg(arg = scale)
  2686. # Set a theme to remove right-hand Y axis lines
  2687. # Also sets right-hand Y axis text label formatting
  2688. no.right <- theme(
  2689. axis.line.y.right = element_blank(),
  2690. axis.ticks.y.right = element_blank(),
  2691. axis.text.y.right = element_blank(),
  2692. axis.title.y.right = element_text(
  2693. face = "bold",
  2694. size = 14,
  2695. margin = margin(r = 7)
  2696. )
  2697. )
  2698. # Determine fov to use
  2699. fov <- fov %||% DefaultFOV(object = object)
  2700. fov <- Filter(
  2701. f = function(x) {
  2702. return(
  2703. x %in% Images(object = object) &&
  2704. inherits(x = object[[x]], what = 'FOV')
  2705. )
  2706. },
  2707. x = fov
  2708. )
  2709. if (!length(x = fov)) {
  2710. stop("No compatible spatial coordinates present")
  2711. }
  2712. # Identify boundaries to use
  2713. boundaries <- boundaries %||% sapply(
  2714. X = fov,
  2715. FUN = function(x) {
  2716. return(DefaultBoundary(object = object[[x]]))
  2717. },
  2718. simplify = FALSE,
  2719. USE.NAMES = TRUE
  2720. )
  2721. boundaries <- .BoundariesByImage(
  2722. object = object,
  2723. fov = fov,
  2724. boundaries = boundaries
  2725. )
  2726. fov <- names(x = boundaries)
  2727. # Check overlaps/crops
  2728. if (isTRUE(x = blend) || !is.null(x = split.by)) {
  2729. type <- ifelse(test = isTRUE(x = 'blend'), yes = 'Blended', no = 'Split')
  2730. if (length(x = fov) != 1L) {
  2731. fov <- fov[1L]
  2732. warning(
  2733. type,
  2734. ' image feature plots can only be done on a single image, using "',
  2735. fov,
  2736. '"',
  2737. call. = FALSE,
  2738. immediate. = TRUE
  2739. )
  2740. }
  2741. if (any(!overlap) && length(x = boundaries[[fov]]) > 1L) {
  2742. warning(
  2743. type,
  2744. " image feature plots require overlapped segmentations",
  2745. call. = FALSE,
  2746. immediate. = TRUE
  2747. )
  2748. }
  2749. overlap <- TRUE
  2750. }
  2751. overlap <- rep_len(x = overlap, length.out = length(x = fov))
  2752. crop <- rep_len(x = crop, length.out = length(x = fov))
  2753. names(x = crop) <- names(x = overlap) <- fov
  2754. # Checks for blending
  2755. if (isTRUE(x = blend)) {
  2756. if (length(x = features) != 2L) {
  2757. stop("Blended feature plots only works with two features")
  2758. }
  2759. default.colors <- eval(expr = formals(fun = ImageFeaturePlot)$cols)
  2760. cols <- switch(
  2761. EXPR = as.character(x = length(x = cols)),
  2762. '0' = {
  2763. warning("No colors provided, using default colors", immediate. = TRUE)
  2764. default.colors
  2765. },
  2766. '1' = {
  2767. warning(
  2768. "Only one color provided, assuming specified is double-negative and augmenting with default colors",
  2769. immediate. = TRUE
  2770. )
  2771. c(cols, default.colors[2:3])
  2772. },
  2773. '2' = {
  2774. warning(
  2775. "Only two colors provided, assuming specified are for features and augmenting with '",
  2776. default.colors[1],
  2777. "' for double-negatives",
  2778. immediate. = TRUE
  2779. )
  2780. c(default.colors[1], cols)
  2781. },
  2782. '3' = cols,
  2783. {
  2784. warning(
  2785. "More than three colors provided, using only first three",
  2786. immediate. = TRUE
  2787. )
  2788. cols[1:3]
  2789. }
  2790. )
  2791. }
  2792. # Get feature, splitting data
  2793. md <- FetchData(
  2794. object = object,
  2795. vars = c(features, split.by[1L]),
  2796. cells = cells
  2797. )
  2798. split.by <- intersect(x = split.by, y = colnames(x = md))
  2799. if (!length(x = split.by)) {
  2800. split.by <- NULL
  2801. }
  2802. imax <- ifelse(
  2803. test = is.null(x = split.by),
  2804. yes = ncol(x = md),
  2805. no = ncol(x = md) - length(x = split.by)
  2806. )
  2807. features <- colnames(x = md)[1:imax]
  2808. # Determine cutoffs
  2809. min.cutoff <- mapply(
  2810. FUN = function(cutoff, feature) {
  2811. return(ifelse(
  2812. test = is.na(x = cutoff),
  2813. yes = min(md[[feature]]),
  2814. no = cutoff
  2815. ))
  2816. },
  2817. cutoff = min.cutoff,
  2818. feature = features
  2819. )
  2820. max.cutoff <- mapply(
  2821. FUN = function(cutoff, feature) {
  2822. return(ifelse(
  2823. test = is.na(x = cutoff),
  2824. yes = max(md[[feature]]),
  2825. no = cutoff
  2826. ))
  2827. },
  2828. cutoff = max.cutoff,
  2829. feature = features
  2830. )
  2831. check.lengths <- unique(x = vapply(
  2832. X = list(features, min.cutoff, max.cutoff),
  2833. FUN = length,
  2834. FUN.VALUE = numeric(length = 1)
  2835. ))
  2836. if (length(x = check.lengths) != 1) {
  2837. stop("There must be the same number of minimum and maximum cuttoffs as there are features")
  2838. }
  2839. brewer.gran <- ifelse(
  2840. test = length(x = cols) == 1,
  2841. yes = brewer.pal.info[cols, ]$maxcolors,
  2842. no = length(x = cols)
  2843. )
  2844. # Apply cutoffs
  2845. for (i in seq_along(along.with = features)) {
  2846. f <- features[[i]]
  2847. data.feature <- md[[f]]
  2848. min.use <- SetQuantile(cutoff = min.cutoff[i], data = data.feature)
  2849. max.use <- SetQuantile(cutoff = max.cutoff[i], data = data.feature)
  2850. data.feature[data.feature < min.use] <- min.use
  2851. data.feature[data.feature > max.use] <- max.use
  2852. if (brewer.gran != 2) {
  2853. data.feature <- if (all(data.feature == 0)) {
  2854. rep_len(x = 0, length.out = length(x = data.feature))
  2855. } else {
  2856. as.numeric(x = as.factor(x = cut(
  2857. x = as.numeric(x = data.feature),
  2858. breaks = brewer.gran
  2859. )))
  2860. }
  2861. }
  2862. md[[f]] <- data.feature
  2863. }
  2864. # Figure out splits
  2865. if (is.null(x = split.by)) {
  2866. split.by <- RandomName()
  2867. md[[split.by]] <- factor(x = split.by)
  2868. }
  2869. if (!is.factor(x = md[[split.by]])) {
  2870. md[[split.by]] <- factor(x = md[[split.by]])
  2871. }
  2872. # Apply blends
  2873. if (isTRUE(x = blend)) {
  2874. md <- lapply(
  2875. X = levels(x = md[[split.by]]),
  2876. FUN = function(x) {
  2877. df <- md[as.character(x = md[[split.by]]) == x, , drop = FALSE]
  2878. no.expression <- features[colMeans(x = df[, features]) == 0]
  2879. if (length(x = no.expression)) {
  2880. stop(
  2881. "The following features have no value: ",
  2882. paste(no.expression, collapse = ', ')
  2883. )
  2884. }
  2885. return(cbind(
  2886. df[, split.by, drop = FALSE],
  2887. BlendExpression(data = df[, features])
  2888. ))
  2889. }
  2890. )
  2891. md <- do.call(what = 'rbind', args = md)
  2892. features <- setdiff(x = colnames(x = md), y = split.by)
  2893. }
  2894. # Prepare plotting data
  2895. pnames <- unlist(x = lapply(
  2896. X = seq_along(along.with = fov),
  2897. FUN = function(i) {
  2898. return(if (isTRUE(x = overlap[i])) {
  2899. fov[i]
  2900. } else {
  2901. paste(fov[i], boundaries[[i]], sep = '_')
  2902. })
  2903. }
  2904. ))
  2905. pdata <- vector(mode = 'list', length = length(x = pnames))
  2906. names(x = pdata) <- pnames
  2907. for (i in names(x = pdata)) {
  2908. ul <- unlist(x = strsplit(x = i, split = '_'))
  2909. # img <- paste(ul[1:length(ul)-1], collapse = '_')
  2910. # Apply overlap
  2911. # lyr <- ul[length(ul)]
  2912. if(length(ul) > 1) {
  2913. img <- paste(ul[1:length(ul)-1], collapse = '_')
  2914. lyr <- ul[length(ul)]
  2915. } else if (length(ul) == 1) {
  2916. img <- ul[1]
  2917. lyr <- "centroids"
  2918. } else {
  2919. stop("the length of ul is 0. please check.")
  2920. }
  2921. if (is.na(x = lyr)) {
  2922. lyr <- boundaries[[img]]
  2923. }
  2924. pdata[[i]] <- lapply(
  2925. X = lyr,
  2926. FUN = function(l) {
  2927. df <- fortify(model = object[[img]][[l]])
  2928. df <- df[df$cell %in% cells, , drop = FALSE]
  2929. if (!is.null(x = md)) {
  2930. df <- merge(x = df, y = md, by.x = 'cell', by.y = 0, all.x = TRUE)
  2931. }
  2932. df$cell <- paste(l, df$cell, sep = '_')
  2933. df$boundary <- l
  2934. return(df)
  2935. }
  2936. )
  2937. pdata[[i]] <- if (!is_na(x = pdata[[i]])) {
  2938. do.call(what = 'rbind', args = pdata[[i]])
  2939. } else {
  2940. unlist(x = pdata[[i]])
  2941. }
  2942. }
  2943. # Fetch molecule information
  2944. if (!is.null(x = molecules)) {
  2945. molecules <- .MolsByFOV(
  2946. object = object,
  2947. fov = fov,
  2948. molecules = molecules
  2949. )
  2950. mdata <- vector(mode = 'list', length = length(x = fov))
  2951. names(x = mdata) <- fov
  2952. for (img in names(x = mdata)) {
  2953. idata <- object[[img]]
  2954. if (!img %in% names(x = molecules)) {
  2955. mdata[[img]] <- NULL
  2956. next
  2957. }
  2958. if (isTRUE(x = crop[img])) {
  2959. idata <- Overlay(x = idata, y = idata)
  2960. }
  2961. imols <- gsub(
  2962. pattern = paste0('^', Key(object = idata)),
  2963. replacement = '',
  2964. x = molecules[[img]]
  2965. )
  2966. mdata[[img]] <- FetchData(
  2967. object = idata,
  2968. vars = imols,
  2969. nmols = nmols
  2970. )
  2971. }
  2972. } else {
  2973. mdata <- NULL
  2974. }
  2975. # Set blended colors
  2976. if (isTRUE(x = blend)) {
  2977. ncol <- 4
  2978. color.matrix <- BlendMatrix(
  2979. two.colors = cols[2:3],
  2980. col.threshold = blend.threshold,
  2981. negative.color = cols[1]
  2982. )
  2983. cols <- cols[2:3]
  2984. colors <- list(
  2985. color.matrix[, 1],
  2986. color.matrix[1, ],
  2987. as.vector(x = color.matrix)
  2988. )
  2989. blend.legend <- BlendMap(color.matrix = color.matrix)
  2990. }
  2991. limits <- switch(
  2992. EXPR = scale,
  2993. 'all' = range(unlist(x = md[, features])),
  2994. NULL
  2995. )
  2996. # Build the plots
  2997. plots <- vector(
  2998. mode = 'list',
  2999. length = length(x = levels(x = md[[split.by]]))
  3000. )
  3001. names(x = plots) <- levels(x = md[[split.by]])
  3002. for (i in seq_along(along.with = levels(x = md[[split.by]]))) {
  3003. ident <- levels(x = md[[split.by]])[i]
  3004. plots[[ident]] <- vector(mode = 'list', length = length(x = pdata))
  3005. names(x = plots[[ident]]) <- names(x = pdata)
  3006. if (isTRUE(x = blend)) {
  3007. blend.key <- suppressMessages(
  3008. expr = blend.legend +
  3009. scale_y_continuous(
  3010. sec.axis = dup_axis(name = ifelse(
  3011. test = length(x = levels(x = md[[split.by]])) > 1,
  3012. yes = ident,
  3013. no = ''
  3014. )),
  3015. expand = c(0, 0)
  3016. ) +
  3017. labs(
  3018. x = features[1L],
  3019. y = features[2L],
  3020. title = if (i == 1L) {
  3021. paste('Color threshold:', blend.threshold)
  3022. } else {
  3023. NULL
  3024. }
  3025. ) +
  3026. no.right
  3027. )
  3028. }
  3029. for (j in seq_along(along.with = pdata)) {
  3030. key <- names(x = pdata)[j]
  3031. img <- unlist(x = strsplit(x = key, split = '_'))[1L]
  3032. plots[[ident]][[key]] <- vector(
  3033. mode = 'list',
  3034. length = length(x = features) + ifelse(
  3035. test = isTRUE(x = blend),
  3036. yes = 1L,
  3037. no = 0L
  3038. )
  3039. )
  3040. data.plot <- pdata[[j]][as.character(x = pdata[[j]][[split.by]]) == ident, , drop = FALSE]
  3041. for (y in seq_along(along.with = features)) {
  3042. feature <- features[y]
  3043. # Get blended colors
  3044. cols.use <- if (isTRUE(x = blend)) {
  3045. cc <- as.numeric(x = as.character(x = data.plot[, feature])) + 1
  3046. colors[[y]][sort(unique(x = cc))]
  3047. } else {
  3048. NULL
  3049. }
  3050. colnames(data.plot) <- gsub("-", "_", colnames(data.plot))
  3051. p <- SingleImagePlot(
  3052. data = data.plot,
  3053. col.by = gsub("-", "_", feature),
  3054. size = size,
  3055. col.factor = blend,
  3056. cols = cols.use,
  3057. molecules = mdata[[img]],
  3058. mols.size = mols.size,
  3059. mols.cols = mols.cols,
  3060. alpha = alpha,
  3061. border.color = border.color,
  3062. border.size = border.size,
  3063. dark.background = dark.background
  3064. ) +
  3065. CenterTitle() + labs(fill=feature)
  3066. # Remove fill guides for blended plots
  3067. if (isTRUE(x = blend)) {
  3068. p <- p + guides(fill = 'none')
  3069. }
  3070. if (isTRUE(coord.fixed)) {
  3071. p <- p + coord_fixed()
  3072. }
  3073. # Remove axes
  3074. if (!isTRUE(x = axes)) {
  3075. p <- p + NoAxes(panel.background = element_blank())
  3076. } else if (isTRUE(x = blend) || length(x = levels(x = md[[split.by]])) > 1L) {
  3077. if (y != 1L) {
  3078. p <- p + theme(
  3079. axis.line.y = element_blank(),
  3080. axis.ticks.y = element_blank(),
  3081. axis.text.y = element_blank(),
  3082. axis.title.y.left = element_blank()
  3083. )
  3084. }
  3085. if (i != length(x = levels(x = md[[split.by]]))) {
  3086. p <- p + theme(
  3087. axis.line.x = element_blank(),
  3088. axis.ticks.x = element_blank(),
  3089. axis.text.x = element_blank(),
  3090. axis.title.x = element_blank()
  3091. )
  3092. }
  3093. }
  3094. # Add colors for unblended plots
  3095. if (!isTRUE(x = blend)) {
  3096. if (length(x = cols) == 1L) {
  3097. p <- p + scale_fill_brewer(palette = cols)
  3098. } else {
  3099. cols.grad <- cols
  3100. fexp <- data.plot[data.plot[[split.by]] == ident, feature, drop = TRUE]
  3101. fexp <- unique(x = fexp)
  3102. if (length(x = fexp) == 1L) {
  3103. warning(
  3104. "All cells have the same value (",
  3105. fexp,
  3106. ") of ",
  3107. feature,
  3108. call. = FALSE,
  3109. immediate. = TRUE
  3110. )
  3111. if (fexp == 0) {
  3112. cols.grad <- cols.grad[1L]
  3113. }
  3114. }
  3115. # Check if we're scaling the colorbar across splits
  3116. if (scale == 'feature') {
  3117. limits <- range(pdata[[j]][[feature]])
  3118. }
  3119. p <- p + ggplot2::scale_fill_gradientn(
  3120. colors = cols.grad,
  3121. guide = 'colorbar',
  3122. limits = limits
  3123. )
  3124. }
  3125. }
  3126. # Add some labels
  3127. p <- p + if (i == 1L) {
  3128. ggplot2::labs(title = feature)
  3129. } else {
  3130. ggplot2::labs(title = NULL)
  3131. }
  3132. plots[[ident]][[key]][[y]] <- p
  3133. }
  3134. if (isTRUE(x = blend)) {
  3135. plots[[ident]][[key]][[length(x = plots[[ident]][[key]])]] <- blend.key
  3136. } else if (length(x = levels(x = md[[split.by]])) > 1L) {
  3137. plots[[ident]][[key]][[y]] <- suppressMessages(
  3138. expr = plots[[ident]][[key]][[y]] +
  3139. scale_y_continuous(sec.axis = dup_axis(name = ident)) +
  3140. no.right
  3141. )
  3142. }
  3143. }
  3144. plots[[ident]] <- unlist(
  3145. x = plots[[ident]],
  3146. recursive = FALSE,
  3147. use.names = FALSE
  3148. )
  3149. }
  3150. plots <- unlist(x = plots, recursive = FALSE, use.names = FALSE)
  3151. if (isTRUE(x = combine)) {
  3152. if (isTRUE(x = blend) || length(x = levels(x = md[[split.by]])) > 1L) {
  3153. plots <- wrap_plots(
  3154. plots,
  3155. ncol = ifelse(
  3156. test = isTRUE(x = blend),
  3157. yes = 4L,
  3158. no = length(x = features)
  3159. ),
  3160. nrow = length(x = levels(x = md[[split.by]])),
  3161. guides = 'collect'
  3162. )
  3163. } else {
  3164. plots <- wrap_plots(plots)
  3165. }
  3166. }
  3167. return(plots)
  3168. }
  3169. #' Visualize spatial and clustering (dimensional reduction) data in a linked,
  3170. #' interactive framework
  3171. #'
  3172. #' @inheritParams SpatialPlot
  3173. #' @inheritParams FeaturePlot
  3174. #' @inheritParams DimPlot
  3175. #' @param feature Feature to visualize
  3176. #' @param image Name of the image to use in the plot
  3177. #'
  3178. #' @return Returns final plots. If \code{combine}, plots are stiched together
  3179. #' using \code{\link{CombinePlots}}; otherwise, returns a list of ggplot objects
  3180. #'
  3181. #' @rdname LinkedPlots
  3182. #' @name LinkedPlots
  3183. #'
  3184. #' @importFrom scales hue_pal
  3185. #' @importFrom patchwork wrap_plots
  3186. #' @importFrom ggplot2 scale_alpha_ordinal guides
  3187. #' @importFrom miniUI miniPage gadgetTitleBar miniTitleBarButton miniContentPanel
  3188. #' @importFrom shiny fillRow plotOutput brushOpts clickOpts hoverOpts
  3189. #' verbatimTextOutput reactiveValues observeEvent stopApp nearPoints
  3190. #' brushedPoints renderPlot renderPrint runGadget
  3191. #'
  3192. #' @aliases LinkedPlot LinkedDimPlot
  3193. #'
  3194. #' @export
  3195. #' @concept visualization
  3196. #' @concept spatial
  3197. #'
  3198. #' @examples
  3199. #' \dontrun{
  3200. #' LinkedDimPlot(seurat.object)
  3201. #' LinkedFeaturePlot(seurat.object, feature = 'Hpca')
  3202. #' }
  3203. #'
  3204. LinkedDimPlot <- function(
  3205. object,
  3206. dims = 1:2,
  3207. reduction = NULL,
  3208. image = NULL,
  3209. image.scale = "lowres",
  3210. group.by = NULL,
  3211. alpha = c(0.1, 1),
  3212. combine = TRUE
  3213. ) {
  3214. # Setup gadget UI
  3215. ui <- miniPage(
  3216. gadgetTitleBar(
  3217. title = 'LinkedDimPlot',
  3218. left = miniTitleBarButton(inputId = 'reset', label = 'Reset')
  3219. ),
  3220. miniContentPanel(
  3221. fillRow(
  3222. plotOutput(
  3223. outputId = 'spatialplot',
  3224. height = '100%',
  3225. # brush = brushOpts(id = 'brush', delay = 10, clip = TRUE, resetOnNew = FALSE),
  3226. click = clickOpts(id = 'spclick', clip = TRUE),
  3227. hover = hoverOpts(id = 'sphover', delay = 10, nullOutside = TRUE)
  3228. ),
  3229. plotOutput(
  3230. outputId = 'dimplot',
  3231. height = '100%',
  3232. brush = brushOpts(id = 'brush', delay = 10, clip = TRUE, resetOnNew = FALSE),
  3233. click = clickOpts(id = 'dimclick', clip = TRUE),
  3234. hover = hoverOpts(id = 'dimhover', delay = 10, nullOutside = TRUE)
  3235. ),
  3236. height = '97%'
  3237. ),
  3238. verbatimTextOutput(outputId = 'info')
  3239. )
  3240. )
  3241. # Prepare plotting data
  3242. image <- image %||% DefaultImage(object = object)
  3243. cells.use <- Cells(x = object[[image]])
  3244. reduction <- reduction %||% DefaultDimReduc(object = object)
  3245. dims <- dims[1:2]
  3246. dims <- paste0(Key(object = object[[reduction]]), dims)
  3247. group.by <- group.by %||% 'ident'
  3248. group.data <- FetchData(
  3249. object = object,
  3250. vars = group.by,
  3251. cells = cells.use
  3252. )
  3253. coords <- GetTissueCoordinates(object = object[[image]], scale = image.scale)
  3254. embeddings <- Embeddings(object = object[[reduction]])[cells.use, dims]
  3255. plot.data <- cbind(coords, group.data, embeddings)
  3256. plot.data$selected_ <- FALSE
  3257. Idents(object = object) <- group.by
  3258. # Retrieve coordinates for tissue plot and dim plot separately
  3259. sp_x <- colnames(coords)[1]
  3260. sp_y <- colnames(coords)[2]
  3261. dp_x <- dims[1]
  3262. dp_y <- dims[2]
  3263. sp_y_min <- min(plot.data[[sp_y]])
  3264. sp_y_max <- max(plot.data[[sp_y]])
  3265. # Add tiny helper function to flip interactive coordinate points
  3266. flip_y <- function(pt) { if (!is.null(pt$y)) { pt$y <- sp_y_max - (pt$y - sp_y_min) }; pt }
  3267. # Setup the server
  3268. server <- function(input, output, session) {
  3269. click <- reactiveValues(pt = NULL, invert = FALSE)
  3270. plot.env <- reactiveValues(data = plot.data, alpha.by = NULL)
  3271. # Handle events
  3272. observeEvent(
  3273. eventExpr = input$done,
  3274. handlerExpr = {
  3275. plots <- list(plot.env$spatialplot, plot.env$dimplot)
  3276. if (combine) {
  3277. plots <- wrap_plots(plots, ncol = 2)
  3278. }
  3279. stopApp(returnValue = plots)
  3280. }
  3281. )
  3282. observeEvent(
  3283. eventExpr = input$reset,
  3284. handlerExpr = {
  3285. click$pt <- NULL
  3286. click$invert <- FALSE
  3287. plot.env$data <- plot.data
  3288. plot.env$alpha.by <- NULL
  3289. session$resetBrush(brushId = 'brush')
  3290. }
  3291. )
  3292. observeEvent(eventExpr = input$brush, handlerExpr = click$pt <- NULL)
  3293. observeEvent(
  3294. eventExpr = input$spclick,
  3295. handlerExpr = {
  3296. # Flip coordinates vertically for spatial plot to match tissue image
  3297. click$pt <- flip_y(input$spclick)
  3298. click$invert <- TRUE
  3299. }
  3300. )
  3301. observeEvent(
  3302. eventExpr = input$dimclick,
  3303. handlerExpr = {
  3304. click$pt <- input$dimclick
  3305. click$invert <- FALSE
  3306. }
  3307. )
  3308. observeEvent(
  3309. eventExpr = c(input$brush, input$spclick, input$dimclick),
  3310. handlerExpr = {
  3311. plot.env$data <- if (is.null(x = input$brush)) {
  3312. clicked <- nearPoints(
  3313. df = plot.data,
  3314. coordinfo = click$pt,
  3315. threshold = 10,
  3316. maxpoints = 1,
  3317. xvar = if (click$invert) sp_x else dp_x,
  3318. yvar = if (click$invert) sp_y else dp_y
  3319. )
  3320. if (nrow(x = clicked) == 1) {
  3321. cell.clicked <- rownames(x = clicked)
  3322. group.clicked <- plot.data[cell.clicked, group.by, drop = TRUE]
  3323. idx.group <- which(x = plot.data[[group.by]] == group.clicked)
  3324. plot.data[idx.group, 'selected_'] <- TRUE
  3325. plot.data
  3326. } else {
  3327. plot.data
  3328. }
  3329. } else if (input$brush$outputId == 'dimplot') {
  3330. brushedPoints(df = plot.data, brush = input$brush, allRows = TRUE, xvar = dp_x, yvar = dp_y)
  3331. } else if (input$brush$outputId == 'spatialplot') {
  3332. b <- input$brush
  3333. b$ymin <- sp_y_max - (b$ymin - sp_y_min)
  3334. b$ymax <- sp_y_max - (b$ymax - sp_y_min)
  3335. brushedPoints(df = plot.data, brush = b, allRows = TRUE, xvar = sp_x, yvar = sp_y)
  3336. }
  3337. plot.env$alpha.by <- if (any(plot.env$data$selected_)) {
  3338. 'selected_'
  3339. } else {
  3340. NULL
  3341. }
  3342. }
  3343. )
  3344. # Set plots
  3345. output$spatialplot <- renderPlot(
  3346. expr = {
  3347. plot.env$spatialplot <- SingleSpatialPlot(
  3348. data = plot.env$data,
  3349. image = object[[image]],
  3350. col.by = group.by,
  3351. pt.size.factor = 1.6,
  3352. crop = TRUE,
  3353. alpha.by = plot.env$alpha.by
  3354. ) + scale_alpha_ordinal(range = alpha) + NoLegend()
  3355. plot.env$spatialplot
  3356. }
  3357. )
  3358. output$dimplot <- renderPlot(
  3359. expr = {
  3360. plot.env$dimplot <- SingleDimPlot(
  3361. data = plot.env$data,
  3362. dims = dims,
  3363. col.by = group.by,
  3364. alpha.by = plot.env$alpha.by
  3365. ) + scale_alpha_ordinal(range = alpha) + guides(alpha = "none")
  3366. plot.env$dimplot
  3367. }
  3368. )
  3369. # Add hover text
  3370. output$info <- renderPrint(
  3371. expr = {
  3372. cell.hover <- rownames(x = nearPoints(
  3373. df = plot.data,
  3374. coordinfo = if (is.null(input[['sphover']])) {
  3375. input$dimhover
  3376. } else {
  3377. flip_y(input$sphover)
  3378. },
  3379. threshold = 10,
  3380. maxpoints = 1,
  3381. xvar = if (is.null(input$sphover)) dp_x else sp_x,
  3382. yvar = if (is.null(input$sphover)) dp_y else sp_y
  3383. ))
  3384. # if (length(x = cell.hover) == 1) {
  3385. # palette <- hue_pal()(n = length(x = levels(x = object)))
  3386. # group <- plot.data[cell.hover, group.by, drop = TRUE]
  3387. # background <- palette[which(x = levels(x = object) == group)]
  3388. # text <- unname(obj = BGTextColor(background = background))
  3389. # style <- paste0(
  3390. # paste(
  3391. # paste('background-color:', background),
  3392. # paste('color:', text),
  3393. # sep = '; '
  3394. # ),
  3395. # ';'
  3396. # )
  3397. # info <- paste(cell.hover, paste('Group:', group), sep = '<br />')
  3398. # } else {
  3399. # style <- 'background-color: white; color: black'
  3400. # info <- NULL
  3401. # }
  3402. # HTML(text = paste0("<div style='", style, "'>", info, "</div>"))
  3403. # p(HTML(info), style = style)
  3404. # paste0('<div style="', style, '">', info, '</div>')
  3405. # TODO: Get newlines, extra information, and background color working
  3406. if (length(x = cell.hover) == 1) {
  3407. paste(cell.hover, paste('Group:', plot.data[cell.hover, group.by, drop = TRUE]), collapse = '<br />')
  3408. } else {
  3409. NULL
  3410. }
  3411. }
  3412. )
  3413. }
  3414. # Run the thang
  3415. runGadget(app = ui, server = server)
  3416. }
  3417. #' @rdname LinkedPlots
  3418. #'
  3419. #' @aliases LinkedFeaturePlot
  3420. #'
  3421. #' @importFrom ggplot2 scale_fill_gradientn theme scale_alpha guides
  3422. #' scale_color_gradientn guide_colorbar
  3423. #'
  3424. #' @export
  3425. #' @concept visualization
  3426. #' @concept spatial
  3427. LinkedFeaturePlot <- function(
  3428. object,
  3429. feature,
  3430. dims = 1:2,
  3431. reduction = NULL,
  3432. image = NULL,
  3433. image.scale = "lowres",
  3434. slot = 'data',
  3435. alpha = c(0.1, 1),
  3436. combine = TRUE
  3437. ) {
  3438. # Setup gadget UI
  3439. ui <- miniPage(
  3440. gadgetTitleBar(
  3441. title = 'LinkedFeaturePlot',
  3442. left = NULL
  3443. ),
  3444. miniContentPanel(
  3445. fillRow(
  3446. plotOutput(
  3447. outputId = 'spatialplot',
  3448. height = '100%',
  3449. hover = hoverOpts(id = 'sphover', delay = 10, nullOutside = TRUE)
  3450. ),
  3451. plotOutput(
  3452. outputId = 'dimplot',
  3453. height = '100%',
  3454. hover = hoverOpts(id = 'dimhover', delay = 10, nullOutside = TRUE)
  3455. ),
  3456. height = '97%'
  3457. ),
  3458. verbatimTextOutput(outputId = 'info')
  3459. )
  3460. )
  3461. # Prepare plotting data
  3462. cols <- SpatialColors(n = 100)
  3463. image <- image %||% DefaultImage(object = object)
  3464. cells.use <- Cells(x = object[[image]])
  3465. reduction <- reduction %||% DefaultDimReduc(object = object)
  3466. dims <- dims[1:2]
  3467. dims <- paste0(Key(object = object[[reduction]]), dims)
  3468. group.data <- FetchData(
  3469. object = object,
  3470. vars = feature,
  3471. cells = cells.use
  3472. )
  3473. coords <- GetTissueCoordinates(object = object[[image]], scale = image.scale)
  3474. embeddings <- Embeddings(object = object[[reduction]])[cells.use, dims]
  3475. sp_x <- colnames(coords)[1]
  3476. sp_y <- colnames(coords)[2]
  3477. dp_x <- dims[1]
  3478. dp_y <- dims[2]
  3479. # coordinates should be in image space, so need to flip y when setting or displaying info for points
  3480. flip_y <- function(pt) { if (!is.null(pt)) { pt$y <- max(coords[[sp_y]]) - (pt$y - min(coords[[sp_y]])) }; pt }
  3481. plot.data <- cbind(coords, group.data, embeddings)
  3482. # Setup the server
  3483. server <- function(input, output, session) {
  3484. plot.env <- reactiveValues()
  3485. # Handle events
  3486. observeEvent(
  3487. eventExpr = input$done,
  3488. handlerExpr = {
  3489. plots <- list(plot.env$spatialplot, plot.env$dimplot)
  3490. if (combine) {
  3491. plots <- wrap_plots(plots, ncol = 2)
  3492. }
  3493. stopApp(returnValue = plots)
  3494. }
  3495. )
  3496. # Set plots
  3497. output$spatialplot <- renderPlot(
  3498. expr = {
  3499. plot.env$spatialplot <- SingleSpatialPlot(
  3500. data = plot.data,
  3501. image = object[[image]],
  3502. col.by = feature,
  3503. pt.size.factor = 1.6,
  3504. crop = TRUE,
  3505. alpha.by = feature
  3506. ) +
  3507. scale_fill_gradientn(name = feature, colours = cols) +
  3508. theme(legend.position = 'top') +
  3509. scale_alpha(range = alpha) +
  3510. guides(alpha = "none")
  3511. plot.env$spatialplot
  3512. }
  3513. )
  3514. output$dimplot <- renderPlot(
  3515. expr = {
  3516. plot.env$dimplot <- SingleDimPlot(
  3517. data = plot.data,
  3518. dims = dims,
  3519. col.by = feature
  3520. ) +
  3521. scale_color_gradientn(name = feature, colours = cols, guide = 'colorbar') +
  3522. guides(color = guide_colorbar())
  3523. plot.env$dimplot
  3524. }
  3525. )
  3526. # Add hover text
  3527. output$info <- renderPrint(
  3528. expr = {
  3529. cell.hover <- rownames(x = nearPoints(
  3530. df = plot.data,
  3531. coordinfo = if (is.null(x = input[['sphover']])) {
  3532. input$dimhover
  3533. } else {
  3534. flip_y(input$sphover)
  3535. },
  3536. threshold = 10,
  3537. maxpoints = 1,
  3538. # specify plot-specific axis columns for nearPoints
  3539. xvar = if (is.null(x = input$sphover)) dp_x else sp_x,
  3540. yvar = if (is.null(x = input$sphover)) dp_y else sp_y
  3541. ))
  3542. # TODO: Get newlines, extra information, and background color working
  3543. if (length(x = cell.hover) == 1) {
  3544. paste(cell.hover, paste('Expression:', plot.data[cell.hover, feature, drop = TRUE]), collapse = '<br />')
  3545. } else {
  3546. NULL
  3547. }
  3548. }
  3549. )
  3550. }
  3551. runGadget(app = ui, server = server)
  3552. }
  3553. #' Visualize clusters spatially and interactively
  3554. #'
  3555. #' @inheritParams SpatialPlot
  3556. #' @inheritParams DimPlot
  3557. #' @inheritParams LinkedPlots
  3558. #'
  3559. #' @return Returns final plot as a ggplot object
  3560. #'
  3561. #' @importFrom ggplot2 scale_alpha_ordinal
  3562. #' @importFrom miniUI miniPage miniButtonBlock miniTitleBarButton miniContentPanel
  3563. #' @importFrom shiny fillRow plotOutput verbatimTextOutput reactiveValues
  3564. #' observeEvent stopApp nearPoints renderPlot runGadget
  3565. #'
  3566. #' @export
  3567. #' @concept visualization
  3568. #' @concept spatial
  3569. #'
  3570. ISpatialDimPlot <- function(
  3571. object,
  3572. image = NULL,
  3573. image.scale = "lowres",
  3574. group.by = NULL,
  3575. alpha = c(0.3, 1)
  3576. ) {
  3577. # Setup gadget UI
  3578. ui <- miniPage(
  3579. miniButtonBlock(miniTitleBarButton(
  3580. inputId = 'done',
  3581. label = 'Done',
  3582. primary = TRUE
  3583. )),
  3584. miniContentPanel(
  3585. fillRow(
  3586. plotOutput(
  3587. outputId = 'plot',
  3588. height = '100%',
  3589. click = clickOpts(id = 'click', clip = TRUE),
  3590. hover = hoverOpts(id = 'hover', delay = 10, nullOutside = TRUE)
  3591. ),
  3592. height = '97%'
  3593. ),
  3594. verbatimTextOutput(outputId = 'info')
  3595. )
  3596. )
  3597. # Get plotting data
  3598. # Prepare plotting data
  3599. image <- image %||% DefaultImage(object = object)
  3600. cells.use <- Cells(x = object[[image]])
  3601. group.by <- group.by %||% 'ident'
  3602. group.data <- FetchData(
  3603. object = object,
  3604. vars = group.by,
  3605. cells = cells.use
  3606. )
  3607. coords <- GetTissueCoordinates(object = object[[image]], scale = image.scale)
  3608. sp_x <- colnames(coords)[1]
  3609. sp_y <- colnames(coords)[2]
  3610. # coordinates should be in image space, so need to flip y when setting or displaying info for points
  3611. flip_y <- function(pt) { if (!is.null(pt)) { pt$y <- max(coords[[sp_y]]) - (pt$y - min(coords[[sp_y]])) }; pt }
  3612. scale.factor <- ScaleFactors(object[[image]])[[image.scale]]
  3613. plot.data <- cbind(coords, group.data)
  3614. plot.data$selected_ <- FALSE
  3615. Idents(object = object) <- group.by
  3616. # Set up the server
  3617. server <- function(input, output, session) {
  3618. click <- reactiveValues(pt = NULL)
  3619. plot.env <- reactiveValues(data = plot.data, alpha.by = NULL)
  3620. # Handle events
  3621. observeEvent(
  3622. eventExpr = input$done,
  3623. handlerExpr = stopApp(returnValue = plot.env$plot)
  3624. )
  3625. observeEvent(
  3626. eventExpr = input$click,
  3627. handlerExpr = {
  3628. click$pt <- flip_y(input$click)
  3629. clicked <- nearPoints(
  3630. df = plot.data,
  3631. coordinfo = click$pt,
  3632. threshold = 10,
  3633. maxpoints = 1,
  3634. xvar = sp_x,
  3635. yvar = sp_y
  3636. )
  3637. plot.env$data <- if (nrow(x = clicked) == 1) {
  3638. cell.clicked <- rownames(x = clicked)
  3639. group.clicked <- plot.data[cell.clicked, group.by, drop = TRUE]
  3640. idx.group <- which(x = plot.data[[group.by]] == group.clicked)
  3641. plot.data[idx.group, 'selected_'] <- TRUE
  3642. plot.data
  3643. } else {
  3644. plot.data
  3645. }
  3646. plot.env$alpha.by <- if (any(plot.env$data$selected_)) {
  3647. 'selected_'
  3648. } else {
  3649. NULL
  3650. }
  3651. }
  3652. )
  3653. # Set plot
  3654. output$plot <- renderPlot(
  3655. expr = {
  3656. plot.env$plot <- SingleSpatialPlot(
  3657. data = plot.env$data,
  3658. image = object[[image]],
  3659. col.by = group.by,
  3660. crop = TRUE,
  3661. alpha.by = plot.env$alpha.by,
  3662. pt.size.factor = 1.6
  3663. ) + scale_alpha_ordinal(range = alpha) + NoLegend()
  3664. plot.env$plot
  3665. }
  3666. )
  3667. # Add hover text
  3668. output$info <- renderPrint(
  3669. expr = {
  3670. hovered <- nearPoints(
  3671. df = plot.data,
  3672. coordinfo = flip_y(input$hover),
  3673. threshold = 10,
  3674. maxpoints = 1,
  3675. xvar = sp_x,
  3676. yvar = sp_y
  3677. )
  3678. if (nrow(hovered) == 1) {
  3679. cell.hover <- rownames(hovered)
  3680. # SingleSpatialPlot relies on the spatial coordinates appearing
  3681. # in the first two columns of the returned data.frame - it's kinda
  3682. # fragile but we're obligated to use the same behaviour here
  3683. coords.hover <- hovered[1, colnames(coords)[1:2]] / scale.factor
  3684. group.hover <- hovered[1, group.by]
  3685. sprintf(
  3686. "Cell: %s, Group: %s, Coordinates: (%.2f, %.2f)",
  3687. cell.hover,
  3688. group.hover,
  3689. coords.hover[[1]],
  3690. coords.hover[[2]]
  3691. )
  3692. } else {
  3693. NULL
  3694. }
  3695. }
  3696. )
  3697. }
  3698. runGadget(app = ui, server = server)
  3699. }
  3700. #' Visualize features spatially and interactively
  3701. #'
  3702. #' @inheritParams SpatialPlot
  3703. #' @inheritParams FeaturePlot
  3704. #' @inheritParams LinkedPlots
  3705. #'
  3706. #' @return Returns final plot as a ggplot object
  3707. #'
  3708. #' @importFrom ggplot2 scale_fill_gradientn theme scale_alpha guides
  3709. #' @importFrom miniUI miniPage miniButtonBlock miniTitleBarButton miniContentPanel
  3710. #' @importFrom shiny fillRow sidebarPanel sliderInput selectInput reactiveValues
  3711. #' observeEvent stopApp observe updateSelectInput plotOutput renderPlot runGadget
  3712. #'
  3713. #' @export
  3714. #' @concept visualization
  3715. #' @concept spatial
  3716. ISpatialFeaturePlot <- function(
  3717. object,
  3718. feature,
  3719. image = NULL,
  3720. image.scale = "lowres",
  3721. slot = 'data',
  3722. alpha = c(0.1, 1)
  3723. ) {
  3724. # Set inital data values
  3725. assay.keys <- Key(object = object)[Assays(object = object)]
  3726. keyed <- sapply(X = assay.keys, FUN = grepl, x = feature)
  3727. assay <- if (any(keyed)) {
  3728. names(x = which(x = keyed))[1]
  3729. } else {
  3730. DefaultAssay(object = object)
  3731. }
  3732. features <- sort(x = rownames(x = GetAssayData(
  3733. object = object,
  3734. layer = slot,
  3735. assay = assay
  3736. )))
  3737. feature.label <- 'Feature to visualize'
  3738. assays.use <- vapply(
  3739. X = Assays(object = object),
  3740. FUN = function(x) {
  3741. return(!IsMatrixEmpty(x = GetAssayData(
  3742. object = object,
  3743. layer = slot,
  3744. assay = x
  3745. )))
  3746. },
  3747. FUN.VALUE = logical(length = 1L)
  3748. )
  3749. assays.use <- sort(x = Assays(object = object)[assays.use])
  3750. # Setup gadget UI
  3751. ui <- miniPage(
  3752. miniButtonBlock(miniTitleBarButton(
  3753. inputId = 'done',
  3754. label = 'Done',
  3755. primary = TRUE
  3756. )),
  3757. miniContentPanel(
  3758. fillRow(
  3759. sidebarPanel(
  3760. sliderInput(
  3761. inputId = 'alpha',
  3762. label = 'Alpha intensity',
  3763. min = 0,
  3764. max = max(alpha),
  3765. value = min(alpha),
  3766. step = 0.01,
  3767. width = '100%'
  3768. ),
  3769. sliderInput(
  3770. inputId = 'pt.size',
  3771. label = 'Point size',
  3772. min = 0,
  3773. max = 5,
  3774. value = 1.6,
  3775. step = 0.1,
  3776. width = '100%'
  3777. ),
  3778. selectInput(
  3779. inputId = 'assay',
  3780. label = 'Assay',
  3781. choices = assays.use,
  3782. selected = assay,
  3783. selectize = FALSE,
  3784. width = '100%'
  3785. ),
  3786. selectInput(
  3787. inputId = 'feature',
  3788. label = feature.label,
  3789. choices = features,
  3790. selected = feature,
  3791. selectize = FALSE,
  3792. width = '100%'
  3793. ),
  3794. selectInput(
  3795. inputId = 'palette',
  3796. label = 'Color scheme',
  3797. choices = names(x = FeaturePalettes),
  3798. selected = 'Spatial',
  3799. selectize = FALSE,
  3800. width = '100%'
  3801. ),
  3802. width = '100%'
  3803. ),
  3804. plotOutput(outputId = 'plot', height = '100%'),
  3805. flex = c(1, 4)
  3806. )
  3807. )
  3808. )
  3809. # Prepare plotting data
  3810. image <- image %||% DefaultImage(object = object)
  3811. cells.use <- Cells(x = object[[image]])
  3812. coords <- GetTissueCoordinates(object = object[[image]], scale = image.scale)
  3813. feature.data <- FetchData(
  3814. object = object,
  3815. vars = feature,
  3816. cells = cells.use,
  3817. layer = slot
  3818. )
  3819. plot.data <- cbind(coords, feature.data)
  3820. server <- function(input, output, session) {
  3821. plot.env <- reactiveValues(
  3822. data = plot.data,
  3823. feature = feature,
  3824. palette = 'Spatial'
  3825. )
  3826. # Observe events
  3827. observeEvent(
  3828. eventExpr = input$done,
  3829. handlerExpr = stopApp(returnValue = plot.env$plot)
  3830. )
  3831. observe(x = {
  3832. assay <- input$assay
  3833. feature.use <- input$feature
  3834. features.assay <- sort(x = rownames(x = GetAssayData(
  3835. object = object,
  3836. layer = slot,
  3837. assay = assay
  3838. )))
  3839. feature.use <- ifelse(
  3840. test = feature.use %in% features.assay,
  3841. yes = feature.use,
  3842. no = features.assay[1]
  3843. )
  3844. updateSelectInput(
  3845. session = session,
  3846. inputId = 'assay',
  3847. label = 'Assay',
  3848. choices = assays.use,
  3849. selected = assay
  3850. )
  3851. updateSelectInput(
  3852. session = session,
  3853. inputId = 'feature',
  3854. label = feature.label,
  3855. choices = features.assay,
  3856. selected = feature.use
  3857. )
  3858. })
  3859. observe(x = {
  3860. feature.use <- input$feature
  3861. try(
  3862. expr = {
  3863. feature.data <- FetchData(
  3864. object = object,
  3865. vars = paste0(Key(object = object[[input$assay]]), feature.use),
  3866. cells = cells.use,
  3867. layer = slot
  3868. )
  3869. colnames(x = feature.data) <- feature.use
  3870. plot.env$data <- cbind(coords, feature.data)
  3871. plot.env$feature <- feature.use
  3872. },
  3873. silent = TRUE
  3874. )
  3875. })
  3876. observe(x = {
  3877. plot.env$palette <- input$palette
  3878. })
  3879. # Create plot
  3880. output$plot <- renderPlot(expr = {
  3881. plot.env$plot <- SingleSpatialPlot(
  3882. data = plot.env$data,
  3883. image = object[[image]],
  3884. col.by = plot.env$feature,
  3885. pt.size.factor = input$pt.size,
  3886. crop = TRUE,
  3887. alpha.by = plot.env$feature
  3888. ) +
  3889. # scale_fill_gradientn(name = plot.env$feature, colours = cols) +
  3890. scale_fill_gradientn(name = plot.env$feature, colours = FeaturePalettes[[plot.env$palette]]) +
  3891. theme(legend.position = 'top') +
  3892. scale_alpha(range = c(input$alpha, 1)) +
  3893. guides(alpha = "none")
  3894. plot.env$plot
  3895. })
  3896. }
  3897. runGadget(app = ui, server = server)
  3898. }
  3899. #' Interactive Spatial Cell Selection Tool
  3900. #'
  3901. #' Launch an interactive gadget for lasso-based cell selection from a spatial Seurat object.
  3902. #' Supports Visium, SlideSeq, and Vizgen data. Returns the cell names of the selected subset,
  3903. #' suitable for downstream subsetting or analysis.
  3904. #'
  3905. #' @note This function requires the
  3906. #' \href{https://cran.r-project.org/package=plotly}{\pkg{plotly}},
  3907. #' \href{https://cran.r-project.org/package=magrittr}{\pkg{magrittr}},
  3908. #' and \href{https://cran.r-project.org/package=base64enc}{\pkg{base64enc}} packages
  3909. #' to be installed. It also requires \pkg{shiny} and \pkg{miniUI} for the interactive UI.
  3910. #'
  3911. #' @param object A \code{\link[SeuratObject]{Seurat}} object with spatial data.
  3912. #' @param image Name of the spatial image stored in the object. If \code{NULL}, uses the default image for the object.
  3913. #' @param image.scale Character. Which image scaling factor to use for spatial coordinate transformation (\code{"lowres"} by default).
  3914. #' @param group.by Metadata variable (column name) to use for coloring cell points (e.g., cluster assignment). If \code{NULL}, uses \code{"seurat_clusters"} if available, otherwise all cells are grouped together.
  3915. #' @param alpha Numeric transparency value for cell points (default \code{1.0}).
  3916. #' @param pt.size.factor Numeric scaling factor for point size (default \code{1.0}).
  3917. #' @param overlay_image Logical; if \code{TRUE}, overlays the tissue image in the background of the plot (default \code{TRUE}).
  3918. #'
  3919. #' @importFrom grDevices png dev.off
  3920. #' @importFrom miniUI miniPage gadgetTitleBar miniContentPanel
  3921. #' @importFrom shiny uiOutput reactiveVal renderUI tags observeEvent stopApp runGadget
  3922. #'
  3923. #' @return A character vector of cell names selected via lasso, which can be used to subset the object.
  3924. #' @export
  3925. #'
  3926. #' @examples
  3927. #' \dontrun{
  3928. #' selected_cells <- InteractiveSpatialPlot(object = brain)
  3929. #' selected_cells <- InteractiveSpatialPlot(object = brain, overlay_image = FALSE)
  3930. #' }
  3931. InteractiveSpatialPlot <- function(
  3932. object,
  3933. image = NULL,
  3934. image.scale = "lowres",
  3935. group.by = NULL,
  3936. alpha = 1.0,
  3937. pt.size.factor = 1.0,
  3938. overlay_image = TRUE
  3939. ) {
  3940. # Check for required packages, stop with clear message if missing
  3941. required_pkgs <- c("plotly", "magrittr", "base64enc", "shiny")
  3942. missing_pkgs <- required_pkgs[
  3943. !vapply(required_pkgs, requireNamespace, quietly = TRUE, FUN.VALUE = logical(1))
  3944. ]
  3945. if (length(missing_pkgs) > 0) {
  3946. stop(
  3947. "InteractiveSpatialPlot() functionality requires these packages to be installed: ",
  3948. paste0("'", missing_pkgs, "'", collapse = ", "),
  3949. call. = FALSE
  3950. )
  3951. }
  3952. # Import magrittr pipe locally
  3953. `%>%` <- magrittr::`%>%`
  3954. # Use provided image name or fallback to default
  3955. image <- image %||% DefaultImage(object)
  3956. # Sanity check: requested image must exist in the object
  3957. if (!image %in% names(object@images)) {
  3958. stop("Image '", image, "' not found. Available image(s): ", paste(names(object@images), collapse = ", "))
  3959. }
  3960. # Retrieve the spatial image object
  3961. image_obj <- object[[image]]
  3962. # Determine image technology type (Visium, SlideSeq, or Vizgen)
  3963. img_class <- class(image_obj)[1]
  3964. if (img_class %in% c("VisiumV1", "VisiumV2")) {
  3965. type <- "visium"
  3966. } else if (img_class == "SlideSeq") {
  3967. type <- "slideseq"
  3968. } else if (img_class == "FOV") {
  3969. type <- "vizgen"
  3970. } else {
  3971. stop("Unrecognized image class: ", img_class)
  3972. }
  3973. # Extract and scale cell coordinates according to image type
  3974. if (type == "visium") {
  3975. # For Visium: coordinates stored in centroids, need scaling
  3976. if (!"boundaries" %in% slotNames(image_obj)) {
  3977. stop("Image object does not have a 'boundaries' slot; check if data is truly Visium data")
  3978. }
  3979. centroids <- image_obj@boundaries$centroids
  3980. coords <- setNames(as.data.frame(centroids@coords), c("x", "y"))
  3981. coords$cell <- centroids@cells
  3982. # Scale coordinates to match image pixel units
  3983. scale.factor <- Seurat::ScaleFactors(image_obj)[[image.scale]]
  3984. if (is.null(scale.factor)) stop("Scale factor for '", image.scale, "' not found")
  3985. coords$x_raw <- coords$x # Store original, unscaled x
  3986. coords$y_raw <- coords$y # Store original, unscaled y
  3987. coords$x <- coords$x * scale.factor
  3988. coords$y <- coords$y * scale.factor
  3989. } else if (type == "slideseq") {
  3990. # For Slide-seq: coordinates are stored directly
  3991. if (!"coordinates" %in% slotNames(image_obj)) {
  3992. stop("Image object does not have a 'coordinates' slot; check if data is truly Slide-seq data")
  3993. }
  3994. coords <- as.data.frame(image_obj@coordinates)
  3995. coords$cell <- rownames(coords)
  3996. colnames(coords)[1:2] <- c("x", "y")
  3997. coords$x_raw <- coords$x
  3998. coords$y_raw <- coords$y
  3999. } else if (type == "vizgen") {
  4000. # For Vizgen: coordinates in centroids
  4001. if (!"boundaries" %in% slotNames(image_obj)) {
  4002. stop("Vizgen FOV missing 'boundaries' slot")
  4003. }
  4004. centroids <- image_obj@boundaries$centroids
  4005. coords <- as.data.frame(centroids@coords)
  4006. colnames(coords) <- c("x", "y")
  4007. coords$cell <- centroids@cells
  4008. coords$x_raw <- coords$x
  4009. coords$y_raw <- coords$y
  4010. }
  4011. # Get cell-level metadata for grouping/labeling
  4012. meta <- [email hidden]
  4013. # If group.by not given, use 'seurat_clusters' if available; otherwise group all together
  4014. if (is.null(group.by)) {
  4015. group.by <- if ("seurat_clusters" %in% colnames(meta)) "seurat_clusters" else "all"
  4016. }
  4017. # Assign group/cluster for coloring the plot
  4018. if (group.by != "all") {
  4019. coords$group <- meta[coords$cell, group.by]
  4020. } else {
  4021. coords$group <- "all"
  4022. }
  4023. # Compose hover text: show cell name, original (x, y) coordinates, rounded for clarity
  4024. coords$hover <- paste0(
  4025. "Cell: ", coords$cell,
  4026. "<br>x: ", round(coords$x_raw, 1),
  4027. ", y: ", round(coords$y_raw, 1)
  4028. )
  4029. # Prepare background tissue image as a base64-encoded PNG (if available and enabled)
  4030. base64_image <- NULL
  4031. img_width <- NULL
  4032. img_height <- NULL
  4033. if (overlay_image) {
  4034. # Only attempt to overlay image if compatible type and slot present
  4035. if (type == "visium" && "image" %in% slotNames(image_obj)) {
  4036. img_raster <- image_obj@image
  4037. } else if (
  4038. type == "vizgen" &&
  4039. "boundaries" %in% slotNames(image_obj) &&
  4040. "centroids" %in% slotNames(image_obj@boundaries) &&
  4041. "image" %in% slotNames(image_obj@boundaries$centroids)
  4042. ) {
  4043. img_raster <- image_obj@boundaries$centroids@image
  4044. }
  4045. # Convert the raster image array to base64 PNG (for embedding in plotly)
  4046. if (exists("img_raster")) {
  4047. img_width <- dim(img_raster)[2]
  4048. img_height <- dim(img_raster)[1]
  4049. temp_png <- tempfile(fileext = ".png")
  4050. png(temp_png, width = img_width, height = img_height)
  4051. grid::grid.raster(img_raster)
  4052. dev.off()
  4053. img_bytes <- readBin(temp_png, "raw", file.info(temp_png)$size)
  4054. base64_image <- paste0("data:image/png;base64,", base64enc::base64encode(img_bytes))
  4055. }
  4056. }
  4057. # Calculate custom axis tick positions and labels to show original coordinates
  4058. # This is necessary as points are downscaled to fit on the tissue image
  4059. # However, to best retain their original spatial orientation, we plot
  4060. # the original coordinate scale on the axis
  4061. create_axis_ticks <- function(scaled_coords, raw_coords, n_ticks = 6) {
  4062. # Get range of scaled and raw coordinates
  4063. scaled_range <- range(scaled_coords, na.rm = TRUE)
  4064. raw_range <- range(raw_coords, na.rm = TRUE)
  4065. # Create tick positions in the raw coordinate space
  4066. raw_ticks <- pretty(raw_range, n = n_ticks)
  4067. # Calculate corresponding scaled positions
  4068. # Linear interpolation from raw to scaled coordinates
  4069. scale_factor <- diff(scaled_range) / diff(raw_range)
  4070. scaled_ticks <- (raw_ticks - raw_range[1]) * scale_factor + scaled_range[1]
  4071. return(list(tickvals = scaled_ticks, ticktext = as.character(raw_ticks)))
  4072. }
  4073. # Create custom axis ticks for both x and y axes
  4074. x_ticks <- create_axis_ticks(coords$x, coords$x_raw)
  4075. y_ticks <- create_axis_ticks(coords$y, coords$y_raw)
  4076. # Set up the gadget UI with a plotly output area
  4077. ui <- miniPage(
  4078. gadgetTitleBar("Select a subset of cells"),
  4079. miniContentPanel(
  4080. plotly::plotlyOutput("plot", height = "100%"),
  4081. shiny::tags$div(
  4082. shiny::uiOutput("selection_count"),
  4083. style = "position:absolute; bottom:8px; right:10px; padding:4px 6px; background:rgba(255,255,255,0.8); font-size:12px; border-radius:3px; pointer-events:none;"
  4084. )
  4085. )
  4086. )
  4087. # Shiny gadget server logic for interactive plot and lasso selection
  4088. server <- function(input, output, session) {
  4089. current_selection <- shiny::reactiveVal(coords$cell)
  4090. # Render the interactive plotly scattergl plot
  4091. output$plot <- plotly::renderPlotly({
  4092. plt <- plotly::plot_ly(
  4093. data = coords,
  4094. x = ~x,
  4095. y = ~y,
  4096. color = ~group, # Color by group/cluster if available
  4097. key = ~cell, # Store cell names for selection retrieval
  4098. type = "scattergl", # Use WebGL for performance with large datasets
  4099. mode = "markers",
  4100. marker = list(size = 2 * pt.size.factor), # Default pt size is 2
  4101. text = ~hover, # Show hover info (cellid + coordinates)
  4102. hoverinfo = "text",
  4103. alpha = alpha # Global transparency
  4104. )
  4105. # Overlay the tissue image as background if available
  4106. if (!is.null(base64_image)) {
  4107. plt <- plt %>% plotly::layout(
  4108. images = list(
  4109. list(
  4110. source = base64_image,
  4111. xref = "x", yref = "y", # Anchor to data coordinates
  4112. x = 0,
  4113. y = 0,
  4114. sizex = img_width,
  4115. sizey = img_height,
  4116. sizing = "stretch",
  4117. opacity = 0.6,
  4118. layer = "below"
  4119. )
  4120. )
  4121. )
  4122. }
  4123. # Lock axes to same scale and reverse y for image alignment
  4124. # Set lasso mode and custom axis labels
  4125. plt <- plt %>% plotly::layout(
  4126. dragmode = "lasso",
  4127. yaxis = list(
  4128. autorange = "reversed",
  4129. scaleanchor = "x",
  4130. title = "y",
  4131. tickvals = y_ticks$tickvals,
  4132. ticktext = y_ticks$ticktext
  4133. ),
  4134. xaxis = list(
  4135. scaleanchor = "y",
  4136. title = "x",
  4137. tickvals = x_ticks$tickvals,
  4138. ticktext = x_ticks$ticktext
  4139. )
  4140. )
  4141. plt
  4142. })
  4143. observeEvent(plotly::event_data("plotly_selected"), {
  4144. selected <- plotly::event_data("plotly_selected")
  4145. if (is.null(selected) || NROW(selected) == 0) {
  4146. current_selection(NULL)
  4147. } else {
  4148. keys <- selected$key
  4149. keys <- keys[!is.na(keys)]
  4150. current_selection(keys)
  4151. }
  4152. }, ignoreInit = TRUE)
  4153. output$selection_count <- shiny::renderUI({
  4154. shiny::tags$span(paste0("Selected cells: ", NROW(current_selection())))
  4155. })
  4156. # When user clicks "Done", retrieve lasso selection and close gadget
  4157. observeEvent(input$done, {
  4158. stopApp(current_selection())
  4159. })
  4160. # When user clicks "Cancel", exit gadget and return NULL
  4161. observeEvent(input$cancel, {
  4162. stopApp(NULL)
  4163. })
  4164. }
  4165. # Launch the interactive gadget
  4166. runGadget(ui, server)
  4167. }
  4168. #' Visualize spatial clustering and expression data.
  4169. #'
  4170. #' SpatialPlot plots a feature or discrete grouping (e.g. cluster assignments) as
  4171. #' spots over the image that was collected. We also provide SpatialFeaturePlot
  4172. #' and SpatialDimPlot as wrapper functions around SpatialPlot for a consistent
  4173. #' naming framework.
  4174. #'
  4175. #' @inheritParams HoverLocator
  4176. #' @param object A Seurat object
  4177. #' @param group.by Name of meta.data column to group the data by
  4178. #' @param features Name of the feature to visualize. Provide either group.by OR
  4179. #' features, not both.
  4180. #' @param images Name of the images to use in the plot(s)
  4181. #' @param cols Vector of colors, each color corresponds to an identity class.
  4182. #' This may also be a single character or numeric value corresponding to a
  4183. #' palette as specified by \code{\link[RColorBrewer]{brewer.pal.info}}. By
  4184. #' default, ggplot2 assigns colors
  4185. #' @param image.alpha Adjust the opacity of the background images. Set to 0 to
  4186. #' remove.
  4187. #' @param image.scale Choose the scale factor ("lowres"/"hires") to apply in
  4188. #' order to matchthe plot with the specified `image` - defaults to "lowres"
  4189. #' @param crop Crop the plot in to focus on points plotted. Set to \code{FALSE} to show
  4190. #' entire background image.
  4191. #' @param slot If plotting a feature, which data slot to pull from (counts,
  4192. #' data, or scale.data)
  4193. #' @param keep.scale How to handle the color scale across multiple plots. Options are:
  4194. #' \itemize{
  4195. #' \item \dQuote{feature} (default; by row/feature scaling): The plots for
  4196. #' each individual feature are scaled to the maximum expression of the
  4197. #' feature across the conditions provided to \code{split.by}
  4198. #' \item \dQuote{all} (universal scaling): The plots for all features and
  4199. #' conditions are scaled to the maximum expression value for the feature
  4200. #' with the highest overall expression
  4201. #' \item \code{NULL} (no scaling): Each individual plot is scaled to the
  4202. #' maximum expression value of the feature in the condition provided to
  4203. #' \code{split.by}; be aware setting \code{NULL} will result in color
  4204. #' scales that are not comparable between plots
  4205. #' }
  4206. #' @param min.cutoff,max.cutoff Vector of minimum and maximum cutoff
  4207. #' values for each feature, may specify quantile in the form of 'q##' where '##'
  4208. #' is the quantile (eg, 'q1', 'q10')
  4209. #' @param cells.highlight A list of character or numeric vectors of cells to
  4210. #' highlight. If only one group of cells desired, can simply pass a vector
  4211. #' instead of a list. If set, colors selected cells to the color(s) in
  4212. #' cols.highlight
  4213. #' @param cols.highlight A vector of colors to highlight the cells as; ordered
  4214. #' the same as the groups in cells.highlight; last color corresponds to
  4215. #' unselected cells.
  4216. #' @param facet.highlight When highlighting certain groups of cells, split each
  4217. #' group into its own plot
  4218. #' @param label Whether to label the clusters
  4219. #' @param label.size Sets the size of the labels
  4220. #' @param label.color Sets the color of the label text
  4221. #' @param label.box Whether to put a box around the label text (geom_text vs
  4222. #' geom_label)
  4223. #' @param repel Repels the labels to prevent overlap
  4224. #' @param ncol Number of columns if plotting multiple plots
  4225. #' @param combine Combine plots into a single gg object; note that if TRUE;
  4226. #' themeing will not work when plotting multiple features/groupings
  4227. #' @param pt.size.factor Scale the size of the spots.
  4228. #' @param alpha Controls opacity of spots. Provide as a vector specifying the
  4229. #' min and max for SpatialFeaturePlot. For SpatialDimPlot, provide a single
  4230. #' alpha value for each plot.
  4231. #' @param shape Control the shape of the spots - same as the ggplot2 parameter.
  4232. #' The default is 21, which plots circles - use 22 to plot squares.
  4233. #' @param stroke Control the width of the border around the spots
  4234. #' @param stroke.alpha Control the opacity of spot borders (when stroke is specified).
  4235. #' Set to \code{NA} to use the same alpha as the fill.
  4236. #' @param interactive Launch an interactive SpatialDimPlot or SpatialFeaturePlot
  4237. #' session, see \code{\link{ISpatialDimPlot}} or
  4238. #' \code{\link{ISpatialFeaturePlot}} for more details
  4239. #' @param do.identify,do.hover DEPRECATED in favor of \code{interactive}
  4240. #' @param identify.ident DEPRECATED
  4241. #' @param plot_segmentations Define whether plot should plot centroids or segmentations
  4242. #'
  4243. #' @return If \code{do.identify}, either a vector of cells selected or the object
  4244. #' with selected cells set to the value of \code{identify.ident} (if set). Else,
  4245. #' if \code{do.hover}, a plotly object with interactive graphics. Else, a ggplot
  4246. #' object
  4247. #'
  4248. #' @importFrom ggplot2 scale_fill_gradientn ggtitle theme element_text scale_alpha
  4249. #' @importFrom patchwork wrap_plots
  4250. #' @export
  4251. #' @concept visualization
  4252. #' @concept spatial
  4253. #'
  4254. #' @examples
  4255. #' \dontrun{
  4256. #' # For functionality analagous to FeaturePlot
  4257. #' SpatialPlot(seurat.object, features = "MS4A1")
  4258. #' SpatialFeaturePlot(seurat.object, features = "MS4A1")
  4259. #'
  4260. #' # For functionality analagous to DimPlot
  4261. #' SpatialPlot(seurat.object, group.by = "clusters")
  4262. #' SpatialDimPlot(seurat.object, group.by = "clusters")
  4263. #' }
  4264. #'
  4265. SpatialPlot <- function(
  4266. object,
  4267. group.by = NULL,
  4268. features = NULL,
  4269. images = NULL,
  4270. cols = NULL,
  4271. image.alpha = 1,
  4272. image.scale = "lowres",
  4273. crop = TRUE,
  4274. slot = 'data',
  4275. keep.scale = "feature",
  4276. min.cutoff = NA,
  4277. max.cutoff = NA,
  4278. cells.highlight = NULL,
  4279. cols.highlight = c('#DE2D26', 'grey50'),
  4280. facet.highlight = FALSE,
  4281. label = FALSE,
  4282. label.size = 5,
  4283. label.color = 'white',
  4284. label.box = TRUE,
  4285. repel = FALSE,
  4286. ncol = NULL,
  4287. combine = TRUE,
  4288. pt.size.factor = 1.6,
  4289. alpha = c(1, 1),
  4290. shape = 21,
  4291. stroke = NA,
  4292. stroke.alpha = NA,
  4293. interactive = FALSE,
  4294. do.identify = FALSE,
  4295. identify.ident = NULL,
  4296. do.hover = FALSE,
  4297. information = NULL,
  4298. plot_segmentations = FALSE
  4299. ) {
  4300. if (isTRUE(x = do.hover) || isTRUE(x = do.identify)) {
  4301. warning(
  4302. "'do.hover' and 'do.identify' are deprecated as we are removing plotly-based interactive graphics, use 'interactive' instead for Shiny-based interactivity",
  4303. call. = FALSE,
  4304. immediate. = TRUE
  4305. )
  4306. interactive <- TRUE
  4307. }
  4308. if (!is.null(x = group.by) & !is.null(x = features)) {
  4309. stop("Please specific either group.by or features, not both.")
  4310. }
  4311. images <- images %||% Images(object = object, assay = DefaultAssay(object = object))
  4312. if (length(x = images) == 0) {
  4313. images <- Images(object = object)
  4314. }
  4315. if (length(x = images) < 1) {
  4316. stop("Could not find any spatial image information")
  4317. }
  4318. # Check keep.scale param for valid entries
  4319. if (!(is.null(x = keep.scale)) && !(keep.scale %in% c("feature", "all"))) {
  4320. stop("`keep.scale` must be set to either `feature`, `all`, or NULL")
  4321. }
  4322. cells <- unique(CellsByImage(object, images = images, unlist = TRUE))
  4323. if (is.null(x = features)) {
  4324. if (interactive) {
  4325. # default alpha is 1 but interactive plotting requires
  4326. # a range for proper cluster selection highlighting
  4327. if (identical(alpha, c(1, 1))) {
  4328. alpha <- c(0.1, 1)
  4329. }
  4330. tryCatch(
  4331. expr = {
  4332. return(ISpatialDimPlot(
  4333. object = object,
  4334. image = images[1],
  4335. image.scale = image.scale,
  4336. group.by = group.by,
  4337. alpha = alpha
  4338. ))
  4339. },
  4340. error = function(e) {
  4341. # error can occur when image and assay don't match
  4342. # or when the default assay set doesn't have data corresponding to the default ident etc.
  4343. if (grepl("arguments imply differing number of rows", conditionMessage(e))) {
  4344. stop(
  4345. "Cells were removed due to missing data; check if the specified image and assay are correct.\n",
  4346. call. = FALSE
  4347. )
  4348. } else {
  4349. stop(e)
  4350. }
  4351. }
  4352. )
  4353. }
  4354. group.by <- group.by %||% 'ident'
  4355. object[['ident']] <- Idents(object = object)
  4356. data <- object[[group.by]]
  4357. data <- data[cells,,drop=F]
  4358. for (group in group.by) {
  4359. if (!is.factor(x = data[, group])) {
  4360. data[, group] <- factor(x = data[, group])
  4361. }
  4362. }
  4363. } else {
  4364. if (interactive) {
  4365. return(ISpatialFeaturePlot(
  4366. object = object,
  4367. feature = features[1],
  4368. image = images[1],
  4369. image.scale = image.scale,
  4370. slot = slot,
  4371. alpha = alpha
  4372. ))
  4373. }
  4374. data <- FetchData(
  4375. object = object,
  4376. vars = features,
  4377. cells = cells,
  4378. layer = slot,
  4379. clean = FALSE
  4380. )
  4381. features <- colnames(x = data)
  4382. # Determine cutoffs
  4383. min.cutoff <- mapply(
  4384. FUN = function(cutoff, feature) {
  4385. return(ifelse(
  4386. test = is.na(x = cutoff),
  4387. yes = min(data[, feature]),
  4388. no = cutoff
  4389. ))
  4390. },
  4391. cutoff = min.cutoff,
  4392. feature = features
  4393. )
  4394. max.cutoff <- mapply(
  4395. FUN = function(cutoff, feature) {
  4396. return(ifelse(
  4397. test = is.na(x = cutoff),
  4398. yes = max(data[, feature]),
  4399. no = cutoff
  4400. ))
  4401. },
  4402. cutoff = max.cutoff,
  4403. feature = features
  4404. )
  4405. check.lengths <- unique(x = vapply(
  4406. X = list(features, min.cutoff, max.cutoff),
  4407. FUN = length,
  4408. FUN.VALUE = numeric(length = 1)
  4409. ))
  4410. if (length(x = check.lengths) != 1) {
  4411. stop("There must be the same number of minimum and maximum cuttoffs as there are features")
  4412. }
  4413. # Apply cutoffs
  4414. data <- sapply(
  4415. X = 1:ncol(x = data),
  4416. FUN = function(index) {
  4417. data.feature <- as.vector(x = data[, index])
  4418. min.use <- SetQuantile(cutoff = min.cutoff[index], data.feature)
  4419. max.use <- SetQuantile(cutoff = max.cutoff[index], data.feature)
  4420. data.feature[data.feature < min.use] <- min.use
  4421. data.feature[data.feature > max.use] <- max.use
  4422. return(data.feature)
  4423. }
  4424. )
  4425. colnames(x = data) <- features
  4426. rownames(x = data) <- cells
  4427. }
  4428. features <- colnames(x = data)
  4429. colnames(x = data) <- features
  4430. rownames(x = data) <- cells
  4431. facet.highlight <- facet.highlight && (!is.null(x = cells.highlight) && is.list(x = cells.highlight))
  4432. if (do.hover) {
  4433. if (length(x = images) > 1) {
  4434. images <- images[1]
  4435. warning(
  4436. "'do.hover' requires only one image, using image ",
  4437. images,
  4438. call. = FALSE,
  4439. immediate. = TRUE
  4440. )
  4441. }
  4442. if (length(x = features) > 1) {
  4443. features <- features[1]
  4444. type <- ifelse(test = is.null(x = group.by), yes = 'feature', no = 'grouping')
  4445. warning(
  4446. "'do.hover' requires only one ",
  4447. type,
  4448. ", using ",
  4449. features,
  4450. call. = FALSE,
  4451. immediate. = TRUE
  4452. )
  4453. }
  4454. if (facet.highlight) {
  4455. warning(
  4456. "'do.hover' requires no faceting highlighted cells",
  4457. call. = FALSE,
  4458. immediate. = TRUE
  4459. )
  4460. facet.highlight <- FALSE
  4461. }
  4462. }
  4463. if (facet.highlight) {
  4464. if (length(x = images) > 1) {
  4465. images <- images[1]
  4466. warning(
  4467. "Faceting the highlight only works with a single image, using image ",
  4468. images,
  4469. call. = FALSE,
  4470. immediate. = TRUE
  4471. )
  4472. }
  4473. ncols <- length(x = cells.highlight)
  4474. } else {
  4475. ncols <- length(x = images)
  4476. }
  4477. plots <- vector(
  4478. mode = "list",
  4479. length = length(x = features) * ncols
  4480. )
  4481. # Get max across all features
  4482. if (!(is.null(x = keep.scale)) && keep.scale == "all") {
  4483. max.feature.value <- max(apply(data, 2, function(x) max(x, na.rm = TRUE)))
  4484. }
  4485. for (i in 1:ncols) {
  4486. plot.idx <- i
  4487. image.idx <- ifelse(test = facet.highlight, yes = 1, no = i)
  4488. image.use <- object[[images[[image.idx]]]]
  4489. is_visium_v2 <- inherits(image.use, "VisiumV2")
  4490. old_axis_orientation <- (!.hasSlot(image.use, "coords_x_orientation")) || (.hasSlot(image.use, "coords_x_orientation") && (slot(image.use, "coords_x_orientation") != 'horizontal'))
  4491. if (is_visium_v2 && old_axis_orientation) {
  4492. stop(
  4493. "Please run `UpdateSeuratObject` on your Seurat object first to ensure that data aligns to the image ", images[[image.idx]], " when plotting.",
  4494. call. = TRUE
  4495. )
  4496. }
  4497. # When plotting segmentations, set the default boundary (temporarily) to segmentations
  4498. if (plot_segmentations == TRUE && inherits(image.use, "VisiumV2") &&
  4499. "segmentations" %in% names(image.use)) {
  4500. db <- DefaultBoundary(image.use)
  4501. on.exit(DefaultBoundary(image.use) <- db, add = TRUE) # Reset on exit
  4502. DefaultBoundary(image.use) <- "segmentations"
  4503. }
  4504. coordinates <- GetTissueCoordinates(object = image.use, scale = image.scale)
  4505. highlight.use <- if (facet.highlight) {
  4506. cells.highlight[i]
  4507. } else {
  4508. cells.highlight
  4509. }
  4510. for (j in seq_along(features)) {
  4511. cols.unset <- is.factor(x = data[, features[j]]) && is.null(x = cols)
  4512. if (cols.unset) {
  4513. cols <- hue_pal()(n = length(x = levels(x = data[, features[j]])))
  4514. names(x = cols) <- levels(x = data[, features[j]])
  4515. }
  4516. # Get feature max for individual feature
  4517. if (!(is.null(x = keep.scale)) && keep.scale == "feature" && !inherits(x = data[, features[j]], what = "factor") ) {
  4518. max.feature.value <- max(data[, features[j]])
  4519. }
  4520. # Check if object is of type Visium and contains segmentations
  4521. has_visium_segm_data <- inherits(image.use, "VisiumV2") &&
  4522. !is.null(image.use@boundaries$segmentations) &&
  4523. "sf.data" %in% slotNames(image.use@boundaries$segmentations)
  4524. # GetTissueCoordinates will not always return a "cell" column (e.g., Visium V1)
  4525. if (!("cell" %in% colnames(x = coordinates))) {
  4526. coordinates$cell <- rownames(x = coordinates)
  4527. }
  4528. idx <- match(coordinates$cell, rownames(x = data))
  4529. plot.data <- cbind(coordinates, data[idx, features[j], drop = FALSE])
  4530. plot <- SingleSpatialPlot(
  4531. data = plot.data,
  4532. image = image.use,
  4533. image.scale = image.scale,
  4534. image.alpha = image.alpha,
  4535. col.by = features[j],
  4536. cols = cols,
  4537. alpha.by = if (is.null(x = group.by)) {
  4538. features[j]
  4539. } else {
  4540. NULL
  4541. },
  4542. pt.alpha = if (!is.null(x = group.by)) {
  4543. alpha[j]
  4544. } else {
  4545. NULL
  4546. },
  4547. geom = if (inherits(x = image.use, what = "STARmap")) {
  4548. "poly_starmap"
  4549. } else if (has_visium_segm_data && plot_segmentations) {
  4550. "poly"
  4551. } else {
  4552. "spatial"
  4553. },
  4554. cells.highlight = highlight.use,
  4555. cols.highlight = cols.highlight,
  4556. pt.size.factor = pt.size.factor,
  4557. shape = shape,
  4558. stroke = stroke,
  4559. stroke.alpha = stroke.alpha,
  4560. crop = crop
  4561. )
  4562. if (is.null(x = group.by)) {
  4563. plot <- plot +
  4564. scale_fill_gradientn(
  4565. name = features[j],
  4566. colours = SpatialColors(n = 100)
  4567. ) +
  4568. theme(legend.position = 'top') +
  4569. scale_alpha(range = alpha) +
  4570. guides(alpha = "none")
  4571. } else if (label) {
  4572. plot <- LabelClusters(
  4573. plot = plot,
  4574. id = ifelse(
  4575. test = is.null(x = cells.highlight),
  4576. yes = features[j],
  4577. no = 'highlight'
  4578. ),
  4579. geom = if (inherits(x = image.use, what = "STARmap") || (has_visium_segm_data && plot_segmentations)) {
  4580. 'GeomPolygon'
  4581. } else {
  4582. 'GeomSpatial'
  4583. },
  4584. repel = repel,
  4585. size = label.size,
  4586. color = label.color,
  4587. box = label.box,
  4588. position = "nearest"
  4589. )
  4590. }
  4591. if (j == 1 && length(x = images) > 1 && !facet.highlight) {
  4592. plot <- plot +
  4593. ggtitle(label = images[[image.idx]]) +
  4594. theme(plot.title = element_text(hjust = 0.5))
  4595. }
  4596. if (facet.highlight) {
  4597. plot <- plot +
  4598. ggtitle(label = names(x = cells.highlight)[i]) +
  4599. theme(plot.title = element_text(hjust = 0.5)) +
  4600. NoLegend()
  4601. }
  4602. if (has_visium_segm_data && plot_segmentations && !is.null(group.by)) {
  4603. # Add legend guides to show filled squares next to labels when plotting segmentations
  4604. plot <- plot + guides(fill = guide_legend(override.aes = list(alpha = 1, color = "black", linewidth = 0.2, size = 2)))
  4605. }
  4606. # Plot multiple images depending on keep.scale
  4607. if (!(is.null(x = keep.scale)) && !inherits(x = data[, features[j]], "factor")) {
  4608. plot <- suppressMessages(plot & scale_fill_gradientn(colors = SpatialColors(n = 100), limits = c(NA, max.feature.value)))
  4609. }
  4610. plots[[plot.idx]] <- plot
  4611. plot.idx <- plot.idx + ncols
  4612. if (cols.unset) {
  4613. cols <- NULL
  4614. }
  4615. }
  4616. }
  4617. if (combine) {
  4618. if (!is.null(x = ncol)) {
  4619. return(wrap_plots(plots = plots, ncol = ncol))
  4620. }
  4621. if (length(x = images) > 1) {
  4622. return(wrap_plots(plots = plots, ncol = length(x = images)))
  4623. }
  4624. return(wrap_plots(plots = plots))
  4625. }
  4626. return(plots)
  4627. }
  4628. #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  4629. # Other plotting functions
  4630. #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  4631. #' Plot the Barcode Distribution and Calculated Inflection Points
  4632. #'
  4633. #' This function plots the calculated inflection points derived from the barcode-rank
  4634. #' distribution.
  4635. #'
  4636. #' See [CalculateBarcodeInflections()] to calculate inflection points and
  4637. #' [SubsetByBarcodeInflections()] to subsequently subset the Seurat object.
  4638. #'
  4639. #' @param object Seurat object
  4640. #'
  4641. #' @return Returns a `ggplot2` object showing the by-group inflection points and provided
  4642. #' (or default) rank threshold values in grey.
  4643. #'
  4644. #' @importFrom methods slot
  4645. #' @importFrom cowplot theme_cowplot
  4646. #' @importFrom ggplot2 ggplot geom_line geom_vline
  4647. #'
  4648. #' @export
  4649. #' @concept visualization
  4650. #'
  4651. #' @author Robert A. Amezquita, \email{[email hidden]}
  4652. #' @seealso \code{\link{CalculateBarcodeInflections}} \code{\link{SubsetByBarcodeInflections}}
  4653. #'
  4654. #' @examples
  4655. #' data("pbmc_small")
  4656. #' pbmc_small <- CalculateBarcodeInflections(pbmc_small, group.column = 'groups')
  4657. #' BarcodeInflectionsPlot(pbmc_small)
  4658. #'
  4659. BarcodeInflectionsPlot <- function(object) {
  4660. cbi.data <- Tool(object = object, slot = 'CalculateBarcodeInflections')
  4661. if (is.null(x = cbi.data)) {
  4662. stop("Barcode inflections not calculated, please run CalculateBarcodeInflections")
  4663. }
  4664. ## Extract necessary data frames
  4665. inflection_points <- cbi.data$inflection_points
  4666. barcode_distribution <- cbi.data$barcode_distribution
  4667. threshold_values <- cbi.data$threshold_values
  4668. # Set a cap to max rank to avoid plot being overextended
  4669. if (threshold_values$rank[[2]] > max(barcode_distribution$rank, na.rm = TRUE)) {
  4670. threshold_values$rank[[2]] <- max(barcode_distribution$rank, na.rm = TRUE)
  4671. }
  4672. ## Infer the grouping/barcode variables
  4673. group_var <- colnames(x = barcode_distribution)[1]
  4674. barcode_var <- colnames(x = barcode_distribution)[2]
  4675. barcode_distribution[, barcode_var] <- log10(x = barcode_distribution[, barcode_var] + 1)
  4676. ## Make the plot
  4677. plot <- ggplot(
  4678. data = barcode_distribution,
  4679. mapping = aes(
  4680. x = .data[['rank']],
  4681. y = .data[[barcode_var]],
  4682. group = .data[[group_var]],
  4683. colour = .data[[group_var]]
  4684. )
  4685. ) +
  4686. geom_line() +
  4687. geom_vline(
  4688. data = threshold_values,
  4689. aes(xintercept = .data[['rank']]),
  4690. linetype = "dashed",
  4691. colour = 'grey60',
  4692. size = 0.5
  4693. ) +
  4694. geom_vline(
  4695. data = inflection_points,
  4696. mapping = aes(
  4697. xintercept = .data[['rank']],
  4698. group = .data[[group_var]],
  4699. colour = .data[[group_var]]
  4700. ),
  4701. linetype = "dashed"
  4702. ) +
  4703. theme_cowplot()
  4704. return(plot)
  4705. }
  4706. #' Dot plot visualization
  4707. #'
  4708. #' Intuitive way of visualizing how feature expression changes across different
  4709. #' identity classes (clusters). The size of the dot encodes the percentage of
  4710. #' cells within a class, while the color encodes the AverageExpression level
  4711. #' across all cells within a class (blue is high).
  4712. #'
  4713. #' @param object Seurat object
  4714. #' @param assay Name of assay to use, defaults to the active assay
  4715. #' @param features Input vector of features, or named list of feature vectors
  4716. #' if feature-grouped panels are desired (replicates the functionality of the
  4717. #' old SplitDotPlotGG)
  4718. #' @param cols Colors to plot: the name of a palette from
  4719. #' \code{RColorBrewer::brewer.pal.info}, a pair of colors defining a gradient,
  4720. #' or 3+ colors defining multiple gradients (if split.by is set)
  4721. #' @param col.min Minimum scaled average expression threshold (everything
  4722. #' smaller will be set to this)
  4723. #' @param col.max Maximum scaled average expression threshold (everything larger
  4724. #' will be set to this)
  4725. #' @param dot.min The fraction of cells at which to draw the smallest dot
  4726. #' (default is 0). All cell groups with less than this expressing the given
  4727. #' gene will have no dot drawn.
  4728. #' @param dot.scale Scale the size of the points, similar to cex
  4729. #' @param idents Identity classes to include in plot (default is all)
  4730. #' @param group.by Factor to group the cells by
  4731. #' @param split.by A factor in object metadata to split the plot by, pass 'ident'
  4732. #' to split by cell identity
  4733. #' see \code{\link{FetchData}} for more details
  4734. #' @param cluster.idents Whether to order identities by hierarchical clusters
  4735. #' based on given features, default is FALSE
  4736. #' @param scale Determine whether the data is scaled, TRUE for default
  4737. #' @param scale.by Scale the size of the points by 'size' or by 'radius'
  4738. #' @param scale.min Set lower limit for scaling, use NA for default
  4739. #' @param scale.max Set upper limit for scaling, use NA for default
  4740. #'
  4741. #' @return A ggplot object
  4742. #'
  4743. #' @importFrom grDevices colorRampPalette
  4744. #' @importFrom cowplot theme_cowplot
  4745. #' @importFrom ggplot2 ggplot geom_point scale_size scale_radius
  4746. #' theme element_blank labs scale_color_identity scale_color_distiller
  4747. #' scale_color_gradient guides guide_legend guide_colorbar
  4748. #' facet_grid unit
  4749. #' @importFrom scattermore geom_scattermore
  4750. #' @importFrom stats dist hclust
  4751. #' @importFrom RColorBrewer brewer.pal.info
  4752. #'
  4753. #' @export
  4754. #' @concept visualization
  4755. #'
  4756. #' @aliases SplitDotPlotGG
  4757. #' @seealso \code{RColorBrewer::brewer.pal.info}
  4758. #'
  4759. #' @examples
  4760. #' data("pbmc_small")
  4761. #' cd_genes <- c("CD247", "CD3E", "CD9")
  4762. #' DotPlot(object = pbmc_small, features = cd_genes)
  4763. #' pbmc_small[['groups']] <- sample(x = c('g1', 'g2'), size = ncol(x = pbmc_small), replace = TRUE)
  4764. #' DotPlot(object = pbmc_small, features = cd_genes, split.by = 'groups')
  4765. #'
  4766. DotPlot <- function(
  4767. object,
  4768. features,
  4769. assay = NULL,
  4770. cols = c("lightgrey", "blue"),
  4771. col.min = -2.5,
  4772. col.max = 2.5,
  4773. dot.min = 0,
  4774. dot.scale = 6,
  4775. idents = NULL,
  4776. group.by = NULL,
  4777. split.by = NULL,
  4778. cluster.idents = FALSE,
  4779. scale = TRUE,
  4780. scale.by = 'radius',
  4781. scale.min = NA,
  4782. scale.max = NA
  4783. ) {
  4784. assay <- assay %||% DefaultAssay(object = object)
  4785. DefaultAssay(object = object) <- assay
  4786. split.colors <- !is.null(x = split.by) && !any(cols %in% rownames(x = brewer.pal.info))
  4787. scale.func <- switch(
  4788. EXPR = scale.by,
  4789. 'size' = scale_size,
  4790. 'radius' = scale_radius,
  4791. stop("'scale.by' must be either 'size' or 'radius'")
  4792. )
  4793. feature.groups <- NULL
  4794. if (is.list(features) | any(!is.na(names(features)))) {
  4795. feature.groups <- unlist(x = sapply(
  4796. X = 1:length(features),
  4797. FUN = function(x) {
  4798. return(rep(x = names(x = features)[x], each = length(features[[x]])))
  4799. }
  4800. ))
  4801. if (any(is.na(x = feature.groups))) {
  4802. warning(
  4803. "Some feature groups are unnamed.",
  4804. call. = FALSE,
  4805. immediate. = TRUE
  4806. )
  4807. }
  4808. features <- unlist(x = features)
  4809. names(x = feature.groups) <- features
  4810. }
  4811. cells <- unlist(x = CellsByIdentities(object = object, cells = colnames(object[[assay]]), idents = idents))
  4812. data.features <- FetchData(object = object, vars = features, cells = cells)
  4813. data.features$id <- if (is.null(x = group.by)) {
  4814. Idents(object = object)[cells, drop = TRUE]
  4815. } else {
  4816. object[[group.by, drop = TRUE]][cells, drop = TRUE]
  4817. }
  4818. if (!is.factor(x = data.features$id)) {
  4819. data.features$id <- factor(x = data.features$id)
  4820. }
  4821. id.levels <- levels(x = data.features$id)
  4822. data.features$id <- as.vector(x = data.features$id)
  4823. if (!is.null(x = split.by)) {
  4824. splits <- FetchData(object = object, vars = split.by)[cells, split.by]
  4825. if (split.colors) {
  4826. if (length(x = unique(x = splits)) > length(x = cols)) {
  4827. stop(paste0("Need to specify at least ", length(x = unique(x = splits)), " colors using the cols parameter"))
  4828. }
  4829. cols <- cols[1:length(x = unique(x = splits))]
  4830. names(x = cols) <- unique(x = splits)
  4831. }
  4832. data.features$id <- paste(data.features$id, splits, sep = '_')
  4833. unique.splits <- unique(x = splits)
  4834. id.levels <- paste0(rep(x = id.levels, each = length(x = unique.splits)), "_", rep(x = unique(x = splits), times = length(x = id.levels)))
  4835. }
  4836. data.plot <- lapply(
  4837. X = unique(x = data.features$id),
  4838. FUN = function(ident) {
  4839. data.use <- data.features[data.features$id == ident, 1:(ncol(x = data.features) - 1), drop = FALSE]
  4840. avg.exp <- apply(
  4841. X = data.use,
  4842. MARGIN = 2,
  4843. FUN = function(x) {
  4844. return(mean(x = expm1(x = x)))
  4845. }
  4846. )
  4847. pct.exp <- apply(X = data.use, MARGIN = 2, FUN = PercentAbove, threshold = 0)
  4848. return(list(avg.exp = avg.exp, pct.exp = pct.exp))
  4849. }
  4850. )
  4851. names(x = data.plot) <- unique(x = data.features$id)
  4852. if (cluster.idents) {
  4853. mat <- do.call(
  4854. what = rbind,
  4855. args = lapply(X = data.plot, FUN = unlist)
  4856. )
  4857. mat <- scale(x = mat)
  4858. id.levels <- id.levels[hclust(d = dist(x = mat))$order]
  4859. }
  4860. data.plot <- lapply(
  4861. X = names(x = data.plot),
  4862. FUN = function(x) {
  4863. data.use <- as.data.frame(x = data.plot[[x]])
  4864. data.use$features.plot <- rownames(x = data.use)
  4865. data.use$id <- x
  4866. return(data.use)
  4867. }
  4868. )
  4869. data.plot <- do.call(what = 'rbind', args = data.plot)
  4870. if (!is.null(x = id.levels)) {
  4871. data.plot$id <- factor(x = data.plot$id, levels = id.levels)
  4872. }
  4873. ngroup <- length(x = levels(x = data.plot$id))
  4874. if (ngroup == 1) {
  4875. scale <- FALSE
  4876. warning(
  4877. "Only one identity present, the expression values will be not scaled",
  4878. call. = FALSE,
  4879. immediate. = TRUE
  4880. )
  4881. } else if (ngroup < 5 & scale) {
  4882. warning(
  4883. "Scaling data with a low number of groups may produce misleading results",
  4884. call. = FALSE,
  4885. immediate. = TRUE
  4886. )
  4887. }
  4888. avg.exp.scaled <- sapply(
  4889. X = unique(x = data.plot$features.plot),
  4890. FUN = function(x) {
  4891. data.use <- data.plot[data.plot$features.plot == x, 'avg.exp']
  4892. if (scale) {
  4893. data.use <- scale(x = log1p(data.use))
  4894. data.use <- MinMax(data = data.use, min = col.min, max = col.max)
  4895. } else {
  4896. data.use <- log1p(x = data.use)
  4897. }
  4898. return(data.use)
  4899. }
  4900. )
  4901. avg.exp.scaled <- as.vector(x = t(x = avg.exp.scaled))
  4902. if (split.colors) {
  4903. avg.exp.scaled <- as.numeric(x = cut(x = avg.exp.scaled, breaks = 20))
  4904. }
  4905. data.plot$avg.exp.scaled <- avg.exp.scaled
  4906. data.plot$features.plot <- factor(
  4907. x = data.plot$features.plot,
  4908. levels = features
  4909. )
  4910. data.plot$pct.exp[data.plot$pct.exp < dot.min] <- NA
  4911. data.plot$pct.exp <- data.plot$pct.exp * 100
  4912. if (split.colors) {
  4913. splits.use <- unlist(x = lapply(
  4914. X = data.plot$id,
  4915. FUN = function(x)
  4916. sub(
  4917. paste0(".*_(",
  4918. paste(sort(unique(x = splits), decreasing = TRUE),
  4919. collapse = '|'
  4920. ),")$"),
  4921. "\\1",
  4922. x
  4923. )
  4924. )
  4925. )
  4926. data.plot$colors <- mapply(
  4927. FUN = function(color, value) {
  4928. return(colorRampPalette(colors = c('grey', color))(20)[value])
  4929. },
  4930. color = cols[splits.use],
  4931. value = avg.exp.scaled
  4932. )
  4933. }
  4934. color.by <- ifelse(test = split.colors, yes = 'colors', no = 'avg.exp.scaled')
  4935. if (!is.na(x = scale.min)) {
  4936. data.plot[data.plot$pct.exp < scale.min, 'pct.exp'] <- scale.min
  4937. }
  4938. if (!is.na(x = scale.max)) {
  4939. data.plot[data.plot$pct.exp > scale.max, 'pct.exp'] <- scale.max
  4940. }
  4941. if (!is.null(x = feature.groups)) {
  4942. data.plot$feature.groups <- factor(
  4943. x = feature.groups[data.plot$features.plot],
  4944. levels = unique(x = feature.groups)
  4945. )
  4946. }
  4947. plot <- ggplot(data = data.plot, mapping = aes(x = .data[["features.plot"]], y = .data[["id"]])) +
  4948. geom_point(mapping = aes(size = .data[["pct.exp"]], color = .data[[color.by]])) +
  4949. scale.func(range = c(0, dot.scale), limits = c(scale.min, scale.max)) +
  4950. theme(axis.title.x = element_blank(), axis.title.y = element_blank()) +
  4951. guides(size = guide_legend(title = 'Percent Expressed')) +
  4952. labs(
  4953. x = 'Features',
  4954. y = ifelse(test = is.null(x = split.by), yes = 'Identity', no = 'Split Identity')
  4955. ) +
  4956. theme_cowplot()
  4957. if (!is.null(x = feature.groups)) {
  4958. plot <- plot + facet_grid(
  4959. rows = ~feature.groups,
  4960. scales = "free_x",
  4961. space = "free_x",
  4962. switch = "y"
  4963. ) + theme(
  4964. panel.spacing = unit(x = 1, units = "lines"),
  4965. strip.background = element_blank()
  4966. )
  4967. }
  4968. if (split.colors) {
  4969. plot <- plot + scale_color_identity()
  4970. } else if (length(x = cols) == 1) {
  4971. plot <- plot + scale_color_distiller(palette = cols)
  4972. } else {
  4973. plot <- plot + scale_color_gradient(low = cols[1], high = cols[2])
  4974. }
  4975. if (!split.colors) {
  4976. plot <- plot + guides(color = guide_colorbar(title = 'Average Expression'))
  4977. }
  4978. return(plot)
  4979. }
  4980. #' Quickly Pick Relevant Dimensions
  4981. #'
  4982. #' Plots per-component standard deviations (or approximate singular values if running PCAFast),
  4983. #' percent variance explained per principal component, or cumulative percent variance explained,
  4984. #' to help pick an elbow in the graph. This elbow often corresponds well with significant
  4985. #' dimensions and is much faster to run than Jackstraw.
  4986. #'
  4987. #' @param object Seurat object
  4988. #' @param ndims Number of dimensions to plot (positive integer; capped by stored components)
  4989. #' @param reduction Reduction technique to plot (default is 'pca')
  4990. #' @param plot_type One of \code{"stdev"} (default), \code{"variance"} (per-PC \% variance), or
  4991. #' \code{"cumulative_variance"} (running sum of those percentages; equals 100\% at the last
  4992. #' stored PC when \code{ndims} spans all of them)
  4993. #'
  4994. #' @return A ggplot object
  4995. #'
  4996. #' @importFrom cowplot theme_cowplot
  4997. #' @importFrom ggplot2 ggplot geom_point labs aes
  4998. #' @export
  4999. #' @concept visualization
  5000. #'
  5001. #' @examples
  5002. #' data("pbmc_small")
  5003. #' ElbowPlot(object = pbmc_small)
  5004. #' ElbowPlot(object = pbmc_small, plot_type = "variance")
  5005. #' ElbowPlot(object = pbmc_small, plot_type = "cumulative_variance")
  5006. #'
  5007. ElbowPlot <- function(object, ndims = 20, reduction = 'pca', plot_type = c("stdev", "variance", "cumulative_variance")) {
  5008. plot_type <- match.arg(plot_type)
  5009. if (!is.numeric(ndims) || length(ndims) != 1L || !is.finite(ndims) || ndims < 1 || ndims != as.integer(ndims)) {
  5010. stop("'ndims' must be a single positive integer", call. = FALSE)
  5011. }
  5012. ndims <- as.integer(ndims)
  5013. data.use <- Stdev(object = object, reduction = reduction)
  5014. if (length(x = data.use) == 0) {
  5015. stop(paste("No standard deviation info stored for", reduction))
  5016. }
  5017. if (anyNA(data.use)) {
  5018. stop("Standard deviations contain NA for reduction ", reduction, call. = FALSE)
  5019. }
  5020. if (ndims > length(x = data.use)) {
  5021. warning("The object only has information for ", length(x = data.use), " dimensions")
  5022. ndims <- length(x = data.use)
  5023. }
  5024. if (plot_type == "stdev") {
  5025. y_label <- "Standard Deviation"
  5026. y_data <- data.use[1:ndims]
  5027. } else {
  5028. den <- sum(data.use^2)
  5029. if (!is.finite(den) || den == 0) {
  5030. stop("Cannot compute variance explained: sum of squared standard deviations is not positive for reduction ", reduction, call. = FALSE)
  5031. }
  5032. pct <- data.use^2 / den * 100
  5033. if (plot_type == "variance") {
  5034. y_label <- "Percentage of Variance Explained"
  5035. y_data <- pct[1:ndims]
  5036. } else {
  5037. y_label <- "Cumulative % Variance Explained"
  5038. y_data <- cumsum(pct)[1:ndims]
  5039. }
  5040. }
  5041. plot <- ggplot(data = data.frame(dims = 1:ndims, y_data = y_data)) +
  5042. geom_point(mapping = aes(x = .data[["dims"]], y = .data[["y_data"]])) +
  5043. labs(x = gsub(pattern = '_$', replacement = '', x = Key(object = object[[reduction]])), y = y_label) +
  5044. theme_cowplot()
  5045. return(plot)
  5046. }
  5047. #' Boxplot of correlation of a variable (e.g. number of UMIs) with expression
  5048. #' data
  5049. #'
  5050. #' @param object Seurat object
  5051. #' @param assay Assay where the feature grouping info and correlations are
  5052. #' stored
  5053. #' @param feature.group Name of the column in meta.features where the feature
  5054. #' grouping info is stored
  5055. #' @param cor Name of the column in meta.features where correlation info is
  5056. #' stored
  5057. #'
  5058. #' @return Returns a ggplot boxplot of correlations split by group
  5059. #'
  5060. #' @importFrom ggplot2 geom_boxplot scale_fill_manual geom_hline
  5061. #' @importFrom cowplot theme_cowplot
  5062. #' @importFrom scales brewer_pal
  5063. #' @importFrom stats complete.cases
  5064. #'
  5065. #' @export
  5066. #' @concept visualization
  5067. #'
  5068. GroupCorrelationPlot <- function(
  5069. object,
  5070. assay = NULL,
  5071. feature.group = "feature.grp",
  5072. cor = "nCount_RNA_cor"
  5073. ) {
  5074. assay <- assay %||% DefaultAssay(object = object)
  5075. data <- object[[assay]][[c(feature.group, cor)]]
  5076. data <- data[complete.cases(data), ]
  5077. colnames(x = data) <- c('grp', 'cor')
  5078. data$grp <- as.character(data$grp)
  5079. plot <- ggplot(data = data, aes(x = .data[["grp"]], y = .data[["cor"]], fill = .data[["grp"]])) +
  5080. geom_boxplot() +
  5081. theme_cowplot() +
  5082. scale_fill_manual(values = rev(x = brewer_pal(palette = 'YlOrRd')(n = 7))) +
  5083. ylab(paste(
  5084. "Correlation with",
  5085. gsub(x = cor, pattern = "_cor", replacement = "")
  5086. )) +
  5087. geom_hline(yintercept = 0) +
  5088. NoLegend() +
  5089. theme(
  5090. axis.line.x = element_blank(),
  5091. axis.title.x = element_blank(),
  5092. axis.ticks.x = element_blank(),
  5093. axis.text.x = element_blank()
  5094. )
  5095. return(plot)
  5096. }
  5097. #' JackStraw Plot
  5098. #'
  5099. #' Plots the results of the JackStraw analysis for PCA significance. For each
  5100. #' PC, plots a QQ-plot comparing the distribution of p-values for all genes
  5101. #' across each PC, compared with a uniform distribution. Also determines a
  5102. #' p-value for the overall significance of each PC (see Details).
  5103. #'
  5104. #' Significant PCs should show a p-value distribution (black curve) that is
  5105. #' strongly skewed to the left compared to the null distribution (dashed line)
  5106. #' The p-value for each PC is based on a proportion test comparing the number
  5107. #' of genes with a p-value below a particular threshold (score.thresh), compared with the
  5108. #' proportion of genes expected under a uniform distribution of p-values.
  5109. #'
  5110. #' @param object Seurat object
  5111. #' @param dims Dims to plot
  5112. #' @param cols Vector of colors, each color corresponds to an individual PC. This may also be a single character
  5113. #' or numeric value corresponding to a palette as specified by \code{\link[RColorBrewer]{brewer.pal.info}}.
  5114. #' By default, ggplot2 assigns colors. We also include a number of palettes from the pals package.
  5115. #' See \code{\link{DiscretePalette}} for details.
  5116. #' @param reduction reduction to pull jackstraw info from
  5117. #' @param xmax X-axis maximum on each QQ plot.
  5118. #' @param ymax Y-axis maximum on each QQ plot.
  5119. #'
  5120. #' @return A ggplot object
  5121. #'
  5122. #' @author Omri Wurtzel
  5123. #' @seealso \code{\link{ScoreJackStraw}}
  5124. #'
  5125. #' @importFrom stats qunif
  5126. #' @importFrom scales hue_pal
  5127. #' @importFrom ggplot2 ggplot stat_qq labs xlim ylim
  5128. #' coord_flip geom_abline guides guide_legend
  5129. #' @importFrom cowplot theme_cowplot
  5130. #'
  5131. #' @export
  5132. #' @concept visualization
  5133. #'
  5134. #' @examples
  5135. #' data("pbmc_small")
  5136. #' JackStrawPlot(object = pbmc_small)
  5137. #'
  5138. JackStrawPlot <- function(
  5139. object,
  5140. dims = 1:5,
  5141. cols = NULL,
  5142. reduction = 'pca',
  5143. xmax = 0.1,
  5144. ymax = 0.3
  5145. ) {
  5146. pAll <- JS(object = object[[reduction]], slot = 'empirical')
  5147. if (max(dims) > ncol(x = pAll)) {
  5148. stop("Max dimension is ", ncol(x = pAll))
  5149. }
  5150. pAll <- pAll[, dims, drop = FALSE]
  5151. pAll <- as.data.frame(x = pAll)
  5152. data.plot <- Melt(x = pAll)
  5153. colnames(x = data.plot) <- c("Contig", "PC", "Value")
  5154. score.df <- JS(object = object[[reduction]], slot = 'overall')
  5155. if (nrow(x = score.df) < max(dims)) {
  5156. stop("Jackstraw procedure not scored for all the provided dims. Please run ScoreJackStraw.")
  5157. }
  5158. score.df <- score.df[dims, , drop = FALSE]
  5159. if (nrow(x = score.df) == 0) {
  5160. stop(paste0("JackStraw hasn't been scored. Please run ScoreJackStraw before plotting."))
  5161. }
  5162. data.plot$PC.Score <- rep(
  5163. x = paste0("PC ", score.df[ ,"PC"], ": ", sprintf("%1.3g", score.df[ ,"Score"])),
  5164. each = length(x = unique(x = data.plot$Contig))
  5165. )
  5166. data.plot$PC.Score <- factor(
  5167. x = data.plot$PC.Score,
  5168. levels = paste0("PC ", score.df[, "PC"], ": ", sprintf("%1.3g", score.df[, "Score"]))
  5169. )
  5170. if (is.null(x = cols)) {
  5171. cols <- hue_pal()(length(x = dims))
  5172. }
  5173. if (length(x = cols) < length(x = dims)) {
  5174. stop("Not enough colors for the number of dims selected")
  5175. }
  5176. gp <- ggplot(data = data.plot, mapping = aes(sample = .data[['Value']], color = .data[['PC.Score']])) +
  5177. stat_qq(distribution = qunif) +
  5178. labs(x = "Theoretical [runif(1000)]", y = "Empirical") +
  5179. scale_color_manual(values = cols) +
  5180. xlim(0, ymax) +
  5181. ylim(0, xmax) +
  5182. coord_flip() +
  5183. geom_abline(intercept = 0, slope = 1, linetype = "dashed", na.rm = TRUE) +
  5184. guides(color = guide_legend(title = "PC: p-value")) +
  5185. theme_cowplot()
  5186. return(gp)
  5187. }
  5188. #' Plot clusters as a tree
  5189. #'
  5190. #' Plots previously computed tree (from BuildClusterTree)
  5191. #'
  5192. #' @param object Seurat object
  5193. #' @param direction A character string specifying the direction of the tree (default is downwards)
  5194. #' Possible options: "rightwards", "leftwards", "upwards", and "downwards".
  5195. #' @param \dots Additional arguments to
  5196. #' \code{\link[ape:plot.phylo]{ape::plot.phylo}}
  5197. #'
  5198. #' @return Plots dendogram (must be precomputed using BuildClusterTree), returns no value
  5199. #'
  5200. #' @export
  5201. #' @concept visualization
  5202. #'
  5203. #' @examples
  5204. #' \dontrun{
  5205. #' if (requireNamespace("ape", quietly = TRUE)) {
  5206. #' data("pbmc_small")
  5207. #' pbmc_small <- BuildClusterTree(object = pbmc_small)
  5208. #' PlotClusterTree(object = pbmc_small)
  5209. #' }
  5210. #' }
  5211. PlotClusterTree <- function(object, direction = "downwards", ...) {
  5212. if (isFALSE(x = requireNamespace('ape', quietly = TRUE))) {
  5213. stop(cluster.ape, call. = FALSE)
  5214. }
  5215. if (is.null(x = Tool(object = object, slot = "BuildClusterTree"))) {
  5216. stop("Phylogenetic tree does not exist, build using BuildClusterTree")
  5217. }
  5218. data.tree <- Tool(object = object, slot = "BuildClusterTree")
  5219. ape::plot.phylo(x = data.tree, direction = direction, ...)
  5220. ape::nodelabels()
  5221. }
  5222. #' Visualize Dimensional Reduction genes
  5223. #'
  5224. #' Visualize top genes associated with reduction components
  5225. #'
  5226. #' @param object Seurat object
  5227. #' @param reduction Reduction technique to visualize results for
  5228. #' @param dims Number of dimensions to display
  5229. #' @param nfeatures Number of genes to display
  5230. #' @param col Color of points to use
  5231. #' @param projected Use reduction values for full dataset (i.e. projected
  5232. #' dimensional reduction values)
  5233. #' @param balanced Return an equal number of genes with + and - scores. If
  5234. #' FALSE (default), returns the top genes ranked by the scores absolute values
  5235. #' @param ncol Number of columns to display
  5236. #' @param combine Combine plots into a single \code{patchwork}
  5237. #' ggplot object. If \code{FALSE}, return a list of ggplot objects
  5238. #'
  5239. #' @return A \code{patchwork} ggplot object if
  5240. #' \code{combine = TRUE}; otherwise, a list of ggplot objects
  5241. #'
  5242. #' @importFrom patchwork wrap_plots
  5243. #' @importFrom cowplot theme_cowplot
  5244. #' @importFrom ggplot2 ggplot geom_point labs
  5245. #' @export
  5246. #' @concept visualization
  5247. #'
  5248. #' @examples
  5249. #' data("pbmc_small")
  5250. #' VizDimLoadings(object = pbmc_small)
  5251. #'
  5252. VizDimLoadings <- function(
  5253. object,
  5254. dims = 1:5,
  5255. nfeatures = 30,
  5256. col = 'blue',
  5257. reduction = 'pca',
  5258. projected = FALSE,
  5259. balanced = FALSE,
  5260. ncol = NULL,
  5261. combine = TRUE
  5262. ) {
  5263. if (is.null(x = ncol)) {
  5264. ncol <- 2
  5265. if (length(x = dims) == 1) {
  5266. ncol <- 1
  5267. }
  5268. if (length(x = dims) > 6) {
  5269. ncol <- 3
  5270. }
  5271. if (length(x = dims) > 9) {
  5272. ncol <- 4
  5273. }
  5274. }
  5275. loadings <- Loadings(object = object[[reduction]], projected = projected)
  5276. features <- lapply(
  5277. X = dims,
  5278. FUN = TopFeatures,
  5279. object = object[[reduction]],
  5280. nfeatures = nfeatures,
  5281. projected = projected,
  5282. balanced = balanced
  5283. )
  5284. features <- lapply(
  5285. X = features,
  5286. FUN = unlist,
  5287. use.names = FALSE
  5288. )
  5289. loadings <- loadings[unlist(x = features), dims, drop = FALSE]
  5290. names(x = features) <- colnames(x = loadings) <- as.character(x = dims)
  5291. plots <- lapply(
  5292. X = as.character(x = dims),
  5293. FUN = function(i) {
  5294. data.plot <- as.data.frame(x = loadings[features[[i]], i, drop = FALSE])
  5295. colnames(x = data.plot) <- paste0(Key(object = object[[reduction]]), i)
  5296. data.plot$feature <- factor(x = rownames(x = data.plot), levels = rownames(x = data.plot))
  5297. plot <- ggplot(
  5298. data = data.plot,
  5299. mapping = aes(x = .data[[paste0(Key(object = object[[reduction]]), i)]], y = .data[['feature']])
  5300. ) +
  5301. geom_point(col = col) +
  5302. labs(y = NULL) + theme_cowplot()
  5303. return(plot)
  5304. }
  5305. )
  5306. if (combine) {
  5307. plots <- wrap_plots(plots, ncol = ncol)
  5308. }
  5309. return(plots)
  5310. }
  5311. #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  5312. # Exported utility functions
  5313. #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  5314. #' Augments ggplot2-based plot with a PNG image.
  5315. #'
  5316. #' Creates "vector-friendly" plots. Does this by saving a copy of the plot as a PNG file,
  5317. #' then adding the PNG image with \code{\link[ggplot2]{annotation_raster}} to a blank plot
  5318. #' of the same dimensions as \code{plot}. Please note: original legends and axes will be lost
  5319. #' during augmentation.
  5320. #'
  5321. #' @param plot A ggplot object
  5322. #' @param width,height Width and height of PNG version of plot
  5323. #' @param dpi Plot resolution
  5324. #'
  5325. #' @return A ggplot object
  5326. #'
  5327. #' @importFrom png readPNG
  5328. #' @importFrom ggplot2 ggplot_build ggsave ggplot geom_blank annotation_raster ggtitle
  5329. #'
  5330. #' @export
  5331. #' @concept visualization
  5332. #'
  5333. #' @examples
  5334. #' \dontrun{
  5335. #' data("pbmc_small")
  5336. #' plot <- DimPlot(object = pbmc_small)
  5337. #' AugmentPlot(plot = plot)
  5338. #' }
  5339. #'
  5340. AugmentPlot <- function(plot, width = 10, height = 10, dpi = 100) {
  5341. pbuild.params <- ggplot_build(plot = plot)$layout$panel_params[[1]]
  5342. range.values <- c(
  5343. pbuild.params$x.range,
  5344. pbuild.params$y.range
  5345. )
  5346. xyparams <- GetXYAesthetics(
  5347. plot = plot,
  5348. geom = class(x = plot$layers[[1]]$geom)[1]
  5349. )
  5350. title <- plot$labels$title
  5351. tmpfile <- tempfile(fileext = '.png')
  5352. ggsave(
  5353. filename = tmpfile,
  5354. plot = plot + NoLegend() + NoAxes() + theme(plot.title = element_blank()),
  5355. width = width,
  5356. height = height,
  5357. dpi = dpi
  5358. )
  5359. img <- readPNG(source = tmpfile)
  5360. file.remove(tmpfile)
  5361. blank <- ggplot(
  5362. data = plot$data,
  5363. mapping = aes(x = .data[[xyparams$x]], y = .data[[xyparams$y]])
  5364. ) + geom_blank()
  5365. blank <- blank + plot$theme + ggtitle(label = title)
  5366. blank <- blank + annotation_raster(
  5367. raster = img,
  5368. xmin = range.values[1],
  5369. xmax = range.values[2],
  5370. ymin = range.values[3],
  5371. ymax = range.values[4]
  5372. )
  5373. return(blank)
  5374. }
  5375. #' Automagically calculate a point size for ggplot2-based scatter plots
  5376. #'
  5377. #' It happens to look good
  5378. #'
  5379. #' @param data A data frame being passed to ggplot2
  5380. #' @param raster If TRUE, point size is set to 1
  5381. #'
  5382. #' @return The "optimal" point size for visualizing these data
  5383. #'
  5384. #' @export
  5385. #' @concept visualization
  5386. #'
  5387. #' @examples
  5388. #' df <- data.frame(x = rnorm(n = 10000), y = runif(n = 10000))
  5389. #' AutoPointSize(data = df)
  5390. #'
  5391. AutoPointSize <- function(data, raster = NULL) {
  5392. return(ifelse(
  5393. test = isTRUE(x = raster),
  5394. yes = 1,
  5395. no = min(1583 / nrow(x = data), 1)
  5396. ))
  5397. }
  5398. #' Determine text color based on background color
  5399. #'
  5400. #' @param background A vector of background colors; supports R color names and
  5401. #' hexadecimal codes
  5402. #' @param threshold Intensity threshold for light/dark cutoff; intensities
  5403. #' greater than \code{theshold} yield \code{dark}, others yield \code{light}
  5404. #' @param w3c Use \href{https://www.w3.org/TR/WCAG20/}{W3C} formula for calculating
  5405. #' background text color; ignores \code{threshold}
  5406. #' @param dark Color for dark text
  5407. #' @param light Color for light text
  5408. #'
  5409. #' @return A named vector of either \code{dark} or \code{light}, depending on
  5410. #' \code{background}; names of vector are \code{background}
  5411. #'
  5412. #' @export
  5413. #' @concept visualization
  5414. #'
  5415. #' @source \url{https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color}
  5416. #'
  5417. #' @examples
  5418. #' BGTextColor(background = c('black', 'white', '#E76BF3'))
  5419. #'
  5420. BGTextColor <- function(
  5421. background,
  5422. threshold = 186,
  5423. w3c = FALSE,
  5424. dark = 'black',
  5425. light = 'white'
  5426. ) {
  5427. if (w3c) {
  5428. luminance <- Luminance(color = background)
  5429. threshold <- 179
  5430. return(ifelse(
  5431. test = luminance > sqrt(x = 1.05 * 0.05) - 0.05,
  5432. yes = dark,
  5433. no = light
  5434. ))
  5435. }
  5436. return(ifelse(
  5437. test = Intensity(color = background) > threshold,
  5438. yes = dark,
  5439. no = light
  5440. ))
  5441. }
  5442. #' @export
  5443. #' @concept visualization
  5444. #'
  5445. #' @rdname CustomPalette
  5446. #' @aliases BlackAndWhite
  5447. #'
  5448. #' @examples
  5449. #' df <- data.frame(x = rnorm(n = 100, mean = 20, sd = 2), y = rbinom(n = 100, size = 100, prob = 0.2))
  5450. #' plot(df, col = BlackAndWhite())
  5451. #'
  5452. BlackAndWhite <- function(mid = NULL, k = 50) {
  5453. return(CustomPalette(low = "white", high = "black", mid = mid, k = k))
  5454. }
  5455. #' @export
  5456. #' @concept visualization
  5457. #'
  5458. #' @rdname CustomPalette
  5459. #' @aliases BlueAndRed
  5460. #'
  5461. #' @examples
  5462. #' df <- data.frame(x = rnorm(n = 100, mean = 20, sd = 2), y = rbinom(n = 100, size = 100, prob = 0.2))
  5463. #' plot(df, col = BlueAndRed())
  5464. #'
  5465. BlueAndRed <- function(k = 50) {
  5466. return(CustomPalette(low = "#313695" , high = "#A50026", mid = "#FFFFBF", k = k))
  5467. }
  5468. #' Cell Selector
  5469. #'
  5470. #' Select points on a scatterplot and get information about them
  5471. #'
  5472. #' @param plot A ggplot2 plot
  5473. #' @param object An optional Seurat object; if passes, will return an object
  5474. #' with the identities of selected cells set to \code{ident}
  5475. #' @param ident An optional new identity class to assign the selected cells
  5476. #' @param ... Ignored
  5477. #'
  5478. #' @return If \code{object} is \code{NULL}, the names of the points selected;
  5479. #' otherwise, a Seurat object with the selected cells identity classes set to
  5480. #' \code{ident}
  5481. #'
  5482. #' @importFrom miniUI miniPage gadgetTitleBar miniTitleBarButton
  5483. #' miniContentPanel
  5484. #' @importFrom shiny fillRow plotOutput brushOpts reactiveValues observeEvent
  5485. #' stopApp brushedPoints renderPlot runGadget
  5486. #'
  5487. #' @export
  5488. #' @concept visualization
  5489. #'
  5490. #' @seealso \code{\link{DimPlot}} \code{\link{FeaturePlot}}
  5491. #'
  5492. #' @examples
  5493. #' \dontrun{
  5494. #' data("pbmc_small")
  5495. #' plot <- DimPlot(object = pbmc_small)
  5496. #' # Follow instructions in the terminal to select points
  5497. #' cells.located <- CellSelector(plot = plot)
  5498. #' cells.located
  5499. #' # Automatically set the identity class of selected cells and return a new Seurat object
  5500. #' pbmc_small <- CellSelector(plot = plot, object = pbmc_small, ident = 'SelectedCells')
  5501. #' }
  5502. #'
  5503. CellSelector <- function(plot, object = NULL, ident = 'SelectedCells', ...) {
  5504. # Set up the gadget UI
  5505. ui <- miniPage(
  5506. gadgetTitleBar(
  5507. title = "Cell Selector",
  5508. left = miniTitleBarButton(inputId = "reset", label = "Reset")
  5509. ),
  5510. miniContentPanel(
  5511. fillRow(
  5512. plotOutput(
  5513. outputId = "plot",
  5514. height = '100%',
  5515. brush = brushOpts(
  5516. id = 'brush',
  5517. delay = 100,
  5518. delayType = 'debounce',
  5519. clip = TRUE,
  5520. resetOnNew = FALSE
  5521. )
  5522. )
  5523. ),
  5524. )
  5525. )
  5526. # Get some plot information
  5527. if (inherits(x = plot, what = 'patchwork')) {
  5528. if (length(x = plot$patches$plots)) {
  5529. warning(
  5530. "Multiple plots passed, using last plot",
  5531. call. = FALSE,
  5532. immediate. = TRUE
  5533. )
  5534. }
  5535. class(x = plot) <- grep(
  5536. pattern = 'patchwork',
  5537. x = class(x = plot),
  5538. value = TRUE,
  5539. invert = TRUE
  5540. )
  5541. }
  5542. xy.aes <- GetXYAesthetics(plot = plot)
  5543. dark.theme <- !is.null(x = plot$theme$plot.background$fill) &&
  5544. plot$theme$plot.background$fill == 'black'
  5545. plot.data <- GGpointToBase(plot = plot, do.plot = FALSE)
  5546. plot.data$selected_ <- FALSE
  5547. rownames(x = plot.data) <- rownames(x = plot$data)
  5548. colnames(x = plot.data) <- gsub(
  5549. pattern = '-',
  5550. replacement = '.',
  5551. x = colnames(x = plot.data)
  5552. )
  5553. # Server function
  5554. server <- function(input, output, session) {
  5555. plot.env <- reactiveValues(data = plot.data)
  5556. # Event handlers
  5557. observeEvent(
  5558. eventExpr = input$done,
  5559. handlerExpr = {
  5560. PlotBuild(data = plot.env$data, dark.theme = dark.theme)
  5561. selected <- rownames(x = plot.data)[plot.env$data$selected_]
  5562. if (inherits(x = object, what = 'Seurat')) {
  5563. if (!all(selected %in% Cells(x = object))) {
  5564. stop("Cannot find the selected cells in the Seurat object, please be sure you pass the same object used to generate the plot")
  5565. }
  5566. Idents(object = object, cells = selected) <- ident
  5567. selected <- object
  5568. }
  5569. stopApp(returnValue = selected)
  5570. }
  5571. )
  5572. observeEvent(
  5573. eventExpr = input$reset,
  5574. handlerExpr = {
  5575. plot.env$data <- plot.data
  5576. session$resetBrush(brushId = 'brush')
  5577. }
  5578. )
  5579. observeEvent(
  5580. eventExpr = input$brush,
  5581. handlerExpr = {
  5582. plot.env$data <- brushedPoints(
  5583. df = plot.data,
  5584. brush = input$brush,
  5585. xvar = xy.aes$x,
  5586. yvar = xy.aes$y,
  5587. allRows = TRUE
  5588. )
  5589. plot.env$data$color <- ifelse(
  5590. test = plot.env$data$selected_,
  5591. yes = '#DE2D26',
  5592. no = '#C3C3C3'
  5593. )
  5594. }
  5595. )
  5596. # Render the plot
  5597. output$plot <- renderPlot(expr = PlotBuild(
  5598. data = plot.env$data,
  5599. dark.theme = dark.theme
  5600. ))
  5601. }
  5602. return(runGadget(app = ui, server = server))
  5603. }
  5604. #' Move outliers towards center on dimension reduction plot
  5605. #'
  5606. #' @param object Seurat object
  5607. #' @param reduction Name of DimReduc to adjust
  5608. #' @param dims Dimensions to visualize
  5609. #' @param group.by Group (color) cells in different ways (for example, orig.ident)
  5610. #' @param outlier.sd Controls the outlier distance
  5611. #' @param reduction.key Key for DimReduc that is returned
  5612. #'
  5613. #' @return Returns a DimReduc object with the modified embeddings
  5614. #'
  5615. #' @export
  5616. #' @concept visualization
  5617. #'
  5618. #' @examples
  5619. #' \dontrun{
  5620. #' data("pbmc_small")
  5621. #' pbmc_small <- FindClusters(pbmc_small, resolution = 1.1)
  5622. #' pbmc_small <- RunUMAP(pbmc_small, dims = 1:5)
  5623. #' DimPlot(pbmc_small, reduction = "umap")
  5624. #' pbmc_small[["umap_new"]] <- CollapseEmbeddingOutliers(pbmc_small,
  5625. #' reduction = "umap", reduction.key = 'umap_', outlier.sd = 0.5)
  5626. #' DimPlot(pbmc_small, reduction = "umap_new")
  5627. #' }
  5628. #'
  5629. CollapseEmbeddingOutliers <- function(
  5630. object,
  5631. reduction = 'umap',
  5632. dims = 1:2,
  5633. group.by = 'ident',
  5634. outlier.sd = 2,
  5635. reduction.key = 'UMAP_'
  5636. ) {
  5637. embeddings <- Embeddings(object = object[[reduction]])[, dims]
  5638. idents <- FetchData(object = object, vars = group.by)
  5639. data.medians <- sapply(X = dims, FUN = function(x) {
  5640. tapply(X = embeddings[, x], INDEX = idents, FUN = median)
  5641. })
  5642. data.sd <- apply(X = data.medians, MARGIN = 2, FUN = sd)
  5643. data.medians.scale <- as.matrix(x = scale(x = data.medians, center = TRUE, scale = TRUE))
  5644. data.medians.scale[abs(x = data.medians.scale) < outlier.sd] <- 0
  5645. data.medians.scale <- sign(x = data.medians.scale) * (abs(x = data.medians.scale) - outlier.sd)
  5646. data.correct <- Sweep(
  5647. x = data.medians.scale,
  5648. MARGIN = 2,
  5649. STATS = data.sd,
  5650. FUN = "*"
  5651. )
  5652. data.correct <- data.correct[abs(x = apply(X = data.correct, MARGIN = 1, FUN = min)) > 0, ]
  5653. new.embeddings <- embeddings
  5654. for (i in rownames(x = data.correct)) {
  5655. cells.correct <- rownames(x = idents)[idents[, "ident"] == i]
  5656. new.embeddings[cells.correct, ] <- Sweep(
  5657. x = new.embeddings[cells.correct,],
  5658. MARGIN = 2,
  5659. STATS = data.correct[i, ],
  5660. FUN = "-"
  5661. )
  5662. }
  5663. reduc <- CreateDimReducObject(
  5664. embeddings = new.embeddings,
  5665. loadings = Loadings(object = object[[reduction]]),
  5666. assay = slot(object = object[[reduction]], name = "assay.used"),
  5667. key = reduction.key
  5668. )
  5669. return(reduc)
  5670. }
  5671. #' Combine ggplot2-based plots into a single plot
  5672. #'
  5673. #' @param plots A list of gg objects
  5674. #' @param ncol Number of columns
  5675. #' @param legend Combine legends into a single legend
  5676. #' choose from 'right' or 'bottom'; pass 'none' to remove legends, or \code{NULL}
  5677. #' to leave legends as they are
  5678. #' @param ... Extra parameters passed to plot_grid
  5679. #'
  5680. #' @return A combined plot
  5681. #'
  5682. #' @importFrom cowplot plot_grid get_legend
  5683. #' @export
  5684. #' @concept visualization
  5685. #'
  5686. #' @examples
  5687. #' data("pbmc_small")
  5688. #' pbmc_small[['group']] <- sample(
  5689. #' x = c('g1', 'g2'),
  5690. #' size = ncol(x = pbmc_small),
  5691. #' replace = TRUE
  5692. #' )
  5693. #' plot1 <- FeaturePlot(
  5694. #' object = pbmc_small,
  5695. #' features = 'MS4A1',
  5696. #' split.by = 'group'
  5697. #' )
  5698. #' plot2 <- FeaturePlot(
  5699. #' object = pbmc_small,
  5700. #' features = 'FCN1',
  5701. #' split.by = 'group'
  5702. #' )
  5703. #' CombinePlots(
  5704. #' plots = list(plot1, plot2),
  5705. #' legend = 'none',
  5706. #' nrow = length(x = unique(x = pbmc_small[['group', drop = TRUE]]))
  5707. #' )
  5708. #'
  5709. CombinePlots <- function(plots, ncol = NULL, legend = NULL, ...) {
  5710. .Deprecated(msg = "CombinePlots is being deprecated. Plots should now be combined using the patchwork system.")
  5711. plots.combined <- if (length(x = plots) > 1) {
  5712. if (!is.null(x = legend)) {
  5713. if (legend != 'none') {
  5714. plot.legend <- get_legend(plot = plots[[1]] + theme(legend.position = legend))
  5715. }
  5716. plots <- lapply(
  5717. X = plots,
  5718. FUN = function(x) {
  5719. return(x + NoLegend())
  5720. }
  5721. )
  5722. }
  5723. plots.combined <- plot_grid(
  5724. plotlist = plots,
  5725. ncol = ncol,
  5726. align = 'hv',
  5727. ...
  5728. )
  5729. if (!is.null(x = legend)) {
  5730. plots.combined <- switch(
  5731. EXPR = legend,
  5732. 'bottom' = plot_grid(
  5733. plots.combined,
  5734. plot.legend,
  5735. ncol = 1,
  5736. rel_heights = c(1, 0.2)
  5737. ),
  5738. 'right' = plot_grid(
  5739. plots.combined,
  5740. plot.legend,
  5741. rel_widths = c(3, 0.3)
  5742. ),
  5743. plots.combined
  5744. )
  5745. }
  5746. plots.combined
  5747. } else {
  5748. plots[[1]]
  5749. }
  5750. return(plots.combined)
  5751. }
  5752. #' Create a custom color palette
  5753. #'
  5754. #' Creates a custom color palette based on low, middle, and high color values
  5755. #'
  5756. #' @param low low color
  5757. #' @param high high color
  5758. #' @param mid middle color. Optional.
  5759. #' @param k number of steps (colors levels) to include between low and high values
  5760. #'
  5761. #' @return A color palette for plotting
  5762. #'
  5763. #' @importFrom grDevices col2rgb rgb
  5764. #' @export
  5765. #' @concept visualization
  5766. #'
  5767. #' @rdname CustomPalette
  5768. #' @examples
  5769. #' myPalette <- CustomPalette()
  5770. #' myPalette
  5771. #'
  5772. CustomPalette <- function(
  5773. low = "white",
  5774. high = "red",
  5775. mid = NULL,
  5776. k = 50
  5777. ) {
  5778. low <- col2rgb(col = low) / 255
  5779. high <- col2rgb(col = high) / 255
  5780. if (is.null(x = mid)) {
  5781. r <- seq(from = low[1], to = high[1], len = k)
  5782. g <- seq(from = low[2], to = high[2], len = k)
  5783. b <- seq(from = low[3], to = high[3], len = k)
  5784. } else {
  5785. k2 <- round(x = k / 2)
  5786. mid <- col2rgb(col = mid) / 255
  5787. r <- c(
  5788. seq(from = low[1], to = mid[1], len = k2),
  5789. seq(from = mid[1], to = high[1], len = k2)
  5790. )
  5791. g <- c(
  5792. seq(from = low[2], to = mid[2], len = k2),
  5793. seq(from = mid[2], to = high[2],len = k2)
  5794. )
  5795. b <- c(
  5796. seq(from = low[3], to = mid[3], len = k2),
  5797. seq(from = mid[3], to = high[3], len = k2)
  5798. )
  5799. }
  5800. return(rgb(red = r, green = g, blue = b))
  5801. }
  5802. #' Discrete colour palettes from pals
  5803. #'
  5804. #' These are included here because pals depends on a number of compiled
  5805. #' packages, and this can lead to increases in run time for Travis,
  5806. #' and generally should be avoided when possible.
  5807. #'
  5808. #' These palettes are a much better default for data with many classes
  5809. #' than the default ggplot2 palette.
  5810. #'
  5811. #' Many thanks to Kevin Wright for writing the pals package.
  5812. #'
  5813. #' @param n Number of colours to be generated.
  5814. #' @param palette Options are
  5815. #' "alphabet", "alphabet2", "glasbey", "polychrome", "stepped", and "parade".
  5816. #' Can be omitted and the function will use the one based on the requested n.
  5817. #' @param shuffle Shuffle the colors in the selected palette.
  5818. #'
  5819. #' @return A vector of colors
  5820. #'
  5821. #' @details
  5822. #' Taken from the pals package (Licence: GPL-3).
  5823. #' \url{https://cran.r-project.org/package=pals}
  5824. #' Credit: Kevin Wright
  5825. #'
  5826. #' @export
  5827. #' @concept visualization
  5828. #'
  5829. DiscretePalette <- function(n, palette = NULL, shuffle = FALSE) {
  5830. palettes <- list(
  5831. alphabet = c(
  5832. "#F0A0FF", "#0075DC", "#993F00", "#4C005C", "#191919", "#005C31",
  5833. "#2BCE48", "#FFCC99", "#808080", "#94FFB5", "#8F7C00", "#9DCC00",
  5834. "#C20088", "#003380", "#FFA405", "#FFA8BB", "#426600", "#FF0010",
  5835. "#5EF1F2", "#00998F", "#E0FF66", "#740AFF", "#990000", "#FFFF80",
  5836. "#FFE100", "#FF5005"
  5837. ),
  5838. alphabet2 = c(
  5839. "#AA0DFE", "#3283FE", "#85660D", "#782AB6", "#565656", "#1C8356",
  5840. "#16FF32", "#F7E1A0", "#E2E2E2", "#1CBE4F", "#C4451C", "#DEA0FD",
  5841. "#FE00FA", "#325A9B", "#FEAF16", "#F8A19F", "#90AD1C", "#F6222E",
  5842. "#1CFFCE", "#2ED9FF", "#B10DA1", "#C075A6", "#FC1CBF", "#B00068",
  5843. "#FBE426", "#FA0087"
  5844. ),
  5845. glasbey = c(
  5846. "#0000FF", "#FF0000", "#00FF00", "#000033", "#FF00B6", "#005300",
  5847. "#FFD300", "#009FFF", "#9A4D42", "#00FFBE", "#783FC1", "#1F9698",
  5848. "#FFACFD", "#B1CC71", "#F1085C", "#FE8F42", "#DD00FF", "#201A01",
  5849. "#720055", "#766C95", "#02AD24", "#C8FF00", "#886C00", "#FFB79F",
  5850. "#858567", "#A10300", "#14F9FF", "#00479E", "#DC5E93", "#93D4FF",
  5851. "#004CFF", "#F2F318"
  5852. ),
  5853. polychrome = c(
  5854. "#5A5156", "#E4E1E3", "#F6222E", "#FE00FA", "#16FF32", "#3283FE",
  5855. "#FEAF16", "#B00068", "#1CFFCE", "#90AD1C", "#2ED9FF", "#DEA0FD",
  5856. "#AA0DFE", "#F8A19F", "#325A9B", "#C4451C", "#1C8356", "#85660D",
  5857. "#B10DA1", "#FBE426", "#1CBE4F", "#FA0087", "#FC1CBF", "#F7E1A0",
  5858. "#C075A6", "#782AB6", "#AAF400", "#BDCDFF", "#822E1C", "#B5EFB5",
  5859. "#7ED7D1", "#1C7F93", "#D85FF7", "#683B79", "#66B0FF", "#3B00FB"
  5860. ),
  5861. stepped = c(
  5862. "#990F26", "#B33E52", "#CC7A88", "#E6B8BF", "#99600F", "#B3823E",
  5863. "#CCAA7A", "#E6D2B8", "#54990F", "#78B33E", "#A3CC7A", "#CFE6B8",
  5864. "#0F8299", "#3E9FB3", "#7ABECC", "#B8DEE6", "#3D0F99", "#653EB3",
  5865. "#967ACC", "#C7B8E6", "#333333", "#666666", "#999999", "#CCCCCC"
  5866. ),
  5867. parade = c(
  5868. '#ff6969', '#9b37ff', '#cd3737', '#69cdff', '#ffff69', '#69cdcd',
  5869. '#9b379b', '#3737cd', '#ffff9b', '#cdff69', '#ff9b37', '#37ffff',
  5870. '#9b69ff', '#37cd69', '#ff3769', '#ff3737', '#37ff9b', '#cdcd37',
  5871. '#3769cd', '#37cdff', '#9b3737', '#ff699b', '#9b9bff', '#cd9b37',
  5872. '#69ff37', '#cd3769', '#cd69cd', '#cd6937', '#3737ff', '#cdcd69',
  5873. '#ff9b69', '#cd37cd', '#9bff37', '#cd379b', '#cd6969', '#69ff9b',
  5874. '#ff379b', '#9bff9b', '#6937ff', '#69cd37', '#cdff37', '#9bff69',
  5875. '#9b37cd', '#ff37ff', '#ff37cd', '#ffff37', '#37cd9b', '#379bff',
  5876. '#ffcd37', '#379b37', '#ff9bff', '#379b9b', '#69ffcd', '#379bcd',
  5877. '#ff69ff', '#ff9b9b', '#37ff69', '#ff6937', '#6969ff', '#699bff',
  5878. '#ffcd69', '#69ffff', '#37ff37', '#6937cd', '#37cd37', '#3769ff',
  5879. '#cd69ff', '#6969cd', '#9bcd37', '#69ff69', '#37cdcd', '#cd37ff',
  5880. '#37379b', '#37ffcd', '#69cd69', '#ff69cd', '#9bffff', '#9b9b37'
  5881. )
  5882. )
  5883. if (is.null(x = n)) {
  5884. return(names(x = palettes))
  5885. }
  5886. if (is.null(x = palette)) {
  5887. if (n <= 26) {
  5888. palette <- "alphabet"
  5889. } else if (n <= 32) {
  5890. palette <- "glasbey"
  5891. } else {
  5892. palette <- "polychrome"
  5893. }
  5894. }
  5895. palette.vec <- palettes[[palette]]
  5896. if (n > length(x = palette.vec)) {
  5897. warning("Not enough colours in specified palette")
  5898. }
  5899. if (isTRUE(shuffle)) {
  5900. palette.vec <- sample(palette.vec)
  5901. }
  5902. palette <- palette.vec[seq_len(length.out = n)]
  5903. return(palette)
  5904. }
  5905. #' @rdname CellSelector
  5906. #' @export
  5907. #' @concept visualization
  5908. #'
  5909. FeatureLocator <- function(plot, ...) {
  5910. .Defunct(
  5911. new = 'CellSelector',
  5912. package = 'Seurat',
  5913. msg = "'FeatureLocator' has been replaced by 'CellSelector'"
  5914. )
  5915. }
  5916. #' Hover Locator
  5917. #'
  5918. #' Get quick information from a scatterplot by hovering over points
  5919. #'
  5920. #' @param plot A ggplot2 plot
  5921. #' @param information An optional dataframe or matrix of extra information to be displayed on hover
  5922. #' @param dark.theme Plot using a dark theme?
  5923. #' @param axes Display or hide x- and y-axes
  5924. #' @param ... Extra parameters to be passed to \code{\link[plotly]{layout}}
  5925. #'
  5926. #' @importFrom ggplot2 ggplot_build
  5927. #' @importFrom plotly plot_ly layout add_annotations
  5928. #' @export
  5929. #' @concept visualization
  5930. #'
  5931. #' @seealso \code{\link[plotly]{layout}} \code{\link[ggplot2]{ggplot_build}}
  5932. #' \code{\link{DimPlot}} \code{\link{FeaturePlot}}
  5933. #'
  5934. #' @examples
  5935. #' \dontrun{
  5936. #' data("pbmc_small")
  5937. #' plot <- DimPlot(object = pbmc_small)
  5938. #' HoverLocator(plot = plot, information = FetchData(object = pbmc_small, vars = 'percent.mito'))
  5939. #' }
  5940. #'
  5941. HoverLocator <- function(
  5942. plot,
  5943. information = NULL,
  5944. axes = TRUE,
  5945. dark.theme = FALSE,
  5946. ...
  5947. ) {
  5948. # Use GGpointToBase because we already have ggplot objects
  5949. # with colors (which are annoying in plotly)
  5950. plot.build <- suppressWarnings(expr = GGpointToPlotlyBuild(
  5951. plot = plot,
  5952. information = information,
  5953. ...
  5954. ))
  5955. data <- ggplot_build(plot = plot)$plot$data
  5956. # Set up axis labels here
  5957. # Also, a bunch of stuff to get axis lines done properly
  5958. if (axes) {
  5959. xaxis <- list(
  5960. title = names(x = data)[1],
  5961. showgrid = FALSE,
  5962. zeroline = FALSE,
  5963. showline = TRUE
  5964. )
  5965. yaxis <- list(
  5966. title = names(x = data)[2],
  5967. showgrid = FALSE,
  5968. zeroline = FALSE,
  5969. showline = TRUE
  5970. )
  5971. } else {
  5972. xaxis <- yaxis <- list(visible = FALSE)
  5973. }
  5974. # Check for dark theme
  5975. if (dark.theme) {
  5976. title <- list(color = 'white')
  5977. xaxis <- c(xaxis, color = 'white')
  5978. yaxis <- c(yaxis, color = 'white')
  5979. plotbg <- 'black'
  5980. } else {
  5981. title = list(color = 'black')
  5982. plotbg = 'white'
  5983. }
  5984. # The `~' means pull from the data passed (this is why we reset the names)
  5985. # Use I() to get plotly to accept the colors from the data as is
  5986. # Set hoverinfo to 'text' to override the default hover information
  5987. # rather than append to it
  5988. p <- layout(
  5989. p = plot_ly(
  5990. data = plot.build,
  5991. x = ~x,
  5992. y = ~y,
  5993. type = 'scatter',
  5994. mode = 'markers',
  5995. color = ~I(color),
  5996. hoverinfo = 'text',
  5997. text = ~feature
  5998. ),
  5999. xaxis = xaxis,
  6000. yaxis = yaxis,
  6001. title = plot$labels$title,
  6002. titlefont = title,
  6003. paper_bgcolor = plotbg,
  6004. plot_bgcolor = plotbg,
  6005. ...
  6006. )
  6007. # Add labels
  6008. label.layer <- which(x = sapply(
  6009. X = plot$layers,
  6010. FUN = function(x) {
  6011. return(inherits(x = x$geom, what = c('GeomText', 'GeomTextRepel')))
  6012. }
  6013. ))
  6014. if (length(x = label.layer) == 1) {
  6015. p <- add_annotations(
  6016. p = p,
  6017. x = plot$layers[[label.layer]]$data[, 1],
  6018. y = plot$layers[[label.layer]]$data[, 2],
  6019. xref = "x",
  6020. yref = "y",
  6021. text = plot$layers[[label.layer]]$data[, 3],
  6022. xanchor = 'right',
  6023. showarrow = FALSE,
  6024. font = list(size = plot$layers[[label.layer]]$aes_params$size * 4)
  6025. )
  6026. }
  6027. return(p)
  6028. }
  6029. #' Get the intensity and/or luminance of a color
  6030. #'
  6031. #' @param color A vector of colors
  6032. #'
  6033. #' @return A vector of intensities/luminances for each color
  6034. #'
  6035. #' @name contrast-theory
  6036. #' @rdname contrast-theory
  6037. #'
  6038. #' @importFrom grDevices col2rgb
  6039. #'
  6040. #' @export
  6041. #' @concept visualization
  6042. #'
  6043. #' @source \url{https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color}
  6044. #'
  6045. #' @examples
  6046. #' Intensity(color = c('black', 'white', '#E76BF3'))
  6047. #'
  6048. Intensity <- function(color) {
  6049. intensities <- apply(
  6050. X = col2rgb(col = color),
  6051. MARGIN = 2,
  6052. FUN

visualization.R at commit 586015a, under other · at the source

Overview

Authors: Dou Ye1,2, Haipeng Zhou2, Suqing Qu2, Zhaoyan Wang2, Fan Zhang2, Xiaohua Wang2, Yuan Zhao2, Jialan Liang2, Qian Wang2, Zuo Luan2,3, Yinxiang Yang2
  1. Department of Neurology, Beijing Children’s Hospital, Capital Medical University, National Center for Children’s Health,Beijing, China
  2. Department of Pediatrics, the Sixth Medical Centre, Chinese PLA General Hospital,Beijing, China
  3. Medical School of Chinese PLA,100853 Beijing, China
Journal: Cell death discovery, volume 12, issue 1, article 112
Dates: received 12 June 2025; accepted 16 February 2026; published online 2 March 2026
Type: Review · Language: English
License: CC BY
Identifiers: DOI 10.1038/s41420-026-02971-w · PMID 41771834 · PMCID PMC12979777 · OpenAlex W7133221785
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: genetics / omics (modality), human (organism), cellular / molecular (subfield)
Methods: Spectral & time-frequency, Statistics, Smoothing, state filtering, decompositions
Keywords: Stem-cell differentiation, Development
Topic: Neurogenesis and neuroplasticity mechanisms (Developmental Neuroscience, Neuroscience), according to OpenAlex
Funding: National Key Researsh and Development Program of China,2017YFA0104200
Citations: cited by 2 papers (Europe PMC); 52 references in the paper

Abstract

Heterogeneity is widely recognised across different cell types. Human oligodendrocyte progenitor cells (hOPCs), essential for myelination, exhibit considerable heterogeneity, which has not been fully characterised. In the current study, by examining the transcriptome of hOPCs at the single-cell level, three distinct subclusters were identified: PRE-OPCs, OPCs, and PRE-OLs. Single-cell RNA-sequencing and RNA-Scope detected high platelet-derived growth factor receptor alpha (PDGFRA) expression. PDGFR-α+ hOPCs exhibited greater myelination, migration, and proliferation capabilities compared to both unsorted hOPCs and PDGFR-α– hOPCs. These enhanced functions may be associated with the activation of the PI3K-AKT-mTOR and TGF-β signalling pathways, which support oligodendrocyte differentiation.

hOPCs were induced by hNSCs, their characteristics were identified. RNA-Scope and single-cell RNA Seq sequencing showed PDGFRA were highly expressed at mRNA and protein level. hOPCs were sorted by MACS using PDGFR-α beads. The myelination, migration, and proliferation abilities of PDGFR-α+ hOPCs were higher than that of un-sorting hOPCs and PDGFR-α– hOPCs, possibly being associated with the activation of PI3K–AKT–mTOR and TGF-β signalling pathways, which support oligodendrocyte differentiation (Partly created with Scientific Image and Illustration Software BioRender).

Reproduced under the paper's license (CC BY), from the paper cited above.

Repositories

Its files are read in the Code ↔ Paper reader above, with 5 matches between paragraphs and lines of code.

satijalab/seurat

License: other
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 586015abde10618ecb32d3fe632267a83317a08d, 21 September 2026
Languages: R (114), C++ (8), C/C++ (4), C (1)
Size: 455 files, 127 scripts
Software Heritage: archived
Found in: the text, “Single-cell RNA sequencing (scRNA‑seq) of hOPCs”
Holds: README, license file, environment (DESCRIPTION), tests, continuous integration, documentation, 70 notebooks
Not found: CITATION.cff
Tools: Seurat (75 files), ggplot2 (48 files), patchwork (27 files), tidyverse (19 files), cowplot (10 files), reshape2 (3 files), SingleCellExperiment (3 files), Plotly (2 files), data.table (1 file), DESeq2 (1 file), Harmony (1 file), igraph (1 file), limma (1 file), Monocle 3 (1 file), reticulate (1 file), UMAP (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
130 files

chris-mcginnis-ucsf/DoubletFinder

License: none: the authors keep all their rights
State: the link answers, verified on 29 September 2026
Evidence: files inventoried
Commit: 1b244d8f0d54b4b1cb4365639931bbb16f01e1cd, 21 March 2025
Languages: R (12)
Size: 38 files, 12 scripts
Software Heritage: not archived
Found in: the text, “Single-cell RNA sequencing (scRNA‑seq) of hOPCs”
Holds: README, environment (DESCRIPTION), tests, documentation
Not found: license file, CITATION.cff, continuous integration
Tools: Seurat (4 files)
Availability: 1 check, the latest on 29 September 2026: the link answers
  • 29 September 2026: the link answers
13 files

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 139 scripts, each with its path and the digest of its content;
  • 5 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Data availability

Raw data from scRNA-seq have been deposited at GSA-human. Raw data reported in this paper will be shared by the lead contact upon request. Raw data of scRNA-seq at [https://ngdc.cncb.ac.cn/gsa-human/s/FdDsn5B5] and [https://ngdc.cncb.ac.cn/gsa-human/s/z66g0yYU].

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 30 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 11 authors, 2 keywords, 1 funder, 51 references.

Cite

This paper

Ye, D., Zhou, H., Qu, S., Wang, Z., Zhang, F., Wang, X., Zhao, Y., Liang, J., Wang, Q., Luan, Z., & Yang, Y. (2026). Multi-omics reveals heterogeneity and functional populations of oligodendrocyte progenitor cells induced by human neural stem cells. Cell death discovery, 12(1), 112. https://doi.org/10.1038/s41420-026-02971-w

BibTeX

@article{ye2026multi,
author = {Ye, Dou and Zhou, Haipeng and Qu, Suqing and Wang, Zhaoyan and Zhang, Fan and Wang, Xiaohua and Zhao, Yuan and Liang, Jialan and Wang, Qian and Luan, Zuo and Yang, Yinxiang},
title = {{Multi-omics reveals heterogeneity and functional populations of oligodendrocyte progenitor cells induced by human neural stem cells}},
journal = {Cell death discovery},
year = {2026},
month = mar,
volume = {12},
number = {1},
pages = {112},
publisher = {Nature Publishing Group},
issn = {2058-7716},
doi = {10.1038/s41420-026-02971-w},
url = {https://doi.org/10.1038/s41420-026-02971-w},
pmid = {41771834},
pmcid = {PMC12979777}
}

RIS

TY - JOUR
AU - Ye, Dou
AU - Zhou, Haipeng
AU - Qu, Suqing
AU - Wang, Zhaoyan
AU - Zhang, Fan
AU - Wang, Xiaohua
AU - Zhao, Yuan
AU - Liang, Jialan
AU - Wang, Qian
AU - Luan, Zuo
AU - Yang, Yinxiang
TI - Multi-omics reveals heterogeneity and functional populations of oligodendrocyte progenitor cells induced by human neural stem cells
T2 - Cell death discovery
J2 - Cell Death Discov
PY - 2026
DA - 2026/03/02
VL - 12
IS - 1
SP - 112
SN - 2058-7716
PB - Nature Publishing Group
DO - 10.1038/s41420-026-02971-w
UR - https://doi.org/10.1038/s41420-026-02971-w
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41420-026-02971-w",
"type": "article-journal",
"title": "Multi-omics reveals heterogeneity and functional populations of oligodendrocyte progenitor cells induced by human neural stem cells",
"container-title": "Cell death discovery",
"author": [
{
"family": "Ye",
"given": "Dou"
},
{
"family": "Zhou",
"given": "Haipeng"
},
{
"family": "Qu",
"given": "Suqing"
},
{
"family": "Wang",
"given": "Zhaoyan"
},
{
"family": "Zhang",
"given": "Fan"
},
{
"family": "Wang",
"given": "Xiaohua"
},
{
"family": "Zhao",
"given": "Yuan"
},
{
"family": "Liang",
"given": "Jialan"
},
{
"family": "Wang",
"given": "Qian"
},
{
"family": "Luan",
"given": "Zuo"
},
{
"family": "Yang",
"given": "Yinxiang"
}
],
"container-title-short": "Cell Death Discov",
"volume": "12",
"issue": "1",
"page": "112",
"DOI": "10.1038/s41420-026-02971-w",
"PMID": "41771834",
"PMCID": "PMC12979777",
"ISSN": "2058-7716",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41420-026-02971-w",
"language": "en",
"issued": {
"date-parts": [
[
2026,
3,
2
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.7554/elife.93640 [code]
Sibling chimerism among microglia in marmosets.
Journal: eLife
In common: Monocle 3, Harmony, SingleCellExperiment, 13 other tools, genetics / omics, cellular / molecular
[2] doi:10.1186/s12974-026-03838-8 [code]
Acarbose modulates microglial Pkm2 acetylation to reshape immunometabolism and preserve retinal neurons after ischemia-reperfusion.
Journal: Journal of neuroinflammation
In common: Monocle 3, Harmony, SingleCellExperiment, 13 other tools, genetics / omics, cellular / molecular
[3] doi:10.1002/ctm2.70683 [code]
Niacin promotes motor function recovery after spinal cord injury via Hcar2-dependent microglia immunometabolic regulation.
Journal: Clinical and translational medicine
In common: Monocle 3, Harmony, SingleCellExperiment, 13 other tools, genetics / omics, cellular / molecular
[4] doi:10.1038/s44318-026-00818-9 [code]
FAM134B-mediated ER-phagy degrades APP and suppresses Alzheimer's disease pathology.
Journal: The EMBO journal
In common: Monocle 3, Harmony, SingleCellExperiment, 13 other tools, cellular / molecular
[5] doi:10.1016/j.xcrm.2026.102766 [code]
A longitudinal single-cell and spatial multiomic atlas of pediatric high-grade glioma.
Journal: Cell reports. Medicine
In common: Monocle 3, Harmony, SingleCellExperiment, 12 other tools, genetics / omics, cellular / molecular, 1 reference
[6] doi:10.1038/s41467-026-71803-3 [code]
Charting the transition from in vitro gliogenesis to the in vivo maturation of human glial progenitor cells transplanted into the hypomyelinated mouse brain.
Journal: Nature communications
In common: reticulate, DESeq2, Plotly, 6 other tools, genetics / omics, cellular / molecular, 7 references
[7] doi:10.1038/s41593-026-02367-0 [code]
A reproducible three-dimensional model of human brain tissue to investigate physiological and disease-associated microglia phenotypes.
Journal: Nature neuroscience
In common: Monocle 3, Harmony, SingleCellExperiment, 11 other tools, cellular / molecular, 1 reference
[8] doi:10.3390/ijms27104466 [code]
Uncovering the Key Circuit FOSL2/FOS/EGR3/EGR1, Contributing to the Hyperexcitability of Excitatory Neurons in the Epileptic Temporal Cortex and Hippocampus.
Journal: International journal of molecular sciences
In common: Monocle 3, reticulate, limma, 10 other tools, genetics / omics, 2 references
[9] doi:10.1002/imt2.70163 [code]
Spatial multi-omics unveils sphingolipid metabolic reprogramming within the retinal pathological niche.
Journal: iMeta
In common: Monocle 3, Harmony, SingleCellExperiment, 11 other tools, genetics / omics, cellular / molecular
[10] doi:10.1016/j.xcrm.2026.102651 [code]
Integrative CSF profiling identifies disease-specific immune responses in leptomeningeal disease.
Journal: Cell reports. Medicine
In common: Harmony, SingleCellExperiment, reticulate, 11 other tools, genetics / omics, cellular / molecular

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.