---
title: "Easy-to-use, intuitive, and efficient LC-MS data plotting with lcmsPlot"
author: "Ossama Edbali"
date: "14 December 2025"
output:
  BiocStyle::html_document:
    toc: true
    toc_depth: 2  
    number_sections: true  
    toc_float: true
vignette: >
  %\VignetteIndexEntry{Easy-to-use, intuitive, and efficient LC-MS data plotting with lcmsPlot}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
bibliography: references.bib
csl: biomed-central.csl
---

```{r setup, include = FALSE}
knitr::opts_knit$set(
    root.dir = dirname(getwd())
)

knitr::opts_chunk$set(
    collapse = TRUE,
    comment = "#>")
```

```{r init, message = FALSE, echo = FALSE, results = "hide"}
library(BiocStyle)
library(faahKO)
library(MsExperiment)
library(lcmsPlot)
```

# Introduction

Visualisation of LC–MS data, and, more broadly, X-MS data, is essential for both
quality control and effective communication of results.
Diagnostic plots can reveal acquisition issues such as retention-time shifts,
inconsistencies in internal standards, and signal drift,
while also providing publication-ready figures.

Current LC–MS visualisation approaches primarily include:

- xcms: Base R plotting functions that address common use cases but offer
limited flexibility and visual refinement.
- Workflow-specific tools (e.g. LCMSQA, MetaboAnalystR, CAMERA): Provide
visualisation capabilities that are tightly coupled to their respective
workflows, limiting reuse and general applicability.
- Custom ggplot2-based solutions: Highly flexible, but require substantial
familiarity with ggplot2 and can become cumbersome for advanced or routine
MS-specific visualisations.

Overall, existing solutions tend to be either overly specialised or
insufficiently flexible, making them poorly suited for reusable,
cross-platform LC–MS visualisation workflows.

## Objectives

`lcmsPlot` aims to provide a modern, generic solution for LC-MS visualisation,
focused on:

