Skip to contents

Package: xcms
Authors: Ossama Edbali, Johannes Rainer
Modified: 2026-10-06 07:39:14.62411
Compiled: Tue Oct 6 08:28:50 2026

Introduction

Visualization of mass spectrometry data is an important component of LC-MS data analysis, both for quality control and for interpretation and communication of results. Throughout a metabolomics or proteomics workflow, visualization can help identify problems with data acquisition and processing, such as retention-time shifts, signal drift, differences in signal intensity between samples, and incorrect or poorly defined chromatographic peaks. It also provides an essential means of inspecting detected features and communicating results.

xcms provides a range of visualization functions for inspecting LC-MS data and the results of chromatographic peak detection, retention-time correction, and feature definition. These functions cover many common quality-control and diagnostic use cases and are closely integrated with the xcms data structures and processing workflow. For more complex visualizations, however, users may benefit from the flexibility of the ggplot2 grammar of graphics, particularly when combining multiple samples, annotations, experimental metadata, or several graphical layers into a single figure.

lcmsPlot has been developed to facilitate the visualization of LC-MS raw and processed data in a consistent and flexible manner across different software tools and processing workflows. By providing a common ggplot2-based framework for LC-MS visualization, it enables users to apply a consistent graphical approach to data generated or processed by different packages and at different stages of an analysis workflow.

This vignette illustrates how lcmsPlot can be used together with xcms to generate flexible, reproducible, and publication-ready visualizations of LC-MS data.

For a more thorough description of the package, including its support for raw files and for the outputs of other LC-MS software, see the lcmsPlot vignette.

Load the packages

lcmsPlot is a Bioconductor package and can be installed with BiocManager:

if (!requireNamespace("BiocManager", quietly = TRUE))
    install.packages("BiocManager")
BiocManager::install("lcmsPlot")

Load the data

Throughout this vignette we use the preprocessing result that is shipped with xcms and that is described in detail in the main xcms vignette. It is derived from a subset of the data from [1]; the raw files are provided by the faahKO package.

Using loadXcmsData() we get the fully preprocessed object directly, so that chromatographic peak detection, alignment and correspondence do not have to be repeated here.

xdata <- loadXcmsData("xdata")

The object contains eight samples assigned to the two sample groups "KO" and "WT":

Biobase::pData(xdata)
##   sample_name sample_group sample_type
## 1        ko15           KO          QC
## 2        ko16           KO       study
## 3        ko21           KO       study
## 4        ko22           KO          QC
## 5        wt15           WT       study
## 6        wt16           WT       study
## 7        wt21           WT          QC
## 8        wt22           WT       study

Plot samples summaries

Base peak chromatograms, total ion chromatograms, and total ion current plots are useful in LC-MS because they simplify complex datasets, highlight the most intense signals, and enable rapid detection and comparison of chromatographic features.

In the examples below we plot the base peak and total ion chromatograms for the eight samples (faceted):

lcmsPlot(xdata, sample_id_column = "sample_name") +
    lp_chromatogram(aggregation_fun = "max") +
    lp_facets(facets = "sample_name", ncol = 4)

lcmsPlot(xdata, sample_id_column = "sample_name") +
    lp_chromatogram(aggregation_fun = "sum") +
    lp_facets(facets = "sample_name", ncol = 4)

We can also plot multiple chromatograms in the same plot, overlapped. In the example below, base peak chromatograms are computed for each sample, grouped by sample group, and displayed together in one panel.

lcmsPlot(xdata, sample_id_column = "sample_name") +
    lp_chromatogram(aggregation_fun = "max") +
    lp_arrange(group_by = "sample_group") +
    lp_labels(title = "Base peak chromatograms", legend = "Sample group") +
    lp_legend(position = "bottom")

Below, we generate a total ion current plot for the eight samples under consideration:

