Skip to content

Commit 3f426f3

Browse files
authored
Improve analysis summaries and cross-contrast views (#313)
* feat: add enrichment overview controls * fix: align enrichment table with plot [skip ci] * feat: reorganize analysis summaries * refactor: integrate analysis panel improvements * fix: preserve gene bar colors after averaging
1 parent bfa1a08 commit 3f426f3

50 files changed

Lines changed: 1384 additions & 378 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

R/app-composition.R

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -154,25 +154,6 @@ explorerDifferentialMenu <- function(ns, eselist, options) {
154154
))
155155
}
156156

157-
if (explorerHasExperimentData(eselist, "gene_set_analyses")) {
158-
menu <- push_to_list(menu, bslib::nav_panel("Gene set analyses",
159-
value = "geneset_analyses",
160-
moduleLayout(genesetanalysistableInput(ns("genesetanalysis"), eselist), genesetanalysistableOutput(ns("genesetanalysis"))),
161-
icon = icon("tasks", verify_fa = FALSE)
162-
))
163-
menu <- push_to_list(menu, bslib::nav_panel("Gene set barcode plots",
164-
value = "genesetbarcode",
165-
moduleLayout(genesetbarcodeplotInput(ns(options$app_type), eselist), genesetbarcodeplotOutput(ns(options$app_type))),
166-
icon = icon("barcode")
167-
))
168-
if (has_cross_contrast_enrichment(eselist)) {
169-
menu <- push_to_list(menu, bslib::nav_panel("Gene set overview",
170-
moduleLayout(enrichmentoverviewInput(ns("enrichmentoverview"), eselist), enrichmentoverviewOutput(ns("enrichmentoverview"))),
171-
icon = icon("chart-line")
172-
))
173-
}
174-
}
175-
176157
if (options$dexseq && explorerHasExperimentData(eselist, "dexseq_results")) {
177158
dexseq_output <- if (options$dexseq_output_eselist) {
178159
dexseqplotOutput(ns("deuplot"), eselist)
@@ -189,6 +170,10 @@ explorerDifferentialMenu <- function(ns, eselist, options) {
189170
}
190171

191172
if (length(eselist@contrasts) > 1) {
173+
menu <- push_to_list(menu, bslib::nav_panel("Differential summary",
174+
moduleLayout(differentialsummaryInput(ns("differentialsummary"), eselist), differentialsummaryOutput(ns("differentialsummary"))),
175+
icon = icon("chart-bar", verify_fa = FALSE)
176+
))
192177
menu <- push_to_list(menu, bslib::nav_panel("Differential set intersection",
193178
moduleLayout(upsetInput(ns("upset"), eselist), upsetOutput(ns("upset"), eselist)),
194179
icon = icon("chart-bar", verify_fa = FALSE)
@@ -199,6 +184,32 @@ explorerDifferentialMenu <- function(ns, eselist, options) {
199184
do.call(bslib::nav_menu, menu)
200185
}
201186

187+
explorerGeneSetMenu <- function(ns, eselist, options) {
188+
menu <- list(
189+
"Gene sets",
190+
bslib::nav_panel("Results",
191+
value = "geneset_analyses",
192+
moduleLayout(genesetanalysistableInput(ns("genesetanalysis"), eselist), genesetanalysistableOutput(ns("genesetanalysis"))),
193+
icon = icon("tasks", verify_fa = FALSE)
194+
),
195+
bslib::nav_panel("Barcode plots",
196+
value = "genesetbarcode",
197+
moduleLayout(genesetbarcodeplotInput(ns(options$app_type), eselist), genesetbarcodeplotOutput(ns(options$app_type))),
198+
icon = icon("barcode")
199+
)
200+
)
201+
202+
if (has_cross_contrast_enrichment(eselist)) {
203+
menu <- push_to_list(menu, bslib::nav_panel("Across-contrast overview",
204+
moduleLayout(enrichmentoverviewInput(ns("enrichmentoverview"), eselist), enrichmentoverviewOutput(ns("enrichmentoverview"))),
205+
icon = icon("chart-line")
206+
))
207+
}
208+
209+
menu$icon <- icon("layer-group")
210+
do.call(bslib::nav_menu, menu)
211+
}
212+
202213
explorerAppInput <- function(id, eselist, app_type) {
203214
options <- explorerAppOptions(app_type)
204215
ns <- NS(id)
@@ -215,6 +226,9 @@ explorerAppInput <- function(id, eselist, app_type) {
215226

216227
if (has_slot_data(eselist, "contrasts")) {
217228
menus <- push_to_list(menus, explorerDifferentialMenu(ns, eselist, options))
229+
if (explorerHasExperimentData(eselist, "gene_set_analyses")) {
230+
menus <- push_to_list(menus, explorerGeneSetMenu(ns, eselist, options))
231+
}
218232
}
219233

220234
menus <- push_to_list(menus, bslib::nav_panel("Gene info",
@@ -277,6 +291,7 @@ explorerStartModules <- function(eselist, options, heatmap_layout) {
277291
}
278292
}
279293
if (length(eselist@contrasts) > 1) {
294+
differentialsummary("differentialsummary", eselist)
280295
upset("upset", eselist)
281296
}
282297
}

R/boxplot.R

Lines changed: 174 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ boxplotInput <- function(id, eselist) {
5555
naked_fields[[1]] <- distribution_plot_filters
5656
}
5757

58-
field_sets <- c(field_sets, list(expression = expression_filters))
58+
field_sets <- c(field_sets, list(
59+
expression = expression_filters,
60+
export = simpletableInput(ns("summary"), "Distribution summary")
61+
))
5962

6063
list(naked_fields, fieldSets(ns("fieldset"), field_sets))
6164
}
@@ -93,6 +96,8 @@ boxplotOutput <- function(id) {
9396
moduleMain(
9497
"Value distributions",
9598
uiOutput(ns("quartilesPlot")),
99+
h4("Distribution summary"),
100+
simpletableOutput(ns("summary")),
96101
help = modalInput(ns(boxplot_modal$id), "help", "help")
97102
)
98103
}
@@ -171,18 +176,53 @@ boxplot <- function(id, eselist) {
171176
# leading trace, so a restyled trace index maps directly onto a group.
172177
hiddenGroups <- legendHiddenGroups(plot_source, getLevels, groupby_reactives$getGroupby, trace_offset = 0L)
173178

179+
getBoxplotStatistics <- reactive({
180+
validate(need(!is.null(input$whiskerDistance), "Waiting for whisker distance"))
181+
selected_matrix <- selectmatrix_reactives$selectMatrix()
182+
ese <- selectmatrix_reactives$getExperiment()
183+
boxplot_matrix_statistics(
184+
selected_matrix,
185+
labels = stats::setNames(id_to_label(rownames(selected_matrix), ese), rownames(selected_matrix)),
186+
whisker_distance = input$whiskerDistance,
187+
rmzeros = TRUE
188+
)
189+
})
190+
191+
getLineStatistics <- reactive({
192+
validate(need(!is.null(input$whiskerDistance), "Waiting for whisker distance"))
193+
boxplot_matrix_statistics(
194+
selectmatrix_reactives$selectMatrix(),
195+
whisker_distance = input$whiskerDistance,
196+
rmzeros = FALSE
197+
)
198+
})
199+
174200
output$sampleBoxplot <- renderPlotly({
175201
withProgress(message = "Making sample boxplot", value = 0, {
176-
selected_matrix <- selectmatrix_reactives$selectMatrix()
177-
ese <- selectmatrix_reactives$getExperiment()
178-
interactive_boxplot(selected_matrix, selectmatrix_reactives$selectColData(), groupby_reactives$getGroupby(),
179-
expressiontype = selectmatrix_reactives$getAssayMeasure(), whisker_distance = input$whiskerDistance,
180-
palette = groupby_reactives$getPalette(), hidden_groups = hiddenGroups(), source = plot_source,
181-
labels = stats::setNames(id_to_label(rownames(selected_matrix), ese), rownames(selected_matrix))
202+
interactive_boxplot_from_statistics(list(" " = getBoxplotStatistics()), selectmatrix_reactives$selectColData(), groupby_reactives$getGroupby(),
203+
expressiontype = selectmatrix_reactives$getAssayMeasure(),
204+
palette = groupby_reactives$getPalette(), hidden_groups = hiddenGroups(), source = plot_source
182205
) %>%
183206
shinyngsPlotlyConfig("boxplot", format = session$userData$plotFormat())
184207
})
185208
})
209+
210+
getDistributionSummary <- reactive({
211+
statistics <- if (identical(input$plotType, "lines")) getLineStatistics() else getBoxplotStatistics()
212+
distribution_summary_from_statistics(
213+
statistics,
214+
selectmatrix_reactives$selectColData(),
215+
groupby_reactives$getGroupby()
216+
)
217+
})
218+
219+
simpletable(
220+
"summary",
221+
downloadMatrix = getDistributionSummary,
222+
displayMatrix = getDistributionSummary,
223+
filename = "distribution_summary", rownames = FALSE,
224+
server = FALSE, initial_order = list()
225+
)
186226
})
187227
}
188228

@@ -309,6 +349,96 @@ box_summary <- function(values, labels, whisker_distance = 1.5) {
309349
)
310350
}
311351

352+
boxplot_matrix_statistics <- function(matrix, labels = NULL, whisker_distance = 1.5,
353+
should_transform = NULL, rmzeros = TRUE) {
354+
matrix <- as.matrix(matrix)
355+
transformed <- cond_log2_transform_matrix(
356+
matrix,
357+
should_transform = should_transform,
358+
rmzeros = rmzeros
359+
)
360+
row_labels <- if (is.null(labels)) {
361+
rownames(transformed)
362+
} else {
363+
resolved <- unname(labels[rownames(transformed)])
364+
resolved[is.na(resolved)] <- rownames(transformed)[is.na(resolved)]
365+
resolved
366+
}
367+
368+
statistics <- lapply(colnames(transformed), function(sample) {
369+
values <- transformed[, sample]
370+
finite <- values[is.finite(values)]
371+
summary <- box_summary(values, row_labels, whisker_distance)
372+
c(summary, list(
373+
non_missing = length(finite),
374+
minimum = if (length(finite)) min(finite) else NA_real_,
375+
mean = if (length(finite)) mean(finite) else NA_real_,
376+
maximum = if (length(finite)) max(finite) else NA_real_
377+
))
378+
})
379+
names(statistics) <- colnames(transformed)
380+
statistics
381+
}
382+
383+
distribution_sample_layout <- function(samples, experiment = NULL, groupby = NULL) {
384+
groups <- NULL
385+
if (!is.null(groupby) && !is.null(experiment) && groupby %in% colnames(experiment)) {
386+
groups <- na_replace(
387+
as.character(experiment[[groupby]][match(samples, rownames(experiment))]),
388+
"N/A"
389+
)
390+
sample_order <- order(factor(groups, levels = groupLevels(experiment, groupby)))
391+
samples <- samples[sample_order]
392+
groups <- groups[sample_order]
393+
}
394+
list(samples = samples, groups = groups)
395+
}
396+
397+
distribution_summary_from_statistics <- function(statistics, experiment = NULL, groupby = NULL) {
398+
layout <- distribution_sample_layout(names(statistics), experiment, groupby)
399+
400+
summaries <- lapply(layout$samples, function(sample) {
401+
stats <- statistics[[sample]]
402+
data.frame(
403+
Sample = sample,
404+
`Non-missing` = stats$non_missing,
405+
Minimum = stats$minimum,
406+
Q1 = stats$q1,
407+
Median = stats$median,
408+
Mean = stats$mean,
409+
Q3 = stats$q3,
410+
Maximum = stats$maximum,
411+
IQR = stats$q3 - stats$q1,
412+
Outliers = length(stats$outlier_values),
413+
check.names = FALSE, stringsAsFactors = FALSE
414+
)
415+
})
416+
summary <- do.call(rbind, summaries)
417+
rownames(summary) <- NULL
418+
419+
if (!is.null(layout$groups)) {
420+
summary <- cbind(
421+
summary["Sample"],
422+
stats::setNames(data.frame(layout$groups, stringsAsFactors = FALSE), prettify_variable_name(groupby)),
423+
summary[setdiff(colnames(summary), "Sample")]
424+
)
425+
}
426+
427+
numeric_columns <- vapply(summary, is.numeric, logical(1))
428+
summary[numeric_columns] <- lapply(summary[numeric_columns], function(values) signif(values, 5))
429+
summary
430+
}
431+
432+
distribution_summary <- function(matrix, experiment = NULL, groupby = NULL,
433+
whisker_distance = 1.5, rmzeros = TRUE) {
434+
statistics <- boxplot_matrix_statistics(
435+
matrix,
436+
whisker_distance = whisker_distance,
437+
rmzeros = rmzeros
438+
)
439+
distribution_summary_from_statistics(statistics, experiment, groupby)
440+
}
441+
312442
#' Make an interactive boxplot with coloring by experimental variable
313443
#'
314444
#' Draws a \code{plotly} box plot of the value distribution in each sample.
@@ -360,18 +490,40 @@ interactive_boxplot <- function(plotmatrices, experiment, colorby = NULL, palett
360490
plotmatrices <- list(" " = plotmatrices)
361491
}
362492

363-
# Order samples so members of the same group sit together, preserving
364-
# first-seen order of both samples and groups (mirrors ggplotify()).
493+
statistics <- lapply(plotmatrices, function(matrix) {
494+
boxplot_matrix_statistics(
495+
matrix,
496+
labels = labels,
497+
whisker_distance = whisker_distance,
498+
should_transform = should_transform,
499+
rmzeros = TRUE
500+
)
501+
})
502+
503+
interactive_boxplot_from_statistics(
504+
statistics, experiment, colorby,
505+
palette = palette, expressiontype = expressiontype,
506+
palette_name = palette_name, annotate_samples = annotate_samples,
507+
max_outliers = max_outliers, hidden_groups = hidden_groups, source = source
508+
)
509+
}
510+
511+
interactive_boxplot_from_statistics <- function(statistics, experiment, colorby = NULL,
512+
palette = NULL, expressiontype = "expression",
513+
palette_name = COLORBLIND_PALETTE_NAME,
514+
annotate_samples = FALSE, max_outliers = 500,
515+
hidden_groups = character(0), source = NULL) {
365516

366517
if (!is.null(colorby)) {
367-
groups <- na_replace(as.character(experiment[[colorby]]), "N/A")
518+
sample_layout <- distribution_sample_layout(names(statistics[[1]]), experiment, colorby)
519+
samples <- sample_layout$samples
520+
groups <- sample_layout$groups
521+
group_levels <- groupLevels(experiment, colorby)
368522
} else {
369-
groups <- rep(" ", nrow(experiment))
523+
samples <- names(statistics[[1]])
524+
groups <- rep(" ", length(samples))
525+
group_levels <- " "
370526
}
371-
group_levels <- unique(groups)
372-
sample_order <- order(factor(groups, levels = group_levels))
373-
samples <- rownames(experiment)[sample_order]
374-
groups <- groups[sample_order]
375527

376528
palette <- resolvePalette(palette, group_levels, palette_name)
377529

@@ -386,25 +538,13 @@ interactive_boxplot <- function(plotmatrices, experiment, colorby = NULL, palett
386538

387539
# Legend-click events are only routed for a single (non-subplotted) panel,
388540
# which is what the Shiny module produces.
389-
event_source <- if (length(plotmatrices) == 1) source else NULL
541+
event_source <- if (length(statistics) == 1) source else NULL
390542

391-
facet_names <- prettify_variable_name(names(plotmatrices))
543+
facet_names <- prettify_variable_name(names(statistics))
392544
yaxis_title <- expressionAxisLabel(expressiontype)
393545

394-
facet_plots <- lapply(seq_along(plotmatrices), function(i) {
395-
m <- cond_log2_transform_matrix(as.matrix(plotmatrices[[i]]), should_transform = should_transform, rmzeros = TRUE)
396-
m <- m[, samples, drop = FALSE]
397-
398-
row_labels <- if (is.null(labels)) {
399-
rownames(m)
400-
} else {
401-
resolved <- unname(labels[rownames(m)])
402-
resolved[is.na(resolved)] <- rownames(m)[is.na(resolved)]
403-
resolved
404-
}
405-
406-
stats <- lapply(samples, function(s) box_summary(m[, s], row_labels, whisker_distance))
407-
names(stats) <- samples
546+
facet_plots <- lapply(seq_along(statistics), function(i) {
547+
stats <- statistics[[i]][samples]
408548

409549
p <- if (is.null(event_source)) plot_ly() else plot_ly(source = event_source)
410550

@@ -454,9 +594,9 @@ interactive_boxplot <- function(plotmatrices, experiment, colorby = NULL, palett
454594
)
455595
}
456596

457-
xaxis <- list(title = if (length(plotmatrices) > 1) facet_names[i] else NULL)
458-
if (length(plotmatrices) > 1) {
459-
xaxis$showticklabels <- i == length(plotmatrices)
597+
xaxis <- list(title = if (length(statistics) > 1) facet_names[i] else NULL)
598+
if (length(statistics) > 1) {
599+
xaxis$showticklabels <- i == length(statistics)
460600
}
461601
if (length(visible_samples) > 0) {
462602
xaxis$categoryorder <- "array"

R/colormaker.R

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,7 @@ colormakerInput <- function(id) {
7979
colormaker <- function(id, getNumberCategories) {
8080
moduleServer(id, function(input, output, session) {
8181
getPaletteName <- reactive({
82-
validate(need(!is.null(input$palette_name), "Waiting for palette"))
83-
input$palette_name
82+
if (is.null(input$palette_name)) COLORBLIND_PALETTE_NAME else input$palette_name
8483
})
8584

8685
reactive({

0 commit comments

Comments
 (0)