Retrieving the metadata (such as pixel coordinates of the plotted data points and axes margins) in ggplot2

Hello, I would like to generate a synthetic dataset comprising of scatter plots. I would like to use this dataset for the training of a computer vision model. So, for the automatic annotation of data points of different data shapes, I would like to know if there is any in-built function in ggplot2 that can provide us with their pixel coordinates.
If not, then do we have the possibility to fix the margin of axes to our desirable values? If this is also not possible, then at least can we know the axes margin values used by ggplot2? The issue is that the margin values dynamically change depending on the size and format of the labels and axes values.

Hi Vipul,

You can do all of the above — get and set coordinates and margin values, and more — but what to suggest would depend on the complexity of what you would like to do. How would you describe your experience and proficiency with R and ggplot? An do you know what specifications you would want the generated data and resulting plot to satisfy?

@dromano ,my R proficiency is between intermediate to advanced, but I am quick learner ;). I found the package "wrGraph", which basically can provide a handy function convertPlotCoordPix(...) to get pixel coordinates corresponding to data points. However, this function expects the margin vector to get accurate pixel coordinates. Since my proficiency with ggplot2 is very limited, I was just wondering if there is a way to get the pixel coordinates of data points directly, so that I don't have to be dependent on calculating the margin values. Regarding specifications - I would like to have as much diversity in the data as possible in terms of axes scale (linear or logarithmic), data shapes, themes, legend position, different sizes for label, axis scale, and shape. This part is almost implemented. I am only lacking with pixel coordinates. In the first step, corresponding to each plot I would like to generate annotation, which is box/rectangle coordinates for data point shapes . So, knowing the way to retrieve the center pixel coordinates for corresponding data point shapes is all I need at the moment. Let me know if this information helps you.

Thank you; this is very helpful background for folks here who may be able to help.

And to confirm I understand you correctly: Are the coordinates you're after the literal pixel coordinates on the user's screen? Or would the coordinates relative, say, to the user's RStudio plot window sufficient? I guess the underlying question is how you intend to process the coordinates once they've been extracted.

@dromano let us assume a plot p and filename x. If we save the plot in the following way:

ggsave(x, p, height = 640, width = 640, units = "px", dpi = 100)

then the pixel coordinates I am referring to are the literal pixel coordinates corresponding to data points in the plot file x. E.g., if we have a data point pt1, then pt1(w,h) should be in the range ([1:640], [1:640]).

I see, so to confirm: you would be extracting the coordinate information directly from an image file, then?

@dromano that will be the application on exisiting data. First, in order to train the model, I need those pixel coordinate information for my synthetically generated plots.

I'm still a little confused then: Are you saying you want the pixel coordinates of the centers of the points that appear in a scatterplot that is to be saved in a 640 x 640 image file? Could you say a little more about why the coordinates as, say, supplied directly to ggplot() won't suffice for your purposes?

@dromano i intend to use those pixel coordinates of points as labels in my supervised machine learning task.

The problem here seems to me to be this: A plot produced by a tool like ggplot() can be either rendered, say, in RStudio's plot window, or saved as an image file. This means that the pixel coordinates of (the center of) a point in the plot depend on either the size of RStudio's plot window (which can be changed interactively by the user) or on the dimensions of the image file. So, as far as I can see, there is no natural choice of pixel coordinates unless you specify the pixel dimensions of the rendered image in advance. Unless you're not worried about image dimensions at all in your machine learning task, in which case the original coordinates passed to ggplot() could serve the same purpose (once translated to all be nonnegative).

There could well be something I'm missing, but as it stands, it doesn't seem to me that your request is phrased precisely enough for folks to be able to help you. Maybe if you gave a concrete example of what you mean — like a specific scatterplot, along with the pixel coordinates you would expect to correspond to it — that could help to clarify what you'd like to accomplish.

Would this work? Set the expand value of the axes to zero and use coord_fixed to remove distortion, then save with the desired pixel dimensions.

library(ggplot2)
library(ggrepel)

n_pixels <- 512

# create a random  x and y value in the range (0:n_pixels)
df <- data.frame(
  x <- as.integer(runif(1, 0, n_pixels)),
  y <- as.integer(runif(1, 0, n_pixels))
)

ggplot(df, aes(x, y)) +
  geom_point(size = 1, color = "black") +
  # ggplot has (0,0) in the bottom left corner, so reverse the y value
  geom_text_repel(label = paste0("(", x, ",", n_pixels - y, ")"), size = 4) +
  scale_x_continuous(limits = c(0, n_pixels), expand = c(0, 0)) +
  scale_y_continuous(limits = c(0, n_pixels), expand = c(0, 0)) +
  coord_fixed() +
  theme_void() +
  theme(plot.background = element_rect(fill = "white", color = "white"))

# save a n_pixel x n_pixel image with ggsave
ggsave(
  "point.png",
  plot = last_plot(),
  units = "px",
  width = n_pixels,
  height = n_pixels,
  dpi = ppi
)

