How do I align peak labels?

Viewed 188

I am attempting to graph data with peaks in R. The graphing itself has progressed well, but I've run into issues with labelling the relevant peaks. My current labelling system, detailed below, shifts the peak labels oddly to the side and results in lines crossing each other. Is there a way to align labels with the peaks themselves, or otherwise organize them aesthetically?

The following code reproduces my problem, using this data.

library(ggplot2)
library(ggpmisc)
library(ggrepel)

x=read.csv("data.csv")

colnames(x)=c("wv", "abs")

ggplot(x, aes(x=wv, y=abs)) + geom_line() + xlab(bquote('Wavenumbers ('~cm^-1*')')) + ylab("Absorbance (A.U.)") + scale_x_reverse(limits=c(2275,1975), expand=c(0,0)) + ylim(-0.01,0.29) + stat_peaks(colour = "black", span = 11,  geom ="text_repel", direction = "y", angle = 90, ignore_threshold = 0.09, size = 3, x.label.fmt = "%.2f", vjust = 1, hjust = 0, segment.color = "red") + ggtitle("FTIR - Carbon Monoxide, Fundamentals")

Problematic Labels

3 Answers

hjust = 0.5 should work better. Using hjust = 0 aligns your labels a little to the right, with the top edge of the text aligned with the middle of each peak.

Here's a reproducible example that doesn't rely on external data that might not remain available at that link. (See bottom for application to the OP dataset.)

library(ggpmisc)
library(ggrepel)
library(ggplot2)
x <- data.frame(wv = 2300:2000)
x$abs = abs(cos(x$wv/50) * sin(x$wv/2))
ggplot(x, aes(wv, abs)) +
  geom_line() +
  stat_peaks(colour = "black", span = 11,  
             geom ="text_repel", direction = "y", 
             angle = 90, ignore_threshold = 0.09, 
             size = 3, x.label.fmt = "%.2f", 
             vjust = 1, hjust = 0.5, segment.color = "red") +
  scale_x_reverse(limits = c(2300,2000))

enter image description here

Here's loading the original data:

library(readr)
x <- read_csv("~/Downloads/CO-FTIR Spectrum-1800 mTorr-2021.csv")
colnames(x)=c("wv", "abs")

Here with these parameters added:

box.padding = 0.0, nudge_y = 0.02,

enter image description here

I wasn't able to find a way to do this without constructing an auxiliary dataset. However, once constructed the actual plotting then becomes fairly easy:

The code is as follows:

library(dplyr)
library(ggplot2)
library(ggpmisc)
library(ggrepel)
library(pracma)


#####################################
# Constants
#####################################
wvfilter = c(1975, 2275)
data_filename = "data.csv"


#####################################
# Read data
#####################################
dat = read.csv(data_filename)
colnames(dat)=c("wv", "abs")


#####################################
# Identify peaks
#####################################

# Extract peaks
peaks = data.frame(findpeaks(dat$abs, threshold=0, minpeakheight=0.03))
# Give data frame reasonable names
colnames(peaks) = c("height", "x_index", "peak_begin_index", "peak_end_index")
# Convert from index to wavelength
peaks$wv = dat$wv[peaks$x_index]
# Set the y position of the labels
peaks$nudge = 0.3-peaks$height
# Filter by wavelength
peaks = peaks %>% filter(wvfilter[1] <= wv & wv <= wvfilter[2])


#####################################
# Plot data
#####################################

ggplot(x, aes(x=wv, y=abs)) +
  geom_line() +
  geom_text_repel(
    data=peaks,
    mapping=aes(x=wv, y=height, label=height),
    force=0,
    nudge_y=peaks$nudge,
    direction="x",
    angle=90,
    segment.color="red"
  ) +
  xlab(bquote('Wavenumbers ('~cm^-1*')')) +
  ylab("Absorbance (A.U.)") +
  scale_x_reverse(limits=rev(wvfilter), expand=c(0,0)) +
  ylim(-0.01,0.29) +
  ggtitle("FTIR - Carbon Monoxide, Fundamentals")

And this gives the plot: A better spectral peak plot

Some notes:

  • In the image I'm using the wrong text for the labels. Fix this with label=wv instead of label=height.
  • The peaks dataset needs to be filtered using the same values passed to scale_x_reverse(limits=. To simplify this, I've introduced a global variable for setting the x-axis limits.
  • Playing with the force argument gives different degrees of clustering of the labels. force=0 seems to work best for your data.
  • You need to manually set the peaks$nudge value. I don't know of a good way of doing this other than guess-and-check.

Here's an approach using ggplot2::stage and the built-in segment and text geometries:

library(ggplot2)
library(ggpmisc)

id <- "1S345TaPqANriDPLN6H_PyunuM1QHi485"
x <- read.csv(sprintf("https://docs.google.com/uc?id=%s&export=download", id))
colnames(x)=c("wv", "abs")

ggplot(x, aes(x=wv, y=abs)) + geom_line() +
  xlab(bquote('Wavenumbers ('~cm^-1*')')) + ylab("Absorbance (A.U.)") + ggtitle("FTIR - Carbon Monoxide, Fundamentals") + ylim(-0.01,0.29) +
  stat_peaks(mapping = aes(x = stage(wv, after_scale = x + 0.25)),
             geom ="text", colour = "black", span = 11, angle = 90,
             ignore_threshold = 0.09, size = 2.5, x.label.fmt = "%.2f",
             vjust = 0.5, hjust = -1) +
  stat_peaks(mapping = aes(xend = stage(wv, after_scale = x + 0.25),
                           y = stage(abs, after_stat = y + 0.005),
                           yend = after_stat(y + 0.02)),
             geom ="segment", lwd = 0.5, colour = "red", 
             span = 11, ignore_threshold = 0.09) +
  scale_x_reverse(expand=c(0,0), limits = c(2275,1975))

enter image description here

From help(stat_peaks) we can see that x and y stats are calculated for each peak. Typically we can access these stats with after_stat, but because you've also transformed the x-axis, you need to actually access that stat after it's been scaled.

In the first layer of stat_peaks we use stage(wv, after_scale = x + 0.25) to add a bit of nudge to the x coordinate. In the second call of stat_peaks we set the geometry to segment. Segment requires an xend and yend. We can use stage and after_stat to again add a nudge and line length.

Related