lcmsPlot(xdata, sample_id_column = "sample_name") +
    lp_total_ion_current(type = "violin") +
    lp_arrange(group_by = "sample_group") +
    lp_labels(title = "Total ion current", legend = "Sample group")

These plots show the overall amount of ion signal entering the detector at any moment, widely used for quality control and method assessment.

Plot extracted ion chromatograms

lcmsPlot(xdata, sample_id_column = "sample_name") +
    lp_chromatogram(features = rbind(c(
        mzmin = 334.9,
        mzmax = 335.1,
        rtmin = 2700,
        rtmax = 2900))) +
    lp_arrange(group_by = "sample_group") +
    lp_labels(legend = "Sample group")

The above plot can be gridded on metadata factors; in the plot below we arrange feature_id along the rows and sample_id along the columns with lp_grid(), and detected chromatographic peaks are highlighted using the highlight_peaks parameter:

lcmsPlot(xdata, sample_id_column = 'sample_name') +
    lp_chromatogram(
        features = rbind(
            mz334 = c(
                mzmin = 334.9, mzmax = 335.1,
                rtmin = 2700, rtmax = 2900),
            mz278 = c(
                mzmin = 278.99721, mzmax = 279.00279,
                rtmin = 2740, rtmax = 2840)
        ),
        highlight_peaks = TRUE,
        highlight_peaks_color = '#f00') +
    lp_grid(rows = 'feature_id', cols = 'sample_id', free_y = TRUE)

The same plot with base R graphics

The same figure can be produced with the plotting functions of xcms itself. The extracted ion chromatograms are first created with the chromatogram() function, which returns an XChromatograms object with one chromatogram per feature (row) and sample (column), and the detected chromatographic peaks attached to each of them.

mzr_xic <- rbind(c(334.9, 335.1), c(278.99721, 279.00279))
rtr_xic <- rbind(c(2700, 2900), c(2740, 2840))

chrs_xic <- chromatogram(xdata, mz = mzr_xic, rt = rtr_xic)

## Define one color per sample group
group_colors <- RColorBrewer::brewer.pal(3, "Set1")[1:2]
names(group_colors) <- c("KO", "WT")
sample_colors <- group_colors[chrs_xic$sample_group]

## Label the rows the same way lcmsPlot names its features
feature_ids <- sprintf("M%dT%d", round(rowMeans(mzr_xic)),
                       round(rowMeans(rtr_xic)))

## One panel per feature (row) and sample (column)
par(mfrow = c(nrow(chrs_xic), ncol(chrs_xic)),
    mar = c(3, 3, 1.8, 0.4), mgp = c(1.9, 0.6, 0), cex = 0.7)
for (i in seq_len(nrow(chrs_xic))) {
    for (j in seq_len(ncol(chrs_xic))) {
        plot(chrs_xic[i, j], col = sample_colors[j],
             peakType = "polygon", peakCol = paste0(sample_colors[j], 60),
             peakBg = paste0(sample_colors[j], 40),
             main = paste(feature_ids[i], chrs_xic$sample_name[j]))
    }
}

The result is equivalent to the lp_grid() figure above, but the layout has to be set up explicitly: par(mfrow = ...) defines the grid, the two nested loops address the individual cells chrs_xic[i, j] of the XChromatograms object, and the panel titles and per-sample colors have to be assembled by hand. Each panel also keeps its own y axis, which corresponds to free_y = TRUE in lp_grid().

In lcmsPlot, you can also plot directly from an XChromatograms object. In the example below, two features are defined by their m/z ranges and the corresponding retention time windows. The resulting plot is arranged as a grid with features as rows and samples as columns.

mzr <- matrix(c(344, 344, 360, 360), ncol = 2, byrow = TRUE)

rtr <- matrix(c(2500, 2880, 2600, 2880), ncol = 2, byrow = TRUE)

chrs <- chromatogram(xdata, mz = mzr, rt = rtr)
rownames(chrs) <- c("mz344", "mz360")

