diff --git a/R/dataProcessPlots.R b/R/dataProcessPlots.R index 76da2a7e..f254c799 100644 --- a/R/dataProcessPlots.R +++ b/R/dataProcessPlots.R @@ -1,3 +1,8 @@ +# Width of the Plotly canvas in CSS pixels, matching the container MSstatsShiny +# reserves for these plots. The saved PDF is sized separately, by `width`. +PLOTLY_CANVAS_WIDTH = 1400 + + #' Visualization for explanatory data analysis #' #' @description To illustrate the quantitative data after data-preprocessing and @@ -40,13 +45,21 @@ #' graph in Profile Plot and QC plot. Default is 4. #' @param text.angle angle of labels represented each condition at the top #' of graph in Profile Plot and QC plot or x-axis labeling in Condition plot. -#' Default is 0. +#' Default is 0. In Profile Plot and QC plot the rotation applies to the +#' ggplot2/PDF output only: \code{ggplotly()} does not carry the rotation of +#' the condition labels through, so \code{isPlotly = TRUE} draws them +#' horizontally and fits them to the available room instead. Condition plot +#' rotates its x-axis labels in both outputs. #' @param legend.size size of feature legend (transition-level or peptide-level) #' above graph in Profile Plot. Default is 7. #' @param dot.size.profile size of dots in profile plot. Default is 2. #' @param dot.size.condition size of dots in condition plot. Default is 3. -#' @param width width of the saved file in pixels. Default is 800 pixels. +#' @param width width of the saved PDF file in pixels, converted at 72 pixels +#' per inch, so the default 800 is an 11.1 inch page. Does not affect the Plotly +#' output, whose canvas is fixed at the width MSstatsShiny reserves for these +#' plots. #' @param height height of the saved file in pixels. Default is 600 pixels. +#' Applies to both the PDF and the Plotly output. #' @param which.Protein Protein list to draw plots. List can be names of Proteins #' or order numbers of Proteins from levels(data$FeatureLevelData$PROTEIN). #' Default is "all", which generates all plots for each protein. @@ -152,33 +165,39 @@ dataProcessPlots = function( if("original_plot" %in% names(plots)) { for(i in seq_along(plots[["original_plot"]])) { plot_i <- plots[["original_plot"]][[paste("plot",i)]] - og_plotly_plot <- .convertGgplot2Plotly(plot_i,tips=c("FEATURE","RUN","newABUNDANCE")) + og_plotly_plot <- .convertGgplot2Plotly(plot_i, tips = c("FEATURE","RUN","newABUNDANCE"), + width = PLOTLY_CANVAS_WIDTH, height = height) og_plotly_plot = .fixLegendPlotlyPlotsDataprocess(og_plotly_plot) og_plotly_plot = .fixCensoredPointsLegendProfilePlotsPlotly(og_plotly_plot) og_plotly_plot = .fixErrorBarCapsPlotly(og_plotly_plot) + og_plotly_plot = .fixConditionLabelHoverPlotly(og_plotly_plot, plot_i) if(toupper(featureName) == "NA") { og_plotly_plot = .retainCensoredDataPoints(og_plotly_plot) } + og_plotly_plot = .applyLegendPositionPlotly(og_plotly_plot) plotly_plots = c(plotly_plots, list(og_plotly_plot)) } } if("summary_plot" %in% names(plots)) { for(i in seq_along(plots[["summary_plot"]])) { plot_i <- plots[["summary_plot"]][[paste("plot",i)]] - summ_plotly_plot <- .convertGgplot2Plotly(plot_i,tips=c("FEATURE","RUN","newABUNDANCE")) + summ_plotly_plot <- .convertGgplot2Plotly(plot_i, tips = c("FEATURE","RUN","newABUNDANCE"), + width = PLOTLY_CANVAS_WIDTH, height = height) summ_plotly_plot = .fixLegendPlotlyPlotsDataprocess(summ_plotly_plot) summ_plotly_plot = .fixCensoredPointsLegendProfilePlotsPlotly(summ_plotly_plot) summ_plotly_plot = .fixErrorBarCapsPlotly(summ_plotly_plot) + summ_plotly_plot = .fixConditionLabelHoverPlotly(summ_plotly_plot, plot_i) if(toupper(featureName) == "NA") { summ_plotly_plot = .retainCensoredDataPoints(summ_plotly_plot) } + summ_plotly_plot = .applyLegendPositionPlotly(summ_plotly_plot) plotly_plots = c(plotly_plots, list(summ_plotly_plot)) } } if(address != FALSE) { - .savePlotlyPlotHTML(plotly_plots,address,"ProfilePlot" ,width, height) + .savePlotlyPlotHTML(plotly_plots,address,"ProfilePlot" ,PLOTLY_CANVAS_WIDTH, height) } plotly_plots } @@ -192,12 +211,14 @@ dataProcessPlots = function( if(isPlotly) { for(i in seq_along(plots)) { plot <- plots[[i]] - plotly_plot <- .convertGgplot2Plotly(plot) + plotly_plot <- .convertGgplot2Plotly(plot, width = PLOTLY_CANVAS_WIDTH, height = height) plotly_plot = .fixLegendPlotlyPlotsDataprocess(plotly_plot) + plotly_plot = .fixConditionLabelHoverPlotly(plotly_plot, plot) + plotly_plot = .applyLegendPositionPlotly(plotly_plot) plotly_plots[[i]] = list(plotly_plot) } if(address != FALSE) { - .savePlotlyPlotHTML(plotly_plots,address,"QCPlot" ,width, height) + .savePlotlyPlotHTML(plotly_plots,address,"QCPlot" ,PLOTLY_CANVAS_WIDTH, height) } plotly_plots <- unlist(plotly_plots, recursive = FALSE) plotly_plots @@ -213,12 +234,13 @@ dataProcessPlots = function( if(isPlotly) { for(i in seq_along(plots)) { plot <- plots[[i]] - plotly_plot <- .convertGgplot2Plotly(plot) + plotly_plot <- .convertGgplot2Plotly(plot, width = PLOTLY_CANVAS_WIDTH, height = height) plotly_plot = .fixLegendPlotlyPlotsDataprocess(plotly_plot) + plotly_plot = .applyLegendPositionPlotly(plotly_plot) plotly_plots[[i]] = list(plotly_plot) } if(address != FALSE) { - .savePlotlyPlotHTML(plotly_plots,address,"ConditionPlot" ,width, height) + .savePlotlyPlotHTML(plotly_plots,address,"ConditionPlot" ,PLOTLY_CANVAS_WIDTH, height) } plotly_plots <- unlist(plotly_plots, recursive = FALSE) plotly_plots @@ -283,9 +305,22 @@ dataProcessPlots = function( y.limup = ifelse(is.numeric(ylimUp), ylimUp, ceiling(max(processed$ABUNDANCE, na.rm = TRUE) + 3)) y.limdown = ifelse(is.numeric(ylimDown), ylimDown, -1) + # Laid out for the Plotly output only; the PDF keeps the full names, where + # text.angle still works. + condition.names = levels(tempGroupName$GROUP) + condition.layout = if (isPlotly) { + .layoutConditionLabels(condition.names, + data.table::uniqueN(processed$LABEL), + PLOTLY_CANVAS_WIDTH, text.size) + } else NULL + if (!is.numeric(ylimUp) && !is.null(condition.layout)) { + y.limup = y.limup + (condition.layout$n_lines - 1) * 0.9 + } groupName = data.frame(RUN = c(0, lineNameAxis) + groupAxis / 2 + 0.5, - ABUNDANCE = rep(y.limup - 1, length(groupAxis)), - Name = levels(tempGroupName$GROUP)) + ABUNDANCE = rep(y.limup - 0.5, length(groupAxis)), + Name = condition.names, + Label = if (is.null(condition.layout)) condition.names + else condition.layout$labels) if ("is_labeled_ref" %in% colnames(processed)) { @@ -349,7 +384,8 @@ dataProcessPlots = function( text.size, text.angle, legend.size, dot.size.profile, ss, s, cumGroupAxis, yaxis.name, - lineNameAxis, groupNametemp, dot_colors) + lineNameAxis, groupNametemp, dot_colors, + condition.layout) setTxtProgressBar(pb, i) print(profile_plot) @@ -413,7 +449,7 @@ dataProcessPlots = function( profile_plot = .makeSummaryProfilePlot( combined, is_censored, y.limdown, y.limup, x.axis.size, y.axis.size, text.size, text.angle, legend.size, dot.size.profile, cumGroupAxis, - yaxis.name, lineNameAxis, groupNametemp + yaxis.name, lineNameAxis, groupNametemp, condition.layout ) print(profile_plot) setTxtProgressBar(pb, i) @@ -479,9 +515,22 @@ dataProcessPlots = function( groupAxis = as.numeric(xtabs(~GROUP, tempGroupName)) cumGroupAxis = cumsum(groupAxis) lineNameAxis = cumGroupAxis[-nlevels(tempGroupName$GROUP)] + # Laid out for the Plotly output only; the PDF keeps the full names, where + # text.angle still works. + condition.names = levels(tempGroupName$GROUP) + condition.layout = if (isPlotly) { + .layoutConditionLabels(condition.names, + data.table::uniqueN(processed$LABEL), + PLOTLY_CANVAS_WIDTH, text.size) + } else NULL + if (!is.numeric(ylimUp) && !is.null(condition.layout)) { + y.limup = y.limup + (condition.layout$n_lines - 1) * 0.9 + } groupName = data.frame(RUN = c(0, lineNameAxis) + groupAxis / 2 + 0.5, - ABUNDANCE = rep(y.limup - 1, length(groupAxis)), - Name = levels(tempGroupName$GROUP)) + ABUNDANCE = rep(y.limup - 0.5, length(groupAxis)), + Name = condition.names, + Label = if (is.null(condition.layout)) condition.names + else condition.layout$labels) if (!isPlotly) { savePlot(address, "QCPlot", width, height) } @@ -491,7 +540,7 @@ dataProcessPlots = function( qc_plot = .makeQCPlot(processed, TRUE, y.limdown, y.limup, x.axis.size, y.axis.size, text.size, text.angle, legend.size, label.color, cumGroupAxis, groupName, lineNameAxis, - yaxis.name) + yaxis.name, condition.layout) print(qc_plot) plots[[1]] = qc_plot } @@ -514,7 +563,7 @@ dataProcessPlots = function( qc_plot = .makeQCPlot(single_protein, FALSE, y.limdown, y.limup, x.axis.size, y.axis.size, text.size, text.angle, legend.size, label.color, cumGroupAxis, groupName, - lineNameAxis, yaxis.name) + lineNameAxis, yaxis.name, condition.layout) print(qc_plot) plots[[i+1]] = qc_plot # to accomodate all proteins setTxtProgressBar(pb, i) @@ -615,39 +664,81 @@ dataProcessPlots = function( } } +#' restore the untruncated condition name in the Plotly hover +#' +#' The condition labels arrive as a single text-mode trace, so the full names +#' can be put back on hover without disturbing the drawn text. +#' @param plot converted plotly plot +#' @param ggplot_obj the ggplot it was converted from, carrying the drawn +#' `Label` and the untruncated `Name` on its condition label layer +#' @noRd +.fixConditionLabelHoverPlotly = function(plot, ggplot_obj) { + full_names = NULL + for (layer in ggplot_obj$layers) { + if (all(c("Name", "Label") %in% colnames(layer$data))) { + full_names = as.character(layer$data$Name) + break + } + } + if (is.null(full_names)) { + return(plot) + } + for (i in seq_along(plot$x$data)) { + trace = plot$x$data[[i]] + if (identical(trace$mode, "text") && + length(trace$text) == length(full_names)) { + plot$x$data[[i]]$hovertext = full_names + } + } + plot +} + #' converter for plots from ggplot to plotly +#' +#' `ggplotly()` reserves the legend band from the ggplot theme, so the theme is +#' set to the requested position here and the matching plotly placement is +#' applied by `.applyLegendPositionPlotly()` once post-processing is done. The +#' two have to agree: a theme saying "top" under a legend drawn on the right +#' leaves a dead band across the top and squeezes the panel into the corner. #' @noRd -.convertGgplot2Plotly = function(plot, tips = "all") { - converted_plot <- ggplotly(plot,tooltip = tips) - converted_plot <- plotly::layout( - converted_plot, - width = 800, # Set the width of the chart in pixels - height = 600, # Set the height of the chart in pixels - title = list( - font = list( - size = 18 - ) - ), - xaxis = list( - titlefont = list( - size = 15 # Set the font size for the x-axis label - ) - ), - legend = list( - x = 0, # Set the x position of the legend - y = -0.25, # Set the y position of the legend (negative value to move below the plot) - orientation = "h", # Horizontal orientation - font = list( - size = 12 # Set the font size for legend item labels - ), - title = list( - font = list( - size = 12 # Set the font size for the legend title - ) - ) +.convertGgplot2Plotly = function(plot, tips = "all", legend_position = "right", + width = 1400, height = 600) { + plot = plot + theme(legend.position = legend_position) + converted_plot <- ggplotly(plot, tooltip = tips, width = width, + height = height) + plotly::layout( + converted_plot, + title = list( + font = list( + size = 18 + ) + ), + xaxis = list( + titlefont = list( + size = 15 ) - ) - converted_plot + ) + ) +} + + +#' place the legend, after every other post-processing step has run +#' +#' Applied last on purpose: the `.fix*Plotly()` helpers rewrite `showlegend` on +#' individual traces, so anything deciding whether the legend is drawn has to run +#' after them or be undone by them. Mounted on the right because only the +#' vertical placements get plotly's scrolling behaviour, which is what keeps a +#' legend with hundreds of features from covering the plot. +#' +#' @param plot converted plotly plot +#' @noRd +.applyLegendPositionPlotly = function(plot) { + plotly::layout( + plot, showlegend = TRUE, + legend = list(x = 1.02, y = 1, xanchor = "left", yanchor = "top", + orientation = "v", font = list(size = 10), + title = list(font = list(size = 12))), + margin = list(t = 60)) } .retainCensoredDataPoints = function(plot) { @@ -684,16 +775,18 @@ dataProcessPlots = function( first_false_index <- which(df$legend_entries == "FALSE")[1] first_true_index <- which(df$legend_entries == "TRUE")[1] - # Update plot data for the first occurrence of "FALSE" + # Pin the two shape entries above the scrolling feature list; lower + # legendrank sorts first, and plotly's default is 1000. if (!is.na(first_false_index)) { plot$x$data[[first_false_index]]$name <- "Detected data" plot$x$data[[first_false_index]]$showlegend <- TRUE + plot$x$data[[first_false_index]]$legendrank <- 1 } - # Update plot data for the first occurrence of "TRUE" if (!is.na(first_true_index)) { plot$x$data[[first_true_index]]$name <- "Censored missing data" plot$x$data[[first_true_index]]$showlegend <- TRUE + plot$x$data[[first_true_index]]$legendrank <- 2 } plot } @@ -734,20 +827,17 @@ dataProcessPlots = function( plot } +#' wrap converted plots in sized containers for the saved HTML +#' +#' The container has to be at least as wide as the widget inside it. Pinned at +#' 800 it cropped a 1400px plot, cutting off the side legend. +#' @noRd .getPlotlyPlotHTML = function(plots, width, height) { - doc <- htmltools::tagList(lapply(plots,function(x) htmltools::div(x, style = "float:left;width:100%;"))) - # Set a specific width for each plot - plot_width <- 800 - plot_height <- 600 - - # Create a div for each plot with style settings divs <- lapply(plots, function(x) { - htmltools::div(x, style = paste0("width:", plot_width, "px; height:", plot_height, "px; margin: 10px;")) + htmltools::div(x, style = paste0("width:", width, "px; height:", height, + "px; margin: 10px;")) }) - - # Combine the divs into a tagList - doc <- htmltools::tagList(divs) - doc + htmltools::tagList(divs) } .savePlotlyPlotHTML = function(plots, address, file_name, width, height) { diff --git a/R/utils_dataprocess_plots.R b/R/utils_dataprocess_plots.R index 5036773a..c272b58f 100644 --- a/R/utils_dataprocess_plots.R +++ b/R/utils_dataprocess_plots.R @@ -33,15 +33,220 @@ } +#' Drop the prefix that every condition name shares +#' +#' Only the tail of "Study_Tissue_Timepoint_0hr" identifies the block, but the +#' shared stem is what consumes the horizontal room. The Plotly hover carries +#' the untruncated name. +#' +#' @param names character, condition names in plotting order +#' @return list with `labels` (shortened) and `prefix` (what was removed, "" when +#' nothing is shared) +#' @noRd +.stripCommonAffix = function(names) { + names = as.character(names) + unchanged = list(labels = names, prefix = "") + if (length(unique(names)) < 2L) { + return(unchanged) + } + # Split after each separator so the separator stays with the token it follows + # and the pieces can simply be pasted back together. + tokens = strsplit(names, "(?<=[_.[:space:]-])", perl = TRUE) + n_shared = 0L + repeat { + # Never consume a name entirely; a condition with no label left would be + # indistinguishable from its neighbours. + nth = vapply(tokens, function(x) { + if (length(x) > n_shared + 1L) x[n_shared + 1L] else NA_character_ + }, character(1)) + if (anyNA(nth) || length(unique(nth)) != 1L) { + break + } + n_shared = n_shared + 1L + } + if (n_shared == 0L) { + return(unchanged) + } + list(labels = vapply(tokens, function(x) { + paste(x[-seq_len(n_shared)], collapse = "") + }, character(1)), + prefix = paste(tokens[[1]][seq_len(n_shared)], collapse = "")) +} + + +#' Number of characters that fit in one condition's slot +#' +#' Width is estimated from `nchar` rather than measured. `grid::stringWidth()` is +#' exact but needs an open graphics device, which is not available while the plot +#' is being built; measuring would make the layout device-dependent and this +#' function untestable. 0.53 em per character is calibrated against +#' `graphics::strwidth()` and lands within ~7%. +#' +#' @param n_conditions number of conditions +#' @param n_facets number of facet panels actually drawn. Pass +#' `length(unique(input$LABEL))`, not `nlevels()`: LABEL is a factor over the +#' whole table, so `nlevels()` reports 2 for a protein carrying only one label +#' while `facet_grid()` draws a single panel. +#' @param width width of the canvas in pixels, read as CSS pixels at 96dpi +#' @param text.size size of the condition labels +#' @return integer, at least 1 +#' @noRd +.conditionSlotChars = function(n_conditions, n_facets, width, text.size) { + if (!is.numeric(width) || length(width) != 1L || is.na(width) || + width <= 0 || n_conditions < 1L) { + return(.Machine$integer.max) + } + # ~1.1in of the canvas goes to the y-axis title, tick labels and margins; + # what is left is split across the facets and then across the conditions. + panel_in = (width / 96 - 1.1) / max(n_facets, 1L) + # Only fill part of the slot: a label filling it exactly touches its + # neighbours, and the end labels overhang the panel edge. + slot_in = 0.85 * panel_in / n_conditions + char_in = text.size * ggplot2::.pt * 0.53 / 72 + if (slot_in <= 0 || char_in <= 0) { + return(1L) + } + max(1L, as.integer(floor(slot_in / char_in))) +} + + +#' Shorten a string to `chars`, keeping both ends +#' +#' A head-only truncation is what makes two conditions sharing a stem render as +#' the same label, so the identifying tail is kept too. +#' +#' @param x character(1) +#' @param chars maximum characters to return +#' @return character(1), `x` unchanged when it already fits +#' @noRd +.ellipsize = function(x, chars) { + if (nchar(x) <= chars) { + return(x) + } + if (chars <= 3L) { + return(substr(x, 1L, max(1L, chars))) + } + keep = chars - 3L + head_n = keep %/% 2L + tail_n = keep - head_n + paste0(substr(x, 1L, head_n), "...", + substr(x, nchar(x) - tail_n + 1L, nchar(x))) +} + + +#' Wrap condition names onto several lines so they fit their slot +#' +#' `strwrap()` breaks only at whitespace and condition names are usually +#' underscore-delimited, so separators are turned into break opportunities here. +#' A single token wider than the slot cannot be broken and is shortened. Past +#' `max_lines` the remainder is folded into the last line rather than spilling +#' down the axis. +#' +#' @param names character, condition names +#' @param chars maximum characters per line +#' @param max_lines maximum lines a single label may occupy +#' @return character, `names` unchanged when they all already fit +#' @noRd +.wrapConditionLabels = function(names, chars, max_lines = 3L) { + names = as.character(names) + if (all(nchar(names) <= chars)) { + return(names) + } + vapply(names, function(name) { + tokens = regmatches(name, gregexpr("[^_.[:space:]-]+[_.[:space:]-]*", + name))[[1]] + if (length(tokens) == 0L) { + tokens = name + } + tokens = vapply(tokens, .ellipsize, character(1), chars = chars, + USE.NAMES = FALSE) + lines = character(0) + current = "" + for (token in tokens) { + candidate = paste0(current, token) + if (nchar(trimws(candidate)) > chars && nzchar(current)) { + lines = c(lines, current) + current = token + } else { + current = candidate + } + } + lines = c(lines, current) + if (length(lines) > max_lines) { + kept = lines[seq_len(max_lines - 1L)] + rest = paste(lines[max_lines:length(lines)], collapse = "") + lines = c(kept, .ellipsize(rest, chars)) + } + paste(lines, collapse = "\n") + }, character(1), USE.NAMES = FALSE) +} + + +#' Lay out condition labels so they do not overlap +#' +#' Applies the three mitigations in order of how much they cost the reader: +#' drop the shared stem, then shrink the font, then wrap. Each is a no-op when +#' the labels already fit, so a plot that renders correctly today is unchanged. +#' +#' @inheritParams .conditionSlotChars +#' @param names character, condition names in plotting order +#' @return list with `labels`, the `size` to draw them at, and the `n_lines` +#' they occupy +#' @noRd +.layoutConditionLabels = function(names, n_facets, width, text.size) { + labels = as.character(names) + unchanged = list(labels = labels, size = text.size, n_lines = 1L) + n_conditions = length(labels) + if (n_conditions < 2L) { + return(unchanged) + } + if (max(nchar(labels)) <= + .conditionSlotChars(n_conditions, n_facets, width, text.size)) { + return(unchanged) + } + stripped = .stripCommonAffix(labels) + if (nzchar(stripped$prefix)) { + labels = stripped$labels + } + # Shrink before wrapping: one legible line beats two cramped ones. The floor + # is where shrinking stops buying fit and starts buying illegibility. + size = text.size + repeat { + chars = .conditionSlotChars(n_conditions, n_facets, width, size) + if (max(nchar(labels)) <= chars || size <= 2.5) { + break + } + size = size - 0.25 + } + wrapped = .wrapConditionLabels(labels, chars) + # A shortening that collapses two conditions onto one string is worse than + # a crowded axis, so the full names are kept instead. + if (anyDuplicated(wrapped) == 0L) { + labels = wrapped + } + list(labels = labels, size = size, + n_lines = max(lengths(strsplit(labels, "\n", fixed = TRUE)))) +} + + +#' Font size the condition labels were laid out for, or the caller's +#' @param layout result of `.layoutConditionLabels()`, or NULL +#' @param text.size size to fall back to +#' @noRd +.conditionTextSize = function(layout, text.size) { + if (is.null(layout$size)) text.size else layout$size +} + #' Create profile plot #' @inheritParams dataProcessPlots #' @param input data.table #' @param is_censored TRUE if censored values were imputed -#' @keywords internal +#' @noRd .makeProfilePlot = function( input, is_censored, featureName, y.limdown, y.limup, x.axis.size, y.axis.size, text.size, text.angle, legend.size, dot.size.profile, - ss, s, cumGroupAxis, yaxis.name, lineNameAxis, groupNametemp, dot_colors + ss, s, cumGroupAxis, yaxis.name, lineNameAxis, groupNametemp, dot_colors, + condition.layout = NULL ) { RUN = ABUNDANCE = Name = NULL @@ -93,13 +298,14 @@ profile_plot = profile_plot + scale_linetype_manual(values = ss, guide = "none") profile_plot = profile_plot + - scale_x_continuous('MS runs', breaks = cumGroupAxis) + + scale_x_continuous("MS runs", breaks = cumGroupAxis) + scale_y_continuous(yaxis.name, limits = c(y.limdown, y.limup)) + geom_vline(xintercept = lineNameAxis + 0.5, colour = "grey", linetype = "longdash") + labs(title = unique(input$PROTEIN)) + - geom_text(data = groupNametemp, aes(x = .data$RUN, y = .data$ABUNDANCE, label = .data$Name), - size = text.size, + geom_text(data = groupNametemp, aes(x = .data$RUN, y = .data$ABUNDANCE, label = .data$Label), + size = .conditionTextSize(condition.layout, text.size), angle = text.angle, + vjust = 1, color = "black") + theme_msstats("PROFILEPLOT", x.axis.size, y.axis.size, legend.size) @@ -146,11 +352,11 @@ #' Make summary profile plot #' @inheritParams dataProcessPlots #' @inheritParams .makeProfilePlot -#' @keywords internal +#' @noRd .makeSummaryProfilePlot = function( input, is_censored, y.limdown, y.limup, x.axis.size, y.axis.size, text.size, text.angle, legend.size, dot.size.profile, cumGroupAxis, - yaxis.name, lineNameAxis, groupNametemp + yaxis.name, lineNameAxis, groupNametemp, condition.layout = NULL ) { RUN = ABUNDANCE = Name = NULL @@ -199,9 +405,10 @@ geom_vline(xintercept = lineNameAxis + 0.5, colour = "grey", linetype = "longdash") + labs(title = unique(input$PROTEIN)) + - geom_text(data = groupNametemp, aes(x = .data$RUN, y = .data$ABUNDANCE, label = .data$Name), - size = text.size, + geom_text(data = groupNametemp, aes(x = .data$RUN, y = .data$ABUNDANCE, label = .data$Label), + size = .conditionTextSize(condition.layout, text.size), angle = text.angle, + vjust = 1, color = "black") + theme_msstats("PROFILEPLOT", x.axis.size, y.axis.size, legend.size, legend.title = element_blank()) @@ -228,11 +435,11 @@ #' @inherit dataProcessPlots #' @param input data.table #' @param all_proteins character vector of protein names -#' @keywords internal +#' @noRd .makeQCPlot = function( input, all_proteins, y.limdown, y.limup, x.axis.size, y.axis.size, text.size, text.angle, legend.size, label.color, cumGroupAxis, groupName, - lineNameAxis, yaxis.name + lineNameAxis, yaxis.name, condition.layout = NULL ) { RUN = ABUNDANCE = Name = NULL @@ -252,8 +459,9 @@ geom_vline(xintercept = lineNameAxis + 0.5, colour = "grey", linetype = "longdash") + labs(title = plot_title) + - geom_text(data = groupName, aes(x = .data$RUN, y = .data$ABUNDANCE, label = .data$Name), - size = text.size, angle = text.angle, color = "black") + + geom_text(data = groupName, aes(x = .data$RUN, y = .data$ABUNDANCE, label = .data$Label), + size = .conditionTextSize(condition.layout, text.size), + angle = text.angle, vjust = 1, color = "black") + theme_msstats("QCPLOT", x.axis.size, y.axis.size, legend_size = NULL) diff --git a/inst/NEWS.rd b/inst/NEWS.rd index a7422072..73d01bf9 100644 --- a/inst/NEWS.rd +++ b/inst/NEWS.rd @@ -2,6 +2,14 @@ \title{News for package, \pkg{MSstats}} \encoding{UTF-8} +\section{Version 4.22.0 (in development)}{ + \itemize{ + \item \strong{Profile and QC plots}: Condition names no longer overlap each other. When a name is wider than the horizontal room its condition is given, the stem shared by every condition is dropped, the label font is reduced, and the remainder is wrapped onto at most three lines. Plots whose condition labels already fit are unchanged. In the Plotly output the untruncated name is available on hover. + \item \strong{Profile and QC plots}: In the Plotly output the feature legend is now mounted beside the plot rather than above it, where Plotly makes an over-tall legend scrollable. Proteins with many features no longer have the legend cover the plot, and legend entries are no longer silently dropped. + \item \strong{Bug fix}: In the Plotly output \code{dataProcessPlots} ignored \code{height}, and the saved HTML pinned every plot inside a fixed 800x600 container, cropping anything wider than it. The container is now sized to the plot and \code{height} is honoured. + } +} + \section{Version 4.20.0 (2026-04-23)}{ \itemize{ \item \strong{Protein turnover analysis}: Added support for multi-label summarization, enabling experiments that use multiple isotope labels to quantify protein synthesis and degradation rates. Each isotope label is now summarized independently, giving more accurate per-label abundance estimates. diff --git a/inst/tinytest/test_dataProcessPlots.R b/inst/tinytest/test_dataProcessPlots.R index 6e6352fb..9bfa679d 100644 --- a/inst/tinytest/test_dataProcessPlots.R +++ b/inst/tinytest/test_dataProcessPlots.R @@ -81,3 +81,103 @@ invisible(capture.output(suppressWarnings( ))) expect_true(any(grepl("ConditionPlot.*\\.zip$", list.files(tmp_dir2)))) unlink(tmp_dir2, recursive = TRUE) + +# Test 10: the Plotly legend is mounted on the right ------------------------ +# Regression test. The placement used to disagree with the ggplot theme, so +# ggplotly reserved a band that nothing occupied and squeezed the panel into the +# corner. plotly::layout() defers into layoutAttrs, so this has to be checked +# after plotly_build(). + +legend_spec = function() { + plot = suppressWarnings( + dataProcessPlots(QuantData, type = "ProfilePlot", + which.Protein = protein_name, summaryPlot = FALSE, + address = FALSE, isPlotly = TRUE) + )[[1]] + plotly::plotly_build(plot)$x$layout +} + +spec_right = legend_spec() +expect_true(spec_right$showlegend) +expect_equal(spec_right$legend$orientation, "v") +expect_true(spec_right$legend$x > 1) + +# Test 11: text.angle no longer suppresses the Plotly label layout ----------- +# ggplotly() does not carry geom_text() rotation through, so a rotated Plotly +# plot is drawn horizontally and has exactly the crowding problem the layout +# exists to solve. The layout therefore runs whatever text.angle says, and the +# untruncated name stays on hover. + +QuantDataLong = QuantData +long_group = function(x) factor(paste0("Study_Tissue_Timepoint_", x)) +QuantDataLong$FeatureLevelData$GROUP = long_group(QuantDataLong$FeatureLevelData$GROUP) +QuantDataLong$ProteinLevelData$GROUP = long_group(QuantDataLong$ProteinLevelData$GROUP) + +condition_label_trace = function(text.angle) { + plot = suppressWarnings( + dataProcessPlots(QuantDataLong, type = "ProfilePlot", + which.Protein = protein_name, summaryPlot = FALSE, + address = FALSE, isPlotly = TRUE, + text.angle = text.angle) + )[[1]] + traces = plotly::plotly_build(plot)$x$data + Filter(function(trace) identical(trace$mode, "text"), traces)[[1]] +} + +for (angle in c(0, 90)) { + trace = condition_label_trace(angle) + expect_true(all(nchar(trace$text) < nchar(trace$hovertext))) + expect_true(all(grepl("^Study_Tissue_Timepoint_", trace$hovertext))) +} + +# Test 12: the ggplot2/PDF path keeps the full names and honours text.angle -- +# Nothing is laid out there, so condition.layout is NULL. This exercises that +# branch: the labels fall back to the full condition names and no headroom is +# added. + +tmp_dir3 = tempfile("msstats_dataprocessplots_pdf_") +dir.create(tmp_dir3) +expect_silent(suppressWarnings( + dataProcessPlots(QuantDataLong, type = "ProfilePlot", + which.Protein = protein_name, summaryPlot = FALSE, + address = paste0(tmp_dir3, "/"), text.angle = 90) +)) +expect_true(any(grepl("ProfilePlot.*\\.pdf$", list.files(tmp_dir3)))) +unlink(tmp_dir3, recursive = TRUE) + +# Test 13: there is no separate Plotly width argument ------------------------ +# The Plotly canvas width is an internal constant, not something the caller +# sizes; width is the PDF page. + +expect_false("width.plotly" %in% names(formals(dataProcessPlots))) + +# Test 14: the saved HTML container is sized to the plot it holds ------------ +# Regression test. The container was pinned at 800px while the widget inside it +# was 1400px wide, so the right-hand side of every saved plot -- which is where +# the feature legend is mounted -- fell outside the box. + +tmp_dir4 = tempfile("msstats_dataprocessplots_html_") +dir.create(tmp_dir4) +invisible(capture.output(suppressWarnings( + dataProcessPlots(QuantData, type = "ProfilePlot", which.Protein = protein_name, + summaryPlot = FALSE, address = paste0(tmp_dir4, "/"), + isPlotly = TRUE) +))) +zip_path = list.files(tmp_dir4, pattern = "\\.zip$", full.names = TRUE)[1] +unzip(zip_path, exdir = file.path(tmp_dir4, "unzipped")) +html_path = list.files(file.path(tmp_dir4, "unzipped"), pattern = "\\.html$", + full.names = TRUE, recursive = TRUE)[1] +html = paste(readLines(html_path, warn = FALSE), collapse = "\n") + +# The container div spaces its declarations, the widget div does not, so the +# two are told apart by the space after the semicolon. +px = function(pattern) { + as.integer(sub("^width:([0-9]+)px.*", "\\1", + regmatches(html, regexpr(pattern, html)))) +} +container_width = px("width:[0-9]+px; height:[0-9]+px; margin") +widget_width = px("width:[0-9]+px;height:[0-9]+px") + +expect_equal(container_width, widget_width) +expect_true(container_width >= 1400L) +unlink(tmp_dir4, recursive = TRUE) diff --git a/inst/tinytest/test_utils_dataprocess_plots.R b/inst/tinytest/test_utils_dataprocess_plots.R new file mode 100644 index 00000000..5088133f --- /dev/null +++ b/inst/tinytest/test_utils_dataprocess_plots.R @@ -0,0 +1,206 @@ +# Condition label layout helpers. +# +# These decide what is drawn in place of a condition name that does not fit the +# horizontal room it is given. They are pure functions of the names and the +# canvas geometry, so they are tested directly rather than through a rendered +# plot. + +strip = MSstats:::.stripCommonAffix +slot_chars = MSstats:::.conditionSlotChars +wrap = MSstats:::.wrapConditionLabels +layout_labels = MSstats:::.layoutConditionLabels + +# Test .stripCommonAffix ---------------------------------------------------- + +# Test 1: the shared stem is removed and reported +result = strip(c("Study_Tissue_Timepoint_0hr", "Study_Tissue_Timepoint_12hrs")) +expect_equal(result$labels, c("0hr", "12hrs")) +expect_equal(result$prefix, "Study_Tissue_Timepoint_") + +# Test 2: names sharing nothing are left alone +result = strip(c("Alpha", "Beta")) +expect_equal(result$labels, c("Alpha", "Beta")) +expect_equal(result$prefix, "") + +# Test 3: a shared stem that is not on a separator boundary is not split. +# "Control" and "Contrast" share "Cont", but chopping mid-token would leave +# labels that do not correspond to anything in the data. +result = strip(c("Control_1", "Contrast_1")) +expect_equal(result$prefix, "") + +# Test 4: identical names are left alone rather than reduced to nothing +result = strip(c("same", "same")) +expect_equal(result$labels, c("same", "same")) +expect_equal(result$prefix, "") + +# Test 5: a name is never consumed entirely. Every name here starts with the +# whole of the first, so stripping greedily would leave an empty label. +result = strip(c("A_B", "A_B_C")) +expect_true(all(nzchar(result$labels))) + +# Test 6: a single condition has no shared stem to speak of +result = strip("OnlyOne") +expect_equal(result$labels, "OnlyOne") +expect_equal(result$prefix, "") + +# Test 7: separators other than underscore are honoured +expect_equal(strip(c("run.a", "run.b"))$prefix, "run.") +expect_equal(strip(c("run a", "run b"))$prefix, "run ") +expect_equal(strip(c("run-a", "run-b"))$prefix, "run-") + +# Test .conditionSlotChars -------------------------------------------------- + +# Test 8: more conditions in the same canvas means fewer characters each +expect_true(slot_chars(20, 1, 1400, 4) < slot_chars(5, 1, 1400, 4)) + +# Test 9: a wider canvas means more characters +expect_true(slot_chars(10, 1, 1400, 4) > slot_chars(10, 1, 800, 4)) + +# Test 10: splitting the canvas across facets means fewer characters +expect_true(slot_chars(10, 2, 1400, 4) < slot_chars(10, 1, 1400, 4)) + +# Test 11: a larger font means fewer characters +expect_true(slot_chars(10, 1, 1400, 8) < slot_chars(10, 1, 1400, 4)) + +# Test 12: never returns less than one character, however cramped +expect_true(slot_chars(500, 4, 200, 12) >= 1L) + +# Test 13: a canvas of unknown width imposes no limit, so nothing is shortened +expect_equal(slot_chars(10, 1, NA, 4), .Machine$integer.max) +expect_equal(slot_chars(10, 1, 0, 4), .Machine$integer.max) + +# Test .wrapConditionLabels ------------------------------------------------- + +# Test 14: names that already fit are returned untouched +expect_equal(wrap(c("0hr", "12hrs"), 10), c("0hr", "12hrs")) + +# Test 15: wrapping happens at separators, not mid-token +expect_equal(wrap("aaaa_bbbb_cccc", 6), "aaaa_\nbbbb_\ncccc") + +# Test 16: a single token wider than the slot cannot be broken, so it is +# shortened to exactly the slot width, keeping both ends. Head-only truncation +# would drop the tail, which is the part that tells two conditions apart. +result = wrap("ABCDEFGHIJKLMNOP", 6) +expect_equal(result, "A...OP") +expect_equal(nchar(result), 6L) + +# Test 17: every wrapped line respects the limit +lines = unlist(strsplit(wrap("alpha_beta_gamma_delta", 8), "\n", fixed = TRUE)) +expect_true(all(nchar(lines) <= 8L)) + +# Test .layoutConditionLabels ----------------------------------------------- + +short = c("1", "2", "3") +long = paste0("Study_Tissue_Timepoint_", c("0hr", "12hrs", "168hrs")) + +# Test 18: labels that already fit are returned unchanged, at the caller's font +# size, on one line. This is the path every dataset that renders correctly today +# takes. +result = layout_labels(short, 1, 1400, 4) +expect_equal(result$labels, short) +expect_equal(result$size, 4) +expect_equal(result$n_lines, 1L) + +# Test 19: labels that do not fit are shortened. The x-axis title stays the +# standard "MS runs" whatever the layout does, so it is not asserted on here. +result = layout_labels(long, 2, 800, 4) +expect_equal(result$labels, c("0hr", "12hrs", "168hrs")) + +# Test 20: the layout takes no text.angle. It is only ever computed for the +# Plotly output, which ggplotly() draws horizontally whatever the caller asked +# for, so rotation cannot be a mitigation and cannot suppress one either. +expect_false("text.angle" %in% names(formals(layout_labels))) + +# Test 21: a single condition cannot collide with anything +result = layout_labels("OnlyOneVeryLongConditionName", 1, 400, 4) +expect_equal(result$labels, "OnlyOneVeryLongConditionName") + +# Test 22: when stripping cannot help, the font shrinks rather than giving up, +# but not below the legibility floor +no_stem = c("AlphaHepatocyteBaseline", "BetaRenalCortexStimulated", + "GammaCardiacTissue") +result = layout_labels(no_stem, 2, 800, 4) +expect_true(result$size < 4) +expect_true(result$size >= 2.5) + +# Test 23: the drawn labels stay distinguishable from one another even in that +# worst case, which is the whole point of the exercise +expect_equal(length(unique(result$labels)), length(no_stem)) + +# Test 24: n_lines reports the tallest label, so the caller knows how much +# headroom to add above the data +result = layout_labels(c("alpha_beta_gamma", "delta_epsilon_zeta"), 1, 300, 4) +expect_equal(result$n_lines, + max(lengths(strsplit(result$labels, "\n", fixed = TRUE)))) + +# Test 25: wrapping stops at three lines however cramped the canvas gets. +# Uncapped, this fixture reached 4 lines at 900px and 8 at 400px. +long_condition_names = c( + "0hr_0hr_20240101_XX_Sample_ctrl_f1_merged", + "12hrs_12hrs_20240101_Sample_Tissue_12h_f1_merged", + "168hrs_168hrs_202401011_XX_Sample_168h_f1_merged", + "1hr_1hr_20240101_XX_Sample_1h_f1_merged", + "24hrs_24hrs_20240101_XX_Sample_24h_f1_merged", + "48hrs_48hrs_20240101_XX_Sample_48h_f1_merged", + "4hr_4hr_20240101_XX_Sample_4h_f1_merged", + "96hrs_96hrs_20240101_XX_Sample_96h_f1_merged") +for (canvas in c(1400, 900, 600, 400)) { + result = layout_labels(long_condition_names, 1, canvas, 4) + expect_true(result$n_lines <= 3L) +} + +# Test 26: and the conditions stay tellable apart at every one of those widths +for (canvas in c(1400, 900, 600, 400)) { + result = layout_labels(long_condition_names, 1, canvas, 4) + expect_equal(length(unique(result$labels)), length(long_condition_names)) +} + +# Test 27: names that differ only in their tail survive the fold onto the last +# line. Keeping the first three lines and dropping the rest would render these +# two conditions as the same string. +shared_head = c("Cohort_Baseline_Liver_Replicate_Alpha_Treated", + "Cohort_Baseline_Liver_Replicate_Alpha_Control") +expect_equal(length(unique(wrap(shared_head, 10))), 2L) + +# Test 28: when no amount of shortening keeps the conditions distinct, the full +# names are drawn instead. A crowded axis is recoverable; two conditions sharing +# one label is not. These share a stem with no separator to break on, so the +# wrapper alone collapses them below eight characters. +covariates = c("DiseaseGroupMale", "DiseaseGroupFemale") +expect_equal(length(unique(wrap(covariates, 8))), 1L) +for (canvas in c(800, 400, 200, 120)) { + result = layout_labels(covariates, 1, canvas, 4) + expect_equal(length(unique(result$labels)), 2L) +} + +# Covariate designs --------------------------------------------------------- +# "Condition_Gender" is a very common way to encode a covariate, and the +# condition half of the name must survive. + +covariate_design = c("Disease_Male", "Disease_Female", + "Control_Male", "Control_Female") + +# Test 29: no single stem is shared by every name here -- Disease_ and Control_ +# each cover only half -- so nothing is dropped. +expect_equal(strip(covariate_design)$prefix, "") +expect_equal(strip(covariate_design)$labels, covariate_design) + +# Test 30: and that holds through the whole layout at any canvas width. The +# conditions stay distinct and every label still names its condition, even at +# widths cramped enough to force wrapping and truncation. +for (canvas in c(1400, 800, 500, 300, 200)) { + result = layout_labels(covariate_design, 1, canvas, 4) + expect_equal(length(unique(result$labels)), 4L) + expect_true(all(grepl("^(Dis|Con)", result$labels))) +} + +# Test 31: a third factor does not make the strip loop over-consume. "Week1" is +# shared by every name but is not a leading token, so it stays put. +expect_equal(strip(paste0(covariate_design, "_Week1"))$prefix, "") + +# Test 32: when every condition genuinely does share a leading stem it is +# dropped, covariate or not. Only reachable once the labels no longer fit, and +# only in the Plotly output, where the hover still carries the full name. +two_level = c("Disease_Male", "Disease_Female") +expect_equal(layout_labels(two_level, 1, 1400, 4)$labels, two_level) +expect_equal(layout_labels(two_level, 1, 300, 4)$labels, c("Male", "Female")) diff --git a/man/dataProcessPlots.Rd b/man/dataProcessPlots.Rd index 2d1f08ca..908a2602 100644 --- a/man/dataProcessPlots.Rd +++ b/man/dataProcessPlots.Rd @@ -70,7 +70,11 @@ graph in Profile Plot and QC plot. Default is 4.} \item{text.angle}{angle of labels represented each condition at the top of graph in Profile Plot and QC plot or x-axis labeling in Condition plot. -Default is 0.} +Default is 0. In Profile Plot and QC plot the rotation applies to the +ggplot2/PDF output only: \code{ggplotly()} does not carry the rotation of +the condition labels through, so \code{isPlotly = TRUE} draws them +horizontally and fits them to the available room instead. Condition plot +rotates its x-axis labels in both outputs.} \item{legend.size}{size of feature legend (transition-level or peptide-level) above graph in Profile Plot. Default is 7.} @@ -79,9 +83,13 @@ above graph in Profile Plot. Default is 7.} \item{dot.size.condition}{size of dots in condition plot. Default is 3.} -\item{width}{width of the saved file in pixels. Default is 800 pixels.} +\item{width}{width of the saved PDF file in pixels, converted at 72 pixels +per inch, so the default 800 is an 11.1 inch page. Does not affect the Plotly +output, whose canvas is fixed at the width MSstatsShiny reserves for these +plots.} -\item{height}{height of the saved file in pixels. Default is 600 pixels.} +\item{height}{height of the saved file in pixels. Default is 600 pixels. +Applies to both the PDF and the Plotly output.} \item{which.Protein}{Protein list to draw plots. List can be names of Proteins or order numbers of Proteins from levels(data$FeatureLevelData$PROTEIN). diff --git a/man/dot-makeConditionPlot.Rd b/man/dot-makeConditionPlot.Rd index dbc11a37..160ca277 100644 --- a/man/dot-makeConditionPlot.Rd +++ b/man/dot-makeConditionPlot.Rd @@ -39,7 +39,11 @@ graph in Profile Plot and QC plot. Default is 4.} \item{text.angle}{angle of labels represented each condition at the top of graph in Profile Plot and QC plot or x-axis labeling in Condition plot. -Default is 0.} +Default is 0. In Profile Plot and QC plot the rotation applies to the +ggplot2/PDF output only: \code{ggplotly()} does not carry the rotation of +the condition labels through, so \code{isPlotly = TRUE} draws them +horizontally and fits them to the available room instead. Condition plot +rotates its x-axis labels in both outputs.} \item{legend.size}{size of feature legend (transition-level or peptide-level) above graph in Profile Plot. Default is 7.} diff --git a/man/dot-makeProfilePlot.Rd b/man/dot-makeProfilePlot.Rd deleted file mode 100644 index c259fa28..00000000 --- a/man/dot-makeProfilePlot.Rd +++ /dev/null @@ -1,57 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/utils_dataprocess_plots.R -\name{.makeProfilePlot} -\alias{.makeProfilePlot} -\title{Create profile plot} -\usage{ -.makeProfilePlot( - input, - is_censored, - featureName, - y.limdown, - y.limup, - x.axis.size, - y.axis.size, - text.size, - text.angle, - legend.size, - dot.size.profile, - ss, - s, - cumGroupAxis, - yaxis.name, - lineNameAxis, - groupNametemp, - dot_colors -) -} -\arguments{ -\item{input}{data.table} - -\item{is_censored}{TRUE if censored values were imputed} - -\item{featureName}{for "ProfilePlot" only, "Transition" (default) means -printing feature legend in transition-level; "Peptide" means printing feature -legend in peptide-level; "NA" means no feature legend printing.} - -\item{x.axis.size}{size of x-axis labeling for "Run" in Profile Plot and -QC Plot, and "Condition" in Condition Plot. Default is 10.} - -\item{y.axis.size}{size of y-axis labels. Default is 10.} - -\item{text.size}{size of labels represented each condition at the top of -graph in Profile Plot and QC plot. Default is 4.} - -\item{text.angle}{angle of labels represented each condition at the top -of graph in Profile Plot and QC plot or x-axis labeling in Condition plot. -Default is 0.} - -\item{legend.size}{size of feature legend (transition-level or peptide-level) -above graph in Profile Plot. Default is 7.} - -\item{dot.size.profile}{size of dots in profile plot. Default is 2.} -} -\description{ -Create profile plot -} -\keyword{internal} diff --git a/man/dot-makeQCPlot.Rd b/man/dot-makeQCPlot.Rd deleted file mode 100644 index 98b17d6c..00000000 --- a/man/dot-makeQCPlot.Rd +++ /dev/null @@ -1,82 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/utils_dataprocess_plots.R -\name{.makeQCPlot} -\alias{.makeQCPlot} -\title{Make QC plot} -\usage{ -.makeQCPlot( - input, - all_proteins, - y.limdown, - y.limup, - x.axis.size, - y.axis.size, - text.size, - text.angle, - legend.size, - label.color, - cumGroupAxis, - groupName, - lineNameAxis, - yaxis.name -) -} -\arguments{ -\item{input}{data.table} - -\item{all_proteins}{character vector of protein names} - -\item{x.axis.size}{size of x-axis labeling for "Run" in Profile Plot and -QC Plot, and "Condition" in Condition Plot. Default is 10.} - -\item{y.axis.size}{size of y-axis labels. Default is 10.} - -\item{text.size}{size of labels represented each condition at the top of -graph in Profile Plot and QC plot. Default is 4.} - -\item{text.angle}{angle of labels represented each condition at the top -of graph in Profile Plot and QC plot or x-axis labeling in Condition plot. -Default is 0.} - -\item{legend.size}{size of feature legend (transition-level or peptide-level) -above graph in Profile Plot. Default is 7.} -} -\description{ -To illustrate the quantitative data after data-preprocessing and -quality control of MS runs, dataProcessPlots takes the quantitative data from -function (\code{\link{dataProcess}}) as input and automatically generate -three types of figures in pdf files as output : -(1) profile plot (specify "ProfilePlot" in option type), -to identify the potential sources of variation for each protein; -(2) quality control plot (specify "QCPlot" in option type), -to evaluate the systematic bias between MS runs; -(3) mean plot for conditions (specify "ConditionPlot" in option type), -to illustrate mean and variability of each condition per protein. -} -\details{ -\itemize{ -\item{Profile Plot : identify the potential sources of variation of each protein. QuantData$FeatureLevelData is used for plots. X-axis is run. Y-axis is log-intensities of transitions. Reference/endogenous signals are in the left/right panel. Line colors indicate peptides and line types indicate transitions. In summarization plots, gray dots and lines are the same as original profile plots with QuantData$FeatureLevelData. Dark dots and lines are for summarized intensities from QuantData$ProteinLevelData.} -\item{QC Plot : illustrate the systematic bias between MS runs. After normalization, the reference signals for all proteins should be stable across MS runs. QuantData$FeatureLevelData is used for plots. X-axis is run. Y-axis is log-intensities of transition. Reference/endogenous signals are in the left/right panel. The pdf file contains (1) QC plot for all proteins and (2) QC plots for each protein separately.} -\item{Condition Plot : illustrate the systematic difference between conditions. Summarized intensnties from QuantData$ProteinLevelData are used for plots. X-axis is condition. Y-axis is summarized log transformed intensity. If scale is TRUE, the levels of conditions is scaled according to its actual values at x-axis. Red points indicate the mean for each condition. If interval is "CI", blue error bars indicate the confidence interval with 0.95 significant level for each condition. If interval is "SD", blue error bars indicate the standard deviation for each condition.The interval is not related with model-based analysis.} -} -The input of this function is the quantitative data from function \code{\link{dataProcess}}. -} -\examples{ -# Consider quantitative data (i.e. QuantData) from a yeast study with ten time points of interests, -# three biological replicates, and no technical replicates which is a time-course experiment. -# The goal is to provide pre-analysis visualization by automatically generate two types of figures -# in two separate pdf files. -# Protein IDHC (gene name IDP2) is differentially expressed in time point 1 and time point 7, -# whereas, Protein PMG2 (gene name GPM2) is not. - -QuantData<-dataProcess(SRMRawData, use_log_file = FALSE) -head(QuantData$FeatureLevelData) -# Profile plot -dataProcessPlots(data=QuantData,type="ProfilePlot") -# Quality control plot -dataProcessPlots(data=QuantData,type="QCPlot") -# Quantification plot for conditions -dataProcessPlots(data=QuantData,type="ConditionPlot") - -} -\keyword{internal} diff --git a/man/dot-makeSummaryProfilePlot.Rd b/man/dot-makeSummaryProfilePlot.Rd deleted file mode 100644 index 94a2f935..00000000 --- a/man/dot-makeSummaryProfilePlot.Rd +++ /dev/null @@ -1,49 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/utils_dataprocess_plots.R -\name{.makeSummaryProfilePlot} -\alias{.makeSummaryProfilePlot} -\title{Make summary profile plot} -\usage{ -.makeSummaryProfilePlot( - input, - is_censored, - y.limdown, - y.limup, - x.axis.size, - y.axis.size, - text.size, - text.angle, - legend.size, - dot.size.profile, - cumGroupAxis, - yaxis.name, - lineNameAxis, - groupNametemp -) -} -\arguments{ -\item{input}{data.table} - -\item{is_censored}{TRUE if censored values were imputed} - -\item{x.axis.size}{size of x-axis labeling for "Run" in Profile Plot and -QC Plot, and "Condition" in Condition Plot. Default is 10.} - -\item{y.axis.size}{size of y-axis labels. Default is 10.} - -\item{text.size}{size of labels represented each condition at the top of -graph in Profile Plot and QC plot. Default is 4.} - -\item{text.angle}{angle of labels represented each condition at the top -of graph in Profile Plot and QC plot or x-axis labeling in Condition plot. -Default is 0.} - -\item{legend.size}{size of feature legend (transition-level or peptide-level) -above graph in Profile Plot. Default is 7.} - -\item{dot.size.profile}{size of dots in profile plot. Default is 2.} -} -\description{ -Make summary profile plot -} -\keyword{internal}