uclahs-cds / package-BoutrosLab-plotting-general

Functions to Create Publication-Quality Plots
https://uclahs-cds.github.io/package-BoutrosLab-plotting-general
12 stars 4 forks source link

Inconsistent `add.text` behavior on panels when grouping terms are used `(y ~ x | g)` #138

Open stefaneng opened 1 year ago

stefaneng commented 1 year ago

Related to #137

add.text has different behavior in a few different plotting functions. In particular, create.boxplot differs in behavior from most of the others.

Boxplot cannot add multiple text labels

A boxplot with multiple text labels and text.x or text.y will only draw the the first label but replicated for each of the coordinates.

library(BoutrosLab.plotting.general);

set.seed(779);
groupA <- rnorm(n = 100, mean = 10, sd = 2);
groupB <- rnorm(n = 134, mean = 10.5, sd = 2);

# Create data frame for plotting
to.plot <- data.frame(
    y = rep(
        c('1', '2'),
        times = c(100, 134)
        ),
    x = c(groupA, groupB)
    );

to.plot.groups <- rbind(
  data.frame(to.plot, z = 1),
  data.frame(to.plot, z = 2)
  )
to.plot.groups$z <- as.factor(to.plot.groups$z);

create.boxplot(
    formula = x ~ y,
    data = to.plot.groups,
    add.stripplot = TRUE,
    add.text = TRUE,
    text.labels = c('A', 'B'),
    text.x = c(1, 2),
    text.y = 15.3,
    text.col = 'black',
    text.cex = 1.5,
    text.fontface = 'bold',
    ylimits = c(
        min(to.plot$x) - abs(min(to.plot$x) * 0.1),
        max(to.plot$x) + abs(max(to.plot$x) * 0.1)
        )
    );
#> Warning in validDetails.text(x): NAs introduced by coercion

Boxplots grouped by a variable will put text in different panels

Only if a grouping variable is used will boxplot place one text label per each panel. This is actually opposite to what other plots do but this behavior seems the most intuitive.

# Boxplots grouped by a variable will put text in different panels
# if text.labels is a vector
create.boxplot(
    formula = x ~ y | z,
    data = to.plot.groups,
    add.stripplot = TRUE,
    add.text = TRUE,
    text.labels = c('A', 'B'),
    text.x = 1,
    text.y = 15.3,
    text.col = 'black',
    text.cex = 1.5,
    text.fontface = 'bold',
    ylimits = c(
        min(to.plot$x) - abs(min(to.plot$x) * 0.1),
        max(to.plot$x) + abs(max(to.plot$x) * 0.1)
        )
    );
#> Warning in validDetails.text(x): NAs introduced by coercion

#> Warning in validDetails.text(x): NAs introduced by coercion

Scatterplots draw multiple text labels as expected

Without grouping variables scatterplots draw on a single panel as expected with vector arguments.

to.plot.scatter <- data.frame(
  x = rnorm(100),
  y = rnorm(100),
  z = c(1,2)
  );
to.plot.scatter$z <- as.factor(to.plot.scatter$z);

create.scatterplot(
  y ~ x,
  data = to.plot.scatter,
  add.text = TRUE,
  text.labels = c('a', 'b'),
  text.x = c(1, 2),
  text.y = 3
  );

Scatterplots with groups place same text on both panels

Scatterplots will draw the same text on each panel, following the same behavior as plot without groups. This is usually not what is desired and is somewhat counter-intuitive.


# Groups place the same text on both panels
(group.scatter.text <- create.scatterplot(
  y ~ x | z,
  data = to.plot.scatter,
  add.text = TRUE,
  text.labels = c('a', 'b'),
  text.x = c(1, 2),
  text.y = 3,
  xlimits = c(-5, 5),
  ylimits = c(-5, 5)
  ))

Solution: Add different text to each panel

A solution to allow for text to be added to each panel separately is to manually create a latticeExtra::layer with lattice::panel.text. We can access the panel number with panel.number() and can create a vector of labels to index.


# A fix is to add text manually
my.panel.text <- c('panel 1', 'panel 2');

group.scatter.text + layer({
  panel.text(
    # Access panel number with panel.number()
    labels = my.panel.text[panel.number()],
    x = -3,
    y = -3
    )
  });

Sometimes we don't want to specify x and y coordinates but want to specify on a [0,1] scale. In this case we can use panel.key and the coordinates with corner.


# Using panel.key allows for 'corner' specification [0-1]
group.scatter.text + layer({
    panel.key(
        text = my.panel.text[[panel.number()]],
        points = FALSE,
        corner = c(0.05, 0.95)
        )
  });

Created on 2023-07-15 with reprex v2.0.2

jarbet commented 10 months ago

There is a strange issue where @stefaneng's solution for adding text to different panels of create.scatterplot does not work when calling create.scatterplot and defining my.panel.text within a larger wrapper function (e.g. making a new function for an R package):