lcmsPlot(chrs) +
    lp_chromatogram(na.rm = TRUE) +
    lp_grid(rows = "feature_id", cols = "sample_id")

Quality control of peak detection

lp_peak_count_image() bins the retention-time axis and shows how many chromatographic peaks each sample yielded in each bin, with samples on the y axis and one tile per sample and bin. It is the counterpart of xcms::plotChromPeakImage().

lcmsPlot(xdata, sample_id_column = "sample_name") +
    lp_peak_count_image(bin_size = 30) +
    lp_labels(title = "Chromatographic peaks per 30 s bin")

lp_chrom_peak_rects() draws one rectangle per detected peak, spanning its retention-time limits by its m/z limits, and shows where in the m/z / retention-time plane the peak detection actually placed its boundaries.

lcmsPlot(xdata, sample_id_column = "sample_name") +
    lp_chrom_peak_rects(
        sample_ids = c("ko15", "wt15"),
        rt_range = c(3200, 3700),
        mz_range = c(480, 540),
        fill = "#c0392b60")

Plot grouped peaks across samples (features)

The correspondence analysis performed on this data set grouped the chromatographic peaks of the individual samples into features, i.e. groups of peaks that likely originate from the same ion. Each feature is identified by the corresponding row name of featureDefinitions(xdata), by default "FT001", "FT002", and so on. These identifiers can be passed to the features parameter of lp_chromatogram() to extract and plot the corresponding chromatograms across all samples without having to specify m/z and retention time ranges manually. The ppm and rt_tol parameters define how far around the feature’s consensus m/z and retention time the signal is extracted.

lcmsPlot(xdata, sample_id_column = 'sample_name') +
    lp_chromatogram(
        features = c('FT002', 'FT004', 'FT019', 'FT027'),
        ppm = 10,
        rt_tol = 80,
        highlight_peaks = TRUE,
        highlight_peaks_factor = "sample_group") +
    lp_arrange(group_by = 'sample_group') +
    lp_facets(facets = 'feature_id', ncol = 2, free_x = TRUE, free_y = TRUE) +
    lp_labels(title = "Four selected features", legend = "Sample") +
    lp_legend(position = "bottom")

Plot peak density alongside chromatograms

Peak density plots show how the detected chromatographic peaks of an m/z window are distributed along the retention time axis. Each detected peak is drawn as a point at the y position of its sample and the kernel density estimate of the peak apex retention times is overlaid as a line. This is the diagnostic used by the peak density correspondence method and makes it easy to judge whether the peaks of the different samples are well aligned and whether a feature is tightly defined. It is the lcmsPlot counterpart of xcms::plotChromPeakDensity().

When lp_peak_density() follows lp_chromatogram(), the features argument is inherited from the chromatogram layer and does not have to be repeated. The bw parameter should match the bandwidth used during correspondence, and when min_fraction is given the grouping is simulated so that the retention time regions that would be defined as features are drawn as semi-transparent rectangles.

lcmsPlot(xdata, sample_id_column = "sample_name") +
    lp_chromatogram(
        sample_ids = c("ko15", "ko16", "wt16", "wt21"),
        features = rbind(c(
            mzmin = 334.9, mzmax = 335.1, rtmin = 2700, rtmax = 2900)),
        highlight_peaks = TRUE
    ) +
    lp_peak_density(
        bw = 30,
        min_fraction = 0.5
    ) +
    lp_labels(legend = "Sample ID")

Plot spectra

Spectra are added to a plot with the lp_spectra() function. Its mode parameter determines which scan a spectrum is extracted from.

Select the closest scan to a specified retention time

With mode = "closest" the spectrum is taken from the scan closest to the retention time given by the rt parameter. The retention time at which the spectrum was acquired is marked with a vertical dashed line on the chromatogram, so that the spectrum can be related to the chromatographic peak it belongs to.