It occurs to me that sharing an example you build with wrGraph and the corresponding pixel coordinate output you get might be the best way to illustrate what you're after — could you do that?

@mduvekot thank you for your time. Your provided solution at least gives the correct coordinates for the points. However, this is only true as long as theme_void() is used. The moment we introduce the axes (with the default theme or any other themes) the pixel values don't match with the data point coordinates.

Could you confirm this with an example?

@dromano Below are two examples. In Example1, the coordinates are accurate. If you note here you will find that I have manually calculated the margin vector. The moment we have different axis range (e.g., in hundreds or decimal), or change in the size of the label/axis values, or positioning of the legend, this margin vector will no more be applicable. To see the effects, please refer to Example2. Hope this will help you to understand the problem!

library(ggplot2)
library(wrGraph)

#Example1:
seed(2)
pngFile <- file.path(tempdir(),"test.png")
png(pngFile, width=600, height=400, res=72)
df <- data.frame(x=sample(90,5), y=sample(90,5), shapes=factor(sample(5,5, replace = T)))
ggplot(df, aes(x, y)) + 
  geom_point(aes(shape=shapes), size=5) +
  theme_classic()
dev.off()
df1 <- cbind(df,convertPlotCoordPix(x=df[,1], y=df[,2], useMar = c(2.8,2.95,0.78,4.97), plotD=c(600,400), plotRes=72))
#note that useMar relies on par(). so unlike margin vector in ggplot2 (t,r,b,l), here the margin vector arguments are (b,l,t,r)

#Example2:
seed(2)
pngFile <- file.path(tempdir(),"test1.png")
png(pngFile, width=600, height=400, res=72)
df <- data.frame(x=sample(200,5), y=sample(200,5), shapes=factor(sample(5,5, replace = T)))
ggplot(df, aes(x, y)) + 
  geom_point(aes(shape=shapes), size=5) +
  theme_classic()
dev.off()
df1 <- cbind(df,convertPlotCoordPix(x=df[,1], y=df[,2], useMar = c(2.8,2.95,0.78,4.97), plotD=c(600,400), plotRes=72))
1 Like

Thanks, Vipul — this is very helpful.

@dromano Here is an example with theme_classic() to see the effects:

library(ggplot2)
library(ggrepel)

seed(2)
n_pixels <- 512
# create a random  x and y value in the range (0:n_pixels)
df <- data.frame(
  x <- as.integer(runif(2, 0, n_pixels)),
  y <- as.integer(runif(2, 0, n_pixels))
)

ggplot(df, aes(x, y)) +
  geom_point(size = 1, color = "black") +
  # ggplot has (0,0) in the bottom left corner, so reverse the y value
  geom_text_repel(label = paste0("(", x, ",", n_pixels - y, ")"), size = 4) +
  scale_x_continuous(limits = c(0, n_pixels), expand = c(0, 0)) +
  scale_y_continuous(limits = c(0, n_pixels), expand = c(0, 0)) +
  coord_fixed() +
  theme_classic() + 
  theme(plot.background = element_rect(fill = "white", color = "white"))

# save a 100 x 100 pixel image with ggsave
ggsave(
  "point.png",
  plot = last_plot(),
  units = "px",
  width = n_pixels,
  height = n_pixels,
  dpi = ppi
)

The package wrGraph seems to be built to report pixel coordinates of points plotted with the base R function plot() and so is not easily made compatible with plots made with ggplot().

However, the code you shared does clarify some of what you're after. Did you intend to use different width, height, and resolution values for your synthetic plots? (I suspect different resolution values could break the relationship between abstract and pixel coordinates.)

@dromano I am aware of this compatibility issue, but the same issue is there with base plot() function as well. The margin vector has to be tuned. Nevertheless, I also explored ggplot_build() and ggplot_gtable() to find the right metadata, but it didn't help so far. I therefore asked here if any internal functions of ggplot() provide us with the feature of retrieving the pixel coordinates for data points before drawing at grid level. Of course, I can generate plots with a fixed setting, but this will impact the diversity in the training data, and thereby the algorithm will perform poorly on unseen data. Having said that, I am keeping the height, width, and resolution constant. The problem is really at the drawing of XY axes. Even if we use plot.margin(), it seems that the axis margins are calculated dynamically by ggplot(). The axis range, font size, and axis label size are influencing factors here.

I'll continue to think about how to extract pixel data from a plot made with ggplot(), but it occurs to me since all of the plotting elements in a plot made with ggplot() are specifiable in advance, you should in principle be able to calculate the pixel coordinates directly from the parameters that go into making your synthetic plots. In other words, you could ensure that you're making plots with given pixel coordinates rather than figuring out how to extract them once the plots have been made. The main reason I expect pre-calculation to be easier than post-calculation is that plotting in general represents a translation of high-level specifications into low-level elements.