suppressPackageStartupMessages(library(BoutrosLab.plotting.general));
to.plot.scatter <- data.frame(
  x = rnorm(100),
  y = rnorm(100),
  z = c(1,2)
  );
to.plot.scatter$z <- as.factor(to.plot.scatter$z);

wrapper <- function() {
    group.scatter.text <- create.scatterplot(
      y ~ x | z,
      data = to.plot.scatter
      );

    my.panel.text <- c('panel 1', 'panel 2');

    group.scatter.text + layer({
        panel.key(
            text = my.panel.text[[panel.number()]],
            points = FALSE,
            corner = c(0.05, 0.95)
            )
        });
    }
wrapper();

Created on 2024-01-25 with reprex v2.0.2

stefaneng commented 10 months ago

Hey @jarbet ! layers/panels are weird and don't have a standard environment so you need to pass the data via the data argument. I don't remember the specifics of how the layer environment works but this will definitely allow you to pass the info you need from the function to the plot.

suppressPackageStartupMessages(library(BoutrosLab.plotting.general));
to.plot.scatter <- data.frame(
  x = rnorm(100),
  y = rnorm(100),
  z = c(1,2)
);
to.plot.scatter$z <- as.factor(to.plot.scatter$z);

wrapper <- function() {
  group.scatter.text <- create.scatterplot(
    y ~ x | z,
    data = to.plot.scatter
  );

  my.panel.text <- c('panel 1', 'panel 2');

  group.scatter.text + layer({
    panel.key(
      text = my.panel.text[[panel.number()]],
      points = FALSE,
      corner = c(0.05, 0.95)
    )
  }, data = list(my.panel.text = my.panel.text));
}
wrapper()

Created on 2024-01-25 with reprex v2.1.0

jarbet commented 10 months ago

Thanks @stefaneng, this works great! Hope school is going well!!

jarbet commented 8 months ago

@stefaneng thanks again for your help with the above problem! I'm curious if you have any ideas for how to put text above/below bars in create.barplot when having multiple panels?

At a minimum, I know I can separately make each plot and add the text using text.above.bars, then combine using create.multipanelplot. However, I'm wondering if you think it is possible to more directly add the text when using | in the create.barplot formula?

For example, putting qvalue above the bars in each panel?

suppressPackageStartupMessages(library(BoutrosLab.plotting.general));

set.seed(123);
dataset <- data.frame(
    feature = c(rep('a', 3), rep('b', 3)),
    group = rep(1:3, 2),
    rho = runif(6, -1, 1),
    qvalue = runif(6, 0, 1)
    );

# how to put q-values above bars in each panel?
create.barplot(
    formula = rho ~ group | feature,
    data = dataset
    );

Created on 2024-03-14 with reprex v2.0.2

stefaneng commented 8 months ago

There is probably a better way to do this via lattice... but almost the same solution as the other one is to split by the feature variable and pass the vector into each panel:

suppressPackageStartupMessages(library(BoutrosLab.plotting.general));

set.seed(123);
dataset <- data.frame(
  feature = c(rep('a', 3), rep('b', 3)),
  group = rep(1:3, 2),
  rho = runif(6, -1, 1),
  qvalue = runif(6, 0, 1)
);

# Create a lattice barplot with q-values above bars grouped by feature rho ~ group | feature
qvalue_feature_split <- split(round(dataset$qvalue, 2), dataset$feature)

create.barplot(
  formula = rho ~ group | feature,
  data = dataset,
  ylimits = c(-1.2, 1.2)
) + layer({
  ltext(x, y + sign(y) * 0.15, labels = qvalue_feature_split[[panel.number()]])
}, data = list(qvalue_feature_split = qvalue_feature_split))

Created on 2024-03-15 with reprex v2.1.0

jarbet commented 8 months ago

There is probably a better way to do this via lattice... but almost the same solution as the other one is to split by the feature variable and pass the vector into each panel:

suppressPackageStartupMessages(library(BoutrosLab.plotting.general));

set.seed(123);
dataset <- data.frame(
  feature = c(rep('a', 3), rep('b', 3)),
  group = rep(1:3, 2),
  rho = runif(6, -1, 1),
  qvalue = runif(6, 0, 1)
);

# Create a lattice barplot with q-values above bars grouped by feature rho ~ group | feature
qvalue_feature_split <- split(round(dataset$qvalue, 2), dataset$feature)

create.barplot(
  formula = rho ~ group | feature,
  data = dataset,
  ylimits = c(-1.2, 1.2)
) + layer({
  ltext(x, y + sign(y) * 0.15, labels = qvalue_feature_split[[panel.number()]])
}, data = list(qvalue_feature_split = qvalue_feature_split))

Created on 2024-03-15 with reprex v2.1.0

Genius! Thanks!!