lcmsPlot(xdata, sample_id_column = "sample_name") +
    lp_chromatogram(
        features = "FT034",
        sample_ids = "ko15",
        ppm = 20,
        rt_tol = 60,
        highlight_peaks = TRUE) +
    lp_spectra(rt = 2789, mode = "closest", ms_level = 1) +
    lp_labels(title = "Feature FT034", legend = "Sample") +
    lp_legend(position = "bottom")

Select the scan closest to a detected peak apex

With mode = "closest_apex" the retention time of the peak maximum is determined from the chromatographic peaks detected by xcms and the nearest scan is used, which removes the need to specify a retention time manually.

In the example below, chromatograms are extracted for two features in one sample and the MS1 spectrum at the apex of each is displayed below its chromatogram. The lp_layout() function is used to give the spectra more vertical space than the chromatograms.

lcmsPlot(xdata, sample_id_column = "sample_name") +
    lp_chromatogram(
        features = c("FT219", "FT250"),
        sample_ids = "ko15",
        ppm = 20,
        rt_tol = 60,
        highlight_peaks = TRUE) +
    lp_spectra(mode = "closest_apex", ms_level = 1) +
    lp_facets(facets = "feature_id", ncol = 2) +
    lp_labels(legend = "Sample") +
    lp_legend(position = "bottom") +
    lp_layout(design = "C\nS\nS")

Each spectrum is dominated by the m/z of its own feature, accompanied by the corresponding isotope peaks.

Select multiple scans across a detected peak

To follow how the spectral composition changes over the duration of a chromatographic peak, several scans can be extracted across it with mode = "across_peak". The interval parameter defines the retention time spacing (in seconds) between the selected scans.

lcmsPlot(xdata, sample_id_column = "sample_name") +
    lp_chromatogram(
        features = "FT083",
        sample_ids = "ko15",
        ppm = 20,
        rt_tol = 60,
        highlight_peaks = TRUE) +
    lp_spectra(mode = "across_peak", interval = 15, ms_level = 1) +
    lp_labels(legend = "Sample") +
    lp_legend(position = "bottom") +
    lp_layout(design = "C\nS\nS")

This view makes co-elution visible: the ion of the feature dominates the spectrum at the beginning of the peak, while towards its end other ions become the most intense signals of the scan.

Plot standalone spectra

Spectra can also be plotted on their own, without an accompanying chromatogram, which is useful for a detailed comparison of isotope patterns or relative ion intensities between samples. Standalone spectra are created by using lp_spectra() without lp_chromatogram().

lcmsPlot(xdata, sample_id_column = "sample_name") +
    lp_spectra(
        sample_ids = c("ko15", "wt15"),
        rt = 3323,
        mode = "closest",
        ms_level = 1) +
    lp_labels(title = "MS1 spectra at 3323 seconds") +
    lp_legend(position = "bottom")

Session information