- **Flexible plotting**: chromatograms, mass traces, spectra, and more.
- **Building-blocks design**: [ggplot](https://ggplot2.tidyverse.org/)-like
interface for customisable, reproducible plots.
- **Interoperability**: works seamlessly with xcms outputs or
raw formats (e.g., mzML).
- **Efficiency**: scales better to large datasets than current solutions.

The goal is for `lcmsPlot` to become the community standard for LC-MS data
visualisation: versatile for exploration, efficient for large studies, and
capable of producing publication-ready figures with minimal effort.

# Installation

`lcmsPlot` is a Bioconductor package and to install it we have to use
[BiocManager](https://cran.r-project.org/web/packages/BiocManager/index.html).

```{r install, eval=FALSE}
if (!requireNamespace("BiocManager", quietly = TRUE))
    install.packages("BiocManager")
BiocManager::install("lcmsPlot")
```

# Load data

For demonstration purposes we will analyse a small subset of the
data from [@Saghatelian04] in which the metabolic consequences of the knock-out
of the fatty acid amide hydrolase (FAAH) gene in mice was investigated
(same data as in the [xcms vignette](https://www.bioconductor.org/packages/devel/bioc/vignettes/xcms/inst/doc/xcms.html)).


```{r load-data, message=FALSE}
library(faahKO)
library(MsExperiment)
library(patchwork)
library(lcmsPlot)

BiocParallel::register(BiocParallel::SerialParam())

sample_indices <- c(1, 2, 5, 6, 7, 8, 11, 12)
cdfs <- dir(
    system.file("cdf", package = "faahKO"),
    full.names = TRUE,
    recursive = TRUE)[sample_indices]

sample_names <- sub(
    basename(cdfs),
    pattern = ".CDF",
    replacement = "",
    fixed = TRUE)

pd <- data.frame(
    sample_name = sample_names,
    sample_group = c(rep("KO", 4), rep("WT", 4)),
    stringsAsFactors = FALSE)

faahko <- readMsExperiment(spectraFiles = cdfs, sampleData = pd)
faahko
```

# Samples summary

Before examining the detected peaks and their chromatograms, we will first gain
an overview of the study samples using a series of plots that highlight
different aspects of the data.

## The entry point to plot

The `lcmsPlot` function is the main starting point for generating visualisations of LC-MS data.
It acts as a high-level wrapper that prepares the dataset, configures plotting parameters, and returns a plot object that can be further customised.

```{r lcmsPlot-object}
p = lcmsPlot(faahko, sample_id_column = "sample_name")
print(p)
```

Through the use of the plotting functions described below, the plot object will display the requested visualisations once it is printed.

## Plot base peak chromatogram (BPC)

Base Peak Chromatograms (BPCs) are useful in LC-MS because they simplify complex
data and help quickly identify the most intense signals.

In the example below we plot the BPC for the eight samples:

```{r bpc, fig.height=6, fig.width=10}
lcmsPlot(faahko, sample_id_column = "sample_name") +
    lp_chromatogram(aggregation_fun = "max") +
    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.

```{r bpc-overlapped}
lcmsPlot(faahko, 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")
```

## Plot total ion chromatogram (TIC)

The Total Ion Chromatogram (TIC) represents the sum of all ion intensities
detected at each time point, providing a comprehensive view of all ions entering
the detector, not just the most abundant ones.

```{r tic, fig.height=6, fig.width=10}
lcmsPlot(faahko, sample_id_column = "sample_name") +
    lp_chromatogram(aggregation_fun = "sum") +
    lp_facets(facets = "sample_name", ncol = 4)
```

## Total ion current

The Total Ion Current (TIC) reflects the overall amount of ion signal entering
the detector at any moment, independent of individual compounds. 
TIC is widely used for quality control and method assessment.
Comparable TIC profiles or distributions across samples indicate consistent
injection amounts, stable chromatography, and reliable instrument performance.
Conversely, large deviations in TIC can signal issues such as sample loss,
ion suppression, contamination, or changes in instrument sensitivity.

Below, we generate a TIC plot for the eight samples under consideration:

```{r total-ion-current}
lcmsPlot(faahko, 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")
```

## Intensity maps

Intensity maps provide a compact, two-dimensional overview of LC-MS data by
displaying signal intensity as a function of m/z and retention time.
They are a fast and intuitive way to explore global structure in the
data and to detect systematic patterns that may not be obvious from
chromatograms or summary statistics alone.

In the example below, intensity density maps are generated for two samples
(`ko15` and `wt15`) to enable a direct visual comparison.
The plot is restricted to an m/z range of 200-600 and a retention time window
of 4200-4500 seconds. Density-based visualisation is used to smooth the signal
and highlight recurring patterns rather than individual noise peaks.

```{r intensity-maps}
pparam <- BiocParallel::MulticoreParam(workers = 2)
lcmsPlot(faahko, sample_id_column = "sample_name", BPPARAM = pparam) +
    lp_intensity_map(
        sample_ids = c("ko15", "wt15"),
        mz_range = c(200, 600),
        rt_range = c(4200, 4500),
        density = TRUE) +
    lp_facets(facets = "sample_id", ncol = 2)
```

# Plot chromatograms

Before plotting chromatograms, we first perform peak detection. This step allows
detected chromatographic peaks to be highlighted in subsequent visualisations,
while still preserving the ability to plot chromatograms directly from the raw
MS data when desired.

Peak detection is carried out using the _centWave_ algorithm,
which is well suited for high-resolution LC-MS data.

```{r peak-detection}
cwp <- xcms::CentWaveParam(
    peakwidth = c(20, 80),
    noise = 5000,
    prefilter = c(6, 5000))
faahko <- xcms::findChromPeaks(faahko, param = cwp)
```

Next, we plot extracted ion chromatograms (XICs) for a feature
detected by xcms across all samples.
The feature is defined by its m/z and retention time limits, and the
corresponding chromatographic signal is extracted from each sample.

In this example, we specify the feature manually using its m/z and RT ranges,
which correspond to a peak previously detected by xcms.
The resulting chromatograms are arranged by sample group to facilitate
comparison across experimental conditions.

```{r plot-xics}
lcmsPlot(faahko, 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 faceted on metadata factors;
in this case we facet on `feature_id` and `sample_id`:

```{r plot-xic-faceted, fig.height=4, fig.width=8}
lcmsPlot(faahko, sample_id_column = "sample_name") +
    lp_chromatogram(
        features = rbind(c(
            mzmin = 334.9,
            mzmax = 335.1,
            rtmin = 2700,
            rtmax = 2900)),
        highlight_peaks = TRUE,
        highlight_peaks_color = "#121212",
        highlight_apices = list(top_n = 1)
    ) +
    lp_facets(facets = c('feature_id', 'sample_id'), ncol = 4) +
    lp_labels(legend = "Sample") +
    lp_legend(position = "bottom")
```

Moreover, with the `highlight_apices` option, the apex of each detected
chromatographic peak is highlighted in the plot and annotated with its
corresponding retention time value. This makes it easier to identify peak maxima
and to compare peak positions across samples.

On the other hand, gridded plots (using the `lp_grid` function) arrange panels
in a matrix defined by two variables. This layout enables structured,
two-dimensional comparisons, making it easier to examine how chromatographic
patterns vary simultaneously across both factors.

```{r plot-xic-highlight-peaks, fig.height=4, fig.width=8}
lcmsPlot(faahko, sample_id_column = 'sample_name') +
    lp_chromatogram(
        features = rbind(
            c(mzmin = 334.9, mzmax = 335.1, rtmin = 2700, rtmax = 2900),
            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)
```

In some cases, you may want to highlight specific regions of an extracted ion
chromatogram, for example, to emphasize a peak that has not been automatically
detected. This can be achieved using the `lp_rt_line` function, which allows you
to add vertical lines at specified retention times, visually marking areas of
interest in the chromatogram.

```{r plot-xic-rt-line, fig.height=4, fig.width=8}
lcmsPlot(faahko, sample_id_column = 'sample_name') +
    lp_chromatogram(features = rbind(c(
        mzmin = 334.9,
        mzmax = 335.1,
        rtmin = 2700,
        rtmax = 2900))) +
    lp_facets(facets = 'sample_id', ncol = 4) +
    lp_rt_line(intercept = 2800, line_type = 'solid', color = 'red') +
    lp_rt_line(intercept = 2750, line_type = 'dashed', color = 'blue') +
    lp_rt_line(intercept = 2850, line_type = 'dashed', color = 'blue')
```

You can also plot directly from an `XChromatograms` object which is the xcms
representation of extracted ion chromatograms (EICs) for one or more features
across multiple samples.
`XChromatograms` objects are produced by `xcms::chromatogram()` and store one
chromatogram per cell of a features x samples matrix.

In the example below, two features are defined by their m/z ranges and the
corresponding retention time windows.
`xcms::chromatogram()` is called to extract the EICs from the `faahko` dataset.
The resulting plot is arranged as a grid with features as rows and
samples as columns.

```{r plot-XChromatograms, message=FALSE, warning=FALSE, fig.height=4, fig.width=10}
mzr <- matrix(c(344, 344, 360, 360), ncol = 2, byrow = TRUE)

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

chrs <- chromatogram(faahko, mz = mzr, rt = rtr)

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

`lcmsPlot` also supports `xcmsRaw` objects, the legacy xcms format for
in-memory raw MS data.
The `XcmsRawList` container wraps one or more `xcmsRaw` objects
(one per sample) and is the recommended way to pass such data to `lcmsPlot`.
The convenience function `create_xcms_raw_list()` reads a set of raw files
into memory via `xcms::xcmsRaw()` and returns a ready-to-use `XcmsRawList`.

In the example below, two samples are read from the faahKO dataset and two
features are plotted side-by-side:

```{r plot-xcmsRaw}
raw_paths <- sampleData(faahko)$spectraOrigin[c(1, 5)]

xl <- create_xcms_raw_list(raw_paths)

lcmsPlot(xl) +
    lp_chromatogram(
        features = rbind(
            c(mzmin = 334.9, mzmax = 335.1, rtmin = 2700, rtmax = 2900),
            c(mzmin = 344.0, mzmax = 344.1, rtmin = 2600, rtmax = 2800)
        )
    ) +
    lp_grid(rows = "feature_id", cols = "sample_id", free_y = TRUE)
```

# Plot retention time alignment effect

Retention times can vary slightly across LC-MS runs due to instrumental drift
or other experimental factors. To accurately compare chromatographic features
across samples, it is important to align retention times so that the same
feature appears at the same RT in all samples.

Here, we perform retention time alignment using the Obiwarp algorithm:

```{r rt-alignment, message=FALSE, warning=FALSE}
faahko <- xcms::adjustRtime(faahko, param = xcms::ObiwarpParam(binSize = 0.6))
```

Now, we can visualise the BPC for the raw and RT-corrected datasets:

```{r plot-bpc-comparison}
lcmsPlot(faahko, sample_id_column = "sample_name") +
    lp_chromatogram(
        ppm = 5,
        rt_tol = 10,
        aggregation_fun = "max",
        rt_type = "both") +
    lp_arrange(group_by = "sample_group") +
    lp_facets("rt_type", ncol = 1) +
    lp_labels(legend = "Sample group") +
    lp_legend(position = "bottom")
```

The following plot visualises the retention time correction applied
during alignment: the x-axis shows the adjusted retention time (`rt_adj`),
while the y-axis displays the difference between the raw and adjusted retention
times (`rt_raw - rt_adj`). This highlights how much each feature's
retention time was shifted to achieve alignment across samples.
Smooth, gradual trends in the plot indicate consistent chromatographic
drift correction, while abrupt changes may suggest anomalies.
This diagnostic view helps you assess the quality and extent of
retention time alignment in LC-MS data processing.

```{r plot-rt-alignment-effect}
lcmsPlot(faahko, sample_id_column = "sample_name") +
    lp_rt_diff_plot() +
    lp_arrange(group_by = "sample_group") +
    lp_labels(legend = "Sample group")
```

# Plot grouped peaks across samples (features)

The next step in LC–MS preprocessing is grouping chromatographic
peaks across samples.
This step groups peaks that likely originate from the same compound but were
detected in different samples.

Here, peak grouping is performed using the peak density approach.
Peaks are grouped based on their proximity in m/z and retention time, while also
considering their presence across samples.
The `sampleGroups` argument specifies the experimental grouping, `minFraction`
defines the minimum fraction of samples within a group in which a peak must be
present, and `bw` controls the retention time bandwidth used for grouping.

```{r peak-grouping}
pdp <- xcms::PeakDensityParam(
    sampleGroups = sampleData(faahko)$sample_group,
    minFraction = 0.4,
    bw = 30)
faahko <- xcms::groupChromPeaks(faahko, param = pdp)
```

Each resulting group of peaks is assigned a unique feature ID, defined by a
consensus m/z value and retention time.
These feature IDs can then be used to automatically extract and plot
chromatograms for individual features across all samples, enabling direct visual
comparison of aligned and grouped peaks:

```{r plot-features}
lcmsPlot(faahko, sample_id_column = 'sample_name') +
    lp_chromatogram(
        features = c('M205T2792', 'M207T2719', 'M279T2791', 'M284T2719'),
        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 visualise how detected chromatographic peaks are
distributed along the retention time axis for a given m/z window.
The y-axis shows sample indices and the x-axis shows retention time.
Each detected peak is drawn as a coloured point at its sample's
y-coordinate, and the kernel density estimate of peak apex RTs is
overlaid as a line.
This makes it straightforward to assess whether peaks from different
samples are well-aligned in retention time and whether a feature group
is tightly defined.

When `lp_peak_density()` is combined with `lp_chromatogram()`,
`features` can be omitted — it is inherited automatically
from the chromatogram layer.

The `bw` parameter controls the kernel density bandwidth and should
match the value used in `xcms::PeakDensityParam()` during peak grouping.
When `min_fraction` and `sample_groups` are provided, the
density-descent grouping algorithm is simulated and semi-transparent
rectangles are drawn around RT regions that would be defined as feature
groups — providing a direct visual check of the grouping parameters.

```{r peak-density}
lcmsPlot(faahko, 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 chromatograms with mass traces

Until now, we have primarily focused on chromatograms to understand the
abundance and retention behavior of features across samples.
Chromatograms indicate when a compound elutes and how strongly it is detected,
but they do not reveal whether the measured mass values are stable and
consistent across the chromatographic peak or between samples.

This is where the mass trace (shown in the bottom panel) becomes valuable.
Mass traces display how the measured m/z values evolve over the retention-time
profile of a peak. They allow assessment of mass accuracy, mass drift,
and signal coherence within a peak, helping to determine whether a feature
represents a single, well-defined ion or a mixture of overlapping signals.
Consistent and narrow mass traces across samples are indicative of high-quality,
reproducible features, whereas variability may point to co-eluting compounds,
interference, or measurement instability.

```{r plot-chroms-with-mass-traces}
lcmsPlot(faahko, sample_id_column = 'sample_name') +
    lp_chromatogram(
        features = 'M205T2792',
        sample_ids = c('ko15', 'ko16', 'wt15', 'wt16'),
        ppm = 10,
        rt_tol = 50,
        highlight_peaks = TRUE) +
    lp_mass_trace() +
    lp_arrange(group_by = 'sample_id') +
    lp_labels(title = "Feature M205T2792", legend = "Sample") +
    lp_legend(position = "bottom")
```

As for plain chromatograms, we can facet a combination of
chromatograms + mass traces on a factor, such as `feature_id`:

```{r plot-chroms-with-mass-traces-faceted}
lcmsPlot(faahko, sample_id_column = 'sample_name') +
    lp_chromatogram(
        features = c('M283T3890', 'M205T2792'),
        sample_ids = c('ko15', 'ko16', 'wt15', 'wt16'),
        ppm = 10,
        rt_tol = 50,
        highlight_peaks = TRUE) +
    lp_mass_trace() +
    lp_arrange(group_by = "sample_id") +
    lp_facets(facets = 'feature_id', ncol = 2) +
    lp_labels(legend = "Sample") +
    lp_legend(position = "bottom")
```

Or multiple factors (`feature_id` and `sample_id`):

```{r plot-chroms-with-mass-traces-faceted-factors, fig.height=12, fig.width=6}
lcmsPlot(faahko, sample_id_column = 'sample_name') +
    lp_chromatogram(
        features = c('M283T3890', 'M205T2792'),
        sample_ids = c('ko15', 'ko16', 'wt15', 'wt16'),
        ppm = 10,
        rt_tol = 50,
        highlight_peaks = TRUE,
        highlight_peaks_color = '#343434') +
    lp_mass_trace() +
    lp_facets(facets = c('feature_id', 'sample_id'))
```

# Plot chromatograms from raw files

Until now, we have been using an `MsExperiment` object as the data source
for plotting.
However, lcmsPlot also supports direct extraction of chromatograms from raw MS
files, such as those in the `mzML` format.
This can be useful when you want to quickly inspect raw data without first
creating a full experiment object or running preprocessing steps.

In the example below, chromatograms are extracted directly from a subset of
raw files. Each file is treated as an individual sample and plotted accordingly.

```{r plot-chroms-raw-files}
raw_files <- sampleData(faahko)$spectraOrigin[1:4]

lcmsPlot(raw_files) +
    lp_chromatogram(features = rbind(c(
        mzmin = 334.9,
        mzmax = 335.1,
        rtmin = 2700,
        rtmax = 2900))) +
    lp_arrange(group_by = "sample_id") +
    lp_legend(position = "bottom") +
    lp_labels(legend = "Sample")
```

By default, file names are used to identify samples.
To enable more informative grouping, labeling, and faceting, custom metadata
can be attached to the raw files and referenced during plotting:

```{r plot-chroms-raw-files-with-metadata}
raw_files <- sampleData(faahko)$spectraOrigin[c(1, 2, 5, 6)]

metadata <- data.frame(
    sample_type = c("KO", "KO", "WT", "WT"),
    sample_id = c("S1", "S2", "S3", "S4")
)

lcmsPlot(raw_files, sample_id_column = "sample_id", metadata = metadata) +
    lp_chromatogram(
        features = rbind(
            c(mzmin = 334.9, mzmax = 335.1, rtmin = 2700, rtmax = 2900),
            c(mzmin = 278.99721, mzmax = 279.00279, rtmin = 2740, rtmax = 2840)
        )
    ) +
    lp_grid(rows = 'feature_id', cols = 'sample_type', free_y = TRUE)
```

`lcmsPlot` also supports ThermoFisher `.raw` files.
The examples below are not evaluated due to file size constraints in Bioconductor.

Chromatograms can be plotted directly from `.raw` files:

```{r plot-raw, eval=FALSE}
raw_files <- c("sample1.raw", "sample2.raw", "sample3.raw")
    
lcmsPlot(raw_files) +
    lp_chromatogram(aggregation_fun = "max") +
    lp_arrange(group_by = "sample_id")
```

Spectra can be plotted from `.raw` files as well:

```{r plot-raw-spectra, eval=FALSE, fig.height=9, fig.width=5}
raw_files <- c("sample1.raw", "sample2.raw", "sample3.raw")

lcmsPlot(raw_files) +
    lp_chromatogram(features = rbind(c(mz = 181.0706641)), ppm = 10) +
    lp_spectra(rt = 120, mode = "closest") +
    lp_facets(facets = "sample_id", ncol = 1)
```

# Plot spectra

While chromatograms describe how signal intensity changes over retention time,
mass spectra provide critical compositional insight by revealing which ions
contribute to a feature at a specific point in time.
Spectra allow inspection of isotopic patterns, adducts, and
potential interferences, and are therefore essential for
feature validation and annotation.

To include spectra in the visualisation, we use the `lp_spectra` function.
This function extracts and displays mass spectra based on a
specified scan selection mode, which determines which scans are used to
generate the spectrum.

## Select closest scan to a specified RT

To extract and plot the mass spectrum from the scan closest to a specified
retention time, you can use `mode = "closest"`.
This is particularly useful when you want to inspect the spectral composition
at or near the apex of a chromatographic peak.

In the example below, a chromatogram is first extracted for a
single feature and sample.
The detected peaks are highlighted, and the mass spectrum corresponding to the
scan closest to the specified RT (3860 seconds) is displayed
below the chromatogram.

```{r plot-spectra-scan-closest-rt, fig.height=4, fig.width=9}
lcmsPlot(faahko, sample_id_column = 'sample_name') +
    lp_chromatogram(
        features = 'M283T3890',
        sample_ids = 'ko15',
        ppm = 5,
        rt_tol = 50,
        highlight_peaks = TRUE) +
    lp_spectra(rt = 3860, mode = "closest", ms_level = 1) +
    lp_labels(title = "Feature M283T3890", legend = "Sample") +
    lp_legend(position = "bottom")
```

## Select scan closest to a detected peak apex

To extract and plot the mass spectrum from the scan closest to the apex of a
detected chromatographic peak, you can use `mode = "closest_apex"`. 
This mode automatically identifies the retention time of the peak maximum and
selects the nearest corresponding scan, removing the need to manually
specify an RT value.

In the example below, chromatograms are extracted for two features in a
single sample, with detected peaks highlighted. For each feature, the MS1
spectrum corresponding to the closest scan at the peak apex is displayed,
allowing direct inspection of the ions contributing to each peak.

```{r plot-spectra-scan-closest-apex, fig.height=5, fig.width=10, fig.small=FALSE}
lcmsPlot(faahko, sample_id_column = 'sample_name') +
    lp_chromatogram(
        features = rbind(
            c(mzmin = 362.0982, mzmax = 362.1018, rtmin = 3130, rtmax = 3200),
            c(mzmin = 357.1946, mzmax = 357.2054, rtmin = 3800, rtmax = 3900)
        ),
        sample_ids = 'ko15',
        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")
```

## Select multiple scans across a detected peak

To inspect how the spectral composition changes across the duration of a
detected chromatographic peak, multiple scans can be selected within a
retention time interval using `mode = "across_peak"`.
This mode extracts spectra at regular intervals spanning the peak,
enabling assessment of signal stability, co-eluting ions, or changes in
ion dominance from peak start to end.

In the example below, a chromatogram is extracted for a single feature in
one sample, with the detected peak highlighted.
MS1 spectra are then extracted at regular RT intervals (every 10 seconds)
across the peak and displayed sequentially beneath the chromatogram.

```{r plot-spectra-across-peak, fig.height=10, fig.width=6}
lcmsPlot(faahko, sample_id_column = 'sample_name') +
    lp_chromatogram(
        features = "M205T2792",
        sample_ids = 'ko15',
        ppm = 5,
        rt_tol = 50,
        highlight_peaks = TRUE) +
    lp_spectra(mode = "across_peak", interval = 10, ms_level = 1) +
    lp_labels(legend = "Sample") +
    lp_legend(position = "bottom") +
    lp_layout(design = "C\nS\nS\nS\nS\nS\nS\nS")
```

## Plot standalone spectra

So far, spectra have been displayed alongside chromatograms to
provide chromatographic context.
However, for detailed inspection of spectral features, such as isotope patterns,
adducts, or relative ion intensities, it is often preferable to plot spectra on
their own, without the accompanying chromatogram.

In the example below, MS1 spectra are extracted from two samples at the scan
closest to a specified retention time and displayed together
for direct comparison.

```{r plot-standalone-spectra}
lcmsPlot(faahko, sample_id_column = 'sample_name') +
    lp_spectra(
        sample_ids = c('ko15', 'wt15'),
        rt = 2740,
        mode = "closest",
        ms_level = 1) +
    lp_legend(position = "bottom")
```

# Interoperability

lcmsPlot is designed to integrate smoothly with results generated by commonly
used LC-MS data processing software. This allows users to visualise processed
results directly in R, without the need to manually reformat outputs or
reprocess raw data.
Below we illustrate interoperability with four widely used tools:
**Compound Discoverer**, **MZmine**, **MS-DIAL**, and **LipidSearch**.

Note: These examples are for illustration only and require real data
to function properly.

## Compound Discoverer

In the example below, a Compound Discoverer results file is loaded and
used to extract chromatograms for a subset of compounds of interest.
These chromatograms are arranged by sample and compound name, making it easy
to compare signal profiles across the dataset. Highlighting detected peaks helps
relate the visualised chromatograms back to the original feature detection
performed by Compound Discoverer.

```{r plot-compound-discoverer-results, eval=FALSE}
lcmsPlot("example_cd.cdResult") +
    lp_compound_discoverer(
        compounds_query = 'name %in% c("Proline", "Betaine")',
        rt_extend = 5
    ) +
    lp_chromatogram(highlight_peaks = TRUE) +
    lp_grid(rows = "sample_id", cols = "name", free_x = TRUE) +
    lp_labels(title = "Compound Discoverer example", legend = "Sample") +
    lp_legend(position = "bottom")
```

### Compound Discoverer scripting node

Beyond loading a `.cdResult` file directly, `lcmsPlot` ships a ready-made
Compound Discoverer **Scripting Node** called **Compounds Chromatograms**. It is
a *post-processing* node: once registered, it appears under **Workflow Nodes →
Scripting nodes** in Compound Discoverer and is dropped into the post-processing
area of a workflow.

For a given Compound Discoverer result the node renders one chromatogram plot per
compound, using the same `lcmsPlot` machinery shown above, and writes one PNG
per compound. It then adds a clickable **Chromatogram Plot** column to the Compounds table so
each compound links straight to its chromatogram inside Compound Discoverer.

The node reads the original Thermo `.raw` files directly (via `rawrr`), deriving
their paths from Compound Discoverer's exported *Input Files* table, so there is
nothing to supply by hand. Internally it is driven by the
`CompoundDiscovererNodeSource()` data source that ships with the package.

The node lives inside the package at
`inst/compound-discoverer-nodes/CompoundsChromatograms/`, locatable at runtime
via `system.file("compound-discoverer-nodes/CompoundsChromatograms", package =
"lcmsPlot")`. That folder's `README.md` covers everything else — installing the
required R packages, registering the node in Compound Discoverer, and the full
list of node parameters.

## MZmine

`lcmsPlot` also supports feature lists generated by
MZmine (version 2 and above). Feature tables, raw data files, and optional
sample metadata can be combined into a single dataset object that serves
as the basis for visualisation.

```{r plot-mzmine-results, eval=FALSE}
mzmine_dataset_example <- MZmineFeatureListsSource(
    feature_lists_paths = c(
        "path/to/feature_list_1.csv",
        "path/to/feature_list_2.csv"
    ),
    sample_paths = c(
        "path/to/sample_path_1.mzML",
        "path/to/sample_path_2.mzML"
    ),
    metadata_path = "path/to/metadata.csv"
)

lcmsPlot(mzmine_dataset_example, sample_id_column = 'filename') +
    lp_chromatogram(
        features = rbind(c(mz = 222.09864, rt = 213.636)),
        sample_ids = c("sample_1", "sample_2"),
        rt_tol = 50,
        ppm = 10,
        highlight_peaks = TRUE,
        highlight_peaks_color = "#894545") +
    lp_facets(facets = "sample_id") +
    lp_labels(title = "MZmine example", legend = "Sample") +
    lp_legend(position = "bottom")
```

## MS-DIAL

Results generated by MS-DIAL can also be directly integrated into lcmsPlot.
Peak tables (corresponding to each sample) exported from MS-DIAL are combined with
the corresponding raw data files and optional sample metadata to create a unified dataset for visualisation.

```{r plot-ms-dial-results, eval=FALSE}
msdial_dataset_example <- MsDialPeaksSource(
    peaks_paths = c(
        "path/to/peaks_1.csv",
        "path/to/peaks_2.csv"
    ),
    sample_paths = c(
        "path/to/sample_path_1.mzML",
        "path/to/sample_path_2.mzML"
    ),
    metadata_path = "path/to/metadata.csv"
)

lcmsPlot(msdial_dataset_example, sample_id_column = 'filename') +
    lp_chromatogram(
        features = rbind(c(mz = 222.09864, rt = 213.636)),
        sample_ids = c("sample_1", "sample_2"),
        rt_tol = 50,
        ppm = 10,
        highlight_peaks = TRUE,
        highlight_peaks_color = "#894545") +
    lp_facets(facets = "sample_id") +
    lp_labels(title = "MS-DIAL example", legend = "Sample") +
    lp_legend(position = "bottom")
```

## LipidSearch

Lipid annotations exported by LipidSearch are read with `LipidSearchSource()`.
Both **LipidSearch 4.2** and **5.2** result files are supported and the version
is auto-detected. Unlike the peak-list sources above, a LipidSearch result file
is *compound-centric*: it names each lipid and reports, per sample, the observed
m/z, retention time, area and identification grade. `lcmsPlot` therefore treats
it like the Compound Discoverer source — you select lipids with a query and get
one extracted ion chromatogram per lipid and per sample.

A **4.2** file declares the raw files it was built from in its header
(`#[c-1]:sample.raw`). Those declarations are matched to the paths you supply by
file basename **without** the extension, so a result file listing ThermoFisher
`.raw` files works unchanged with converted `.mzML` files.

```{r plot-lipid-search-results, eval=FALSE}
lipid_search_example <- LipidSearchSource(
    results_path = "path/to/LIPIDS_POS_5ppm.txt",
    sample_paths = c(
        "path/to/sample_path_1.mzML",
        "path/to/sample_path_2.mzML"
    )
)

## Summarises the samples, lipids and integrated peaks that were read.
lipid_search_example

lcmsPlot(lipid_search_example) +
    lp_lipid_search(
        lipids_query = 'class == "AcCa" & grade == "A"',
        rt_extend = 30
    ) +
    lp_chromatogram(highlight_peaks = TRUE, ppm = 5) +
    lp_grid(rows = "sample_id", cols = "name", free_x = TRUE) +
    lp_labels(title = "LipidSearch example", legend = "Sample") +
    lp_legend(position = "bottom")
```

By default only lipids that LipidSearch accepted are kept; pass
`keep_rejected = TRUE` to `LipidSearchSource()` to retain the rows flagged with
`Rej. == 1` as well. Where the same lipid ion is reported at several retention
times, each occurrence gets its own plot label (e.g. `AcCa(14:0)+H [RT 2.24]`).

The `lipids_query` expression is evaluated against the lipid table, which exposes
the LipidSearch annotations — `lipid_ion`, `class`, `fatty_acid`, `formula`,
`adduct`, `grade`, `mscore`, `sn`, `detected` — alongside the usual `mz`, `rt`,
`rtmin`, `rtmax`, `into` and `maxo` columns. A `lipid_rank` column ranks lipids by
descending total area, which is convenient for plotting only the most abundant
ones:

```{r plot-lipid-search-top-n, eval=FALSE}
lcmsPlot(lipid_search_example) +
    lp_lipid_search(lipids_query = 'lipid_rank <= 6') +
    lp_chromatogram(highlight_peaks = TRUE, ppm = 5) +
    lp_grid(rows = "sample_id", cols = "name", free_x = TRUE)
```

# Session Info

```{r session-info}
sessionInfo()
```
