1

The goal

I'd like to have a function that processes some input X and produces a gglot graph using geom_point.

That function should allow to map columns of X to various aesthetics inside aes() (via arguments .shapefac, .colfac, etc.), but also allow to set e.g. point colour and shape manually (e.g. colour = "tomato"), outside of aes().

The problem

If .shapefac or .colfac arguments are provided, the aesthetics get overridden by the specification of shape and colour, respectively. This is explained e.g. here.

What I've tried

One workaround would be to remove the shape and colour arguments, and allow them to be specified using ... in geom_point. This works, but needs some advanced R knowledge for a user of such a function.

The question

Does anybody know if there is

  • a way to change the default setting for the shape and colour arguments outside of aes() in geom_point?

  • a way to override the specification of the shape and colour arguments, if an aesthetic is used?

  • another way to reach the goal stated above, without using ...?

Any hints or explanations are much appreciated!

The following function and plots hopefully illustrate the problem.

define function

library(mlbench)
data("Ionosphere")

cplot <- function(X, v = 1:ncol(X), .shapefac = NULL, .colfac = NULL, shape = 21, colour = "black",
                  center = TRUE, scale = FALSE, x = 1, y = 2, plot = TRUE) {
  
  library(ggplot2)
  # some code processing X to Y
  d.pca <- prcomp(X, center = center, scale. = scale)
  Y <- data.frame(X, d.pca$x)
  v <- round(100 * (d.pca$sdev^2 / sum(d.pca$sdev^2)), 2)
  
  # plot PCA
  p <- ggplot(Y, aes_string(x = paste0("PC",x), y = paste0("PC",y))) +
    geom_point(aes_string(shape = .shapefac, colour = .colfac),
               shape = shape, color = colour) +
    labs(x = paste0("PC ", x, " (", v[x], "%)"),
         y = paste0("PC ", x, " (", v[y], "%)")) +
    theme_bw()
  if (plot) print(p)
  invisible(p)
}

setting colour and shape arguments outside aes()

This works as it should.

cplot(Ionosphere, v = 3:34, colour = "tomato", shape = 4)

setting shape inside aes() when shape outside aes() has a default of 21

The default shape = 21 outside aes() overrides the shape aesthetic.

cplot(Ionosphere, v = 3:34, .shapefac = "Class")

setting colour inside aes() when colour outside aes() has a default of "black"

The default colour = "black" outside aes() overrides the colour aesthetic.

cplot(Ionosphere, v = 3:34, .colfac = "Class")

failed trials to set default shape and colour to NULLor NA

# results in an empty plot (shape = NA)
cplot(Ionosphere, v = 3:34, .shapefac = "Class", shape = NA)
#> Warning: Removed 351 rows containing missing values (geom_point).

# results in an error
cplot(Ionosphere, v = 3:34, .shapefac = "Class", shape = NULL)
#> Error: Aesthetics must be either length 1 or the same as the data (351): shape

Created on 2021-09-20 by the reprex package (v2.0.1)

scrameri
  • 667
  • 2
  • 12
  • I saw some ideas for changing the default point shape here: https://stackoverflow.com/questions/14196804/how-to-change-default-aesthetics-in-ggplot – aosmith Sep 20 '21 at 18:57
  • how should a mapping on `"Class"` work if you remove it from the data? -> `Ionosphere[,-c(1,2,35)]` – mnist Sep 20 '21 at 19:28
  • Sorry that was a typo, not sure why reprex still worked, probably because X was assigned. I fixed it in the question. – scrameri Sep 20 '21 at 19:39
  • @aosmith I think this is what I was looking for, thanks! – scrameri Sep 20 '21 at 19:41

1 Answers1

1

One option to achieve your desired result may look like so:

  1. If the aesthetics are provided set the color and/or shape params to NULL
  2. Make use of modifyList to construct a list of arguments to be passed to geom_point which includes the mapping and the non-NULL parameters. Making use modifyList will drop any NULL.
  3. Make use of do.call to call geom_point with the list of arguments.

Note: I slightly changed your function to select only numeric columns for the PCA.

library(mlbench)
library(ggplot2)

data(Ionosphere)

cplot <- function(X, .shapefac = NULL, .colfac = NULL, shape = 21, colour = "black",
                  center = TRUE, scale = FALSE, x = 1, y = 2, plot = TRUE) {
  
  col_numeric <- unlist(lapply(X, is.numeric))
  
  # some code processing X to Y
  d.pca <- prcomp(X[, col_numeric], center = center, scale. = scale)
  Y <- data.frame(X, d.pca$x)
  v <- round(100 * (d.pca$sdev^2 / sum(d.pca$sdev^2)), 2)
  
  colour <- if (is.null(.colfac)) colour
  shape <- if (is.null(.shapefac)) shape
  
  mapping <- aes_string(shape = .shapefac, colour = .colfac)
  args <- modifyList(list(mapping = mapping), list(color = colour, shape = shape))
  
  geom <- do.call("geom_point", args)
  
  p <- ggplot(Y, aes_string(x = paste0("PC", x), y = paste0("PC", y))) +
    geom +
    labs(
      x = paste0("PC ", x, " (", v[x], "%)"),
      y = paste0("PC ", x, " (", v[y], "%)")
    ) +
    theme_bw()
  if (plot) print(p)
  invisible(p)
}

cplot(Ionosphere, colour = "tomato", shape = 4)

cplot(Ionosphere, .shapefac = "Class")

cplot(Ionosphere, .colfac = "Class")

cplot(Ionosphere, .colfac = "Class", .shapefac = "Class")

cplot(Ionosphere, .shapefac = "Class", shape = NULL)

stefan
  • 90,330
  • 6
  • 25
  • 51
  • Wow @stefan, this is exactly what I was looking for! Interesting use of `if`, and I learnt about `modifyList`. This solution also seems better compared to using `update_geom_defaults`, because it truly overrides any specification of e.g. `shape` if `.shapefac` is given, also for the legend. – scrameri Sep 20 '21 at 20:12