## R version 4.6.1 (2026-06-24)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 24.04.4 LTS
## 
## Matrix products: default
## BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
## LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so;  LAPACK version 3.12.0
## 
## locale:
##  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
##  [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
##  [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
##  [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
##  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
## [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       
## 
## time zone: UTC
## tzcode source: system (glibc)
## 
## attached base packages:
## [1] stats4    stats     graphics  grDevices utils     datasets  methods  
## [8] base     
## 
## other attached packages:
##  [1] MSnbase_2.39.5       S4Vectors_0.51.10    Biobase_2.73.2      
##  [4] BiocGenerics_0.59.12 generics_0.1.4       mzR_2.47.1          
##  [7] Rcpp_1.1.2           lcmsPlot_1.1.13      xcms_4.11.4         
## [10] BiocParallel_1.47.0  BiocStyle_2.41.0    
## 
## loaded via a namespace (and not attached):
##   [1] DBI_1.3.0                   rlang_1.3.0                
##   [3] magrittr_2.0.5              clue_0.3-68                
##   [5] MassSpecWavelet_1.79.2      otel_0.2.0                 
##   [7] matrixStats_1.5.0           compiler_4.6.1             
##   [9] PTMods_1.1.0                systemfonts_1.3.2          
##  [11] vctrs_0.7.3                 reshape2_1.4.5             
##  [13] stringr_1.6.0               ProtGenerics_1.45.0        
##  [15] crayon_1.5.3                pkgconfig_2.0.3            
##  [17] MetaboCoreUtils_1.21.1      fastmap_1.2.0              
##  [19] XVector_0.53.0              labeling_0.4.3             
##  [21] rmarkdown_2.32              preprocessCore_1.75.1      
##  [23] ragg_1.5.2                  purrr_1.2.2                
##  [25] xfun_0.61                   MultiAssayExperiment_1.39.1
##  [27] cachem_1.1.0                jsonlite_2.0.0             
##  [29] progress_1.2.3              DelayedArray_0.39.8        
##  [31] prettyunits_1.2.0           parallel_4.6.1             
##  [33] cluster_2.1.8.3             R6_2.6.1                   
##  [35] bslib_0.12.0                stringi_1.8.9              
##  [37] RColorBrewer_1.1-3          limma_3.99.0               
##  [39] GenomicRanges_1.65.4        jquerylib_0.1.4            
##  [41] iterators_1.0.14            Seqinfo_1.3.2              
##  [43] bookdown_0.48               SummarizedExperiment_1.43.0
##  [45] knitr_1.52                  IRanges_2.47.5             
##  [47] Matrix_1.7-6                igraph_2.3.4               
##  [49] tidyselect_1.2.1            abind_1.4-8                
##  [51] yaml_2.3.12                 doParallel_1.0.17          
##  [53] codetools_0.2-20            affy_1.91.0                
##  [55] lattice_0.23-1              tibble_3.3.1               
##  [57] plyr_1.8.9                  withr_3.0.3                
##  [59] S7_0.2.2                    evaluate_1.0.5             
##  [61] desc_1.4.3                  Spectra_1.23.5             
##  [63] pillar_1.11.1               affyio_1.83.0              
##  [65] BiocManager_1.30.27         MatrixGenerics_1.25.0      
##  [67] foreach_1.5.2               MALDIquant_1.22.3          
##  [69] ncdf4_1.24                  hms_1.1.4                  
##  [71] ggplot2_4.0.3               scales_1.4.0               
##  [73] MsExperiment_1.15.0         glue_1.8.1                 
##  [75] MsFeatures_1.21.0           lazyeval_0.2.3             
##  [77] tools_4.6.1                 mzID_1.51.0                
##  [79] data.table_1.18.6.1         QFeatures_1.23.2           
##  [81] vsn_3.81.1                  fs_2.1.0                   
##  [83] XML_3.99-0.25               grid_4.6.1                 
##  [85] impute_1.87.0               tidyr_1.3.2                
##  [87] MsCoreUtils_1.25.4          patchwork_1.3.2            
##  [89] PSMatch_1.17.0              cli_3.6.6                  
##  [91] textshaping_1.0.5           viridisLite_0.4.3          
##  [93] S4Arrays_1.13.2             Chromatograms_1.3.3        
##  [95] dplyr_1.2.1                 AnnotationFilter_1.37.0    
##  [97] pcaMethods_2.5.0            gtable_0.3.6               
##  [99] sass_0.4.10                 digest_0.6.39              
## [101] SparseArray_1.13.4          htmlwidgets_1.6.4          
## [103] farver_2.1.2                htmltools_0.5.9            
## [105] pkgdown_2.2.1.9000          lifecycle_1.0.5            
## [107] statmod_1.5.2               MASS_7.3-66
1. Saghatelian A, Trauger SA, Want EJ, Hawkins EG, Siuzdak G, Cravatt BF: Assignment of endogenous substrates to enzymes by global metabolite profiling. Biochemistry 2004, 43:14332–9.