Search code examples
rchartswaffle-chart

How to show groups with values < 1 on waffle chart legend


I am trying to have the waffle plot made from the hrbrmstr/waffle package show the group square in the legend even for groups that have values smaller than 1. However, I am not able to make it work.

I am using the following Stackoverflow solutions to troubleshoot this problem below: Waffle plot does not show one group

# remotes::install_github("hrbrmstr/waffle")
library(waffle)

df <- structure(list(Category = c(
  "Group1", "Group2", "Group3", 
  "Group4", "Group5", "Group6"), 
  Percentage = c(50.31, 39.95, 
                 6.21, 3.10, 0.41, 0)), 
  row.names = c(NA, -6L), class = "data.frame")

# Problem is the last two groups have no colour on the legend
waffle::waffle(df)

# Try to add them manually, but doesn't work
waffle::waffle(df, colors = c(RColorBrewer::brewer.pal(nrow(df), "Set2")))

# If we add one more colour, then we get a weird filling, which I don't want
waffle::waffle(df, colors = c(RColorBrewer::brewer.pal(nrow(df) + 1, "Set2")))

# We try with scale_fill_manual, but we get an error
waffle::waffle(df) +
  ggplot2::scale_fill_manual(
    values = c(RColorBrewer::brewer.pal(nrow(df), "Set2")),
    drop = FALSE)
#> Scale for fill is already present.
#> Adding another scale for fill, which will replace the existing scale.
#> Error in `palette()`:
#> ! Insufficient values in manual scale. 7 needed but only 6 provided.

# We add +1 to make it work, but it solves none of our problems :/
waffle::waffle(df) +
  ggplot2::scale_fill_manual(
    values = c(RColorBrewer::brewer.pal(nrow(df) + 1, "Set2")),
    drop = FALSE)
#> Scale for fill is already present.
#> Adding another scale for fill, which will replace the existing scale.

# We try with a vector instead of a data frame but same thing...
vec <- setNames(df$Percentage, df$Category)

waffle::waffle(vec)

waffle::waffle(vec, colors = c(RColorBrewer::brewer.pal(nrow(df) + 1, "Set2")))

waffle::waffle(vec) +
  ggplot2::scale_fill_manual(
    values = c(RColorBrewer::brewer.pal(nrow(df) + 1, "Set2")),
    drop = FALSE)
#> Scale for fill is already present.
#> Adding another scale for fill, which will replace the existing scale.

Created on 2024-05-17 with reprex v2.1.0

Note that I also do not want to use the ceiling() solution because (1) the total will go over 100%, and (2) this will misrepresent the data if it is 0.00001%.


Solution

  • I don't think this can be done without some alterations to the waffle code. You might consider submitting a feature request, but incorporating groups with values < 1 introduces some design questions without (it seems to me) satisfactory answers.

    I command-clicked on waffle::waffle to get its source code. I made a modified version with this addition just below if (inherits(use_glyph, "logical")) {

    gg <- gg + geom_tile(aes(fill = value), color = "white", size = size,
                         data = data.frame(x = 1, y = 1,
                                           value = part_names))
    

    This will make a layer underneath the main one, which gets covered by the main one but still generates the desired legend:

    enter image description here

    Here's that code (based on waffle 1.0.2), not guaranteed to work in any other situations!

    waffle2 <- function (parts, rows = 10, keep = TRUE, xlab = NULL, title = NULL, 
                         colors = NA, size = 2, flip = FALSE, reverse = FALSE, equal = TRUE, 
                         pad = 0, use_glyph = FALSE, glyph_size = 12, glyph_font = "Font Awesome 5 Free Solid", 
                         glyph_font_family = "FontAwesome5Free-Solid", legend_pos = "right") 
    {
      if (inherits(parts, "data.frame")) {
        parts <- stats::setNames(unlist(parts[, 2], use.names = FALSE), 
                                 unlist(parts[, 1], use.names = FALSE))
      }
      part_names <- names(parts)
      if (length(part_names) < length(parts)) {
        part_names <- c(part_names, LETTERS[1:length(parts) - 
                                              length(part_names)])
      }
      names(parts) <- part_names
      if (all(is.na(colors))) 
        colors <- suppressWarnings(RColorBrewer::brewer.pal(length(parts), 
                                              "Set2"))
      parts_vec <- unlist(sapply(1:length(parts), function(i) {
        rep(names(parts)[i], parts[i])
      }))
      if (reverse) 
        parts_vec <- rev(parts_vec)
      dat <- expand.grid(y = 1:rows, x = seq_len(pad + (ceiling(sum(parts)/rows))))
      dat$value <- c(parts_vec, rep(NA, nrow(dat) - length(parts_vec)))
          
      if (!inherits(use_glyph, "logical")) {
        if (length(use_glyph) == 1L) {
          if (grepl("wesom", glyph_font)) {
            fontlab <- .fa_unicode[.fa_unicode[["name"]] == 
                                     use_glyph, "unicode"]
            dat$fontlab <- c(rep(fontlab, length(parts_vec)), 
                             rep("", nrow(dat) - length(parts_vec)))
          }
          else {
            dat$fontlab <- c(rep(use_glyph, length(parts_vec)), 
                             rep("", nrow(dat) - length(parts_vec)))
          }
        }
        else if (length(use_glyph) == length(parts)) {
          if (grepl("wesom", glyph_font)) {
            fontlab <- .fa_unicode[.fa_unicode[["name"]] %in% 
                                     use_glyph, "unicode"]
            dat$fontlab <- c(fontlab[as.numeric(factor(parts_vec, 
                                                       levels = names(parts)))], rep("", nrow(dat) - 
                                                                                       length(parts_vec)))
          }
          else {
            dat$fontlab <- c(use_glyph[as.numeric(factor(parts_vec, 
                                                         levels = names(parts)))], rep("", nrow(dat) - 
                                                                                         length(parts_vec)))
          }
        }
        else if (length(use_glyph) == length(parts_vec)) {
          if (grepl("wesom", glyph_font)) {
            fontlab <- .fa_unicode[.fa_unicode[["name"]] %in% 
                                     use_glyph, "unicode"]
            dat$fontlab <- c(fontlab, rep(NA, nrow(dat) - 
                                            length(parts_vec)))
          }
          else {
            dat$fontlab <- c(use_glyph, rep(NA, nrow(dat) - 
                                              length(parts_vec)))
          }
        }
        else {
          stop("'use_glyph' must have length 1, length(parts), or sum(parts)")
        }
      }
      dat$value <- ifelse(is.na(dat$value), " ", dat$value)
      if (" " %in% dat$value) 
        part_names <- c(part_names, " ")
      if (" " %in% dat$value) 
        colors <- c(colors, "#00000000")
      dat$value <- factor(dat$value, levels = part_names)
      gg <- ggplot(dat, aes(x = x, y = y))
      if (flip) 
        gg <- ggplot(dat, aes(x = y, y = x))
      gg <- gg + theme_bw()
      if (inherits(use_glyph, "logical")) {
        gg <- gg + geom_tile(aes(fill = value), color = "white", ## ADDED BIT
                             size = size,                        ## ADDED BIT
                             data =  data.frame(x = 1, y = 1,    ## ADDED BIT
                             value = part_names))                ## ADDED BIT
        gg <- gg + geom_tile(aes(fill = value), color = "white", 
                             size = size)
        gg <- gg + scale_fill_manual(name = "", values = colors, 
                                     label = part_names, na.value = "white", drop = !keep)
        gg <- gg + guides(fill = guide_legend(override.aes = list(colour = "#00000000")))
        gg <- gg + theme(legend.background = element_rect(fill = "#00000000", 
                                                          color = "#00000000"))
        gg <- gg + theme(legend.key = element_rect(fill = "#00000000", 
                                                   color = "#00000000"))
      }
      else {
        if (extrafont::choose_font(glyph_font, quiet = TRUE) == 
            "") {
          stop(sprintf("Font [%s] not found. Please install it and use extrafont to make it available to R", 
                       glyph_font), call. = FALSE)
        }
        load_fontawesome()
        gg <- gg + geom_tile(color = "#00000000", fill = "#00000000", 
                             size = size, alpha = 0, show.legend = FALSE)
        gg <- gg + geom_point(aes(color = value), fill = "#00000000", 
                              size = 0, show.legend = TRUE)
        gg <- gg + geom_text(aes(color = value, label = fontlab), 
                             family = glyph_font_family, size = glyph_size, show.legend = FALSE)
        gg <- gg + scale_color_manual(name = NULL, values = colors, 
                                      labels = part_names, drop = !keep)
        gg <- gg + guides(color = guide_legend(override.aes = list(shape = 15, 
                                                                   size = 7)))
        gg <- gg + theme(legend.background = element_rect(fill = "#00000000", 
                                                          color = "#00000000"))
        gg <- gg + theme(legend.key = element_rect(color = "#00000000"))
      }
      gg <- gg + labs(x = xlab, y = NULL, title = title)
      gg <- gg + scale_x_continuous(expand = c(0, 0))
      gg <- gg + scale_y_continuous(expand = c(0, 0))
      if (equal) 
        gg <- gg + coord_equal()
      gg <- gg + theme(panel.grid = element_blank())
      gg <- gg + theme(panel.border = element_blank())
      gg <- gg + theme(panel.background = element_blank())
      gg <- gg + theme(panel.spacing = unit(0, "null"))
      gg <- gg + theme(axis.text = element_blank())
      gg <- gg + theme(axis.title.x = element_text(size = 10))
      gg <- gg + theme(axis.ticks = element_blank())
      gg <- gg + theme(axis.line = element_blank())
      gg <- gg + theme(axis.ticks.length = unit(0, "null"))
      gg <- gg + theme(plot.title = element_text(size = 18))
      gg <- gg + theme(plot.background = element_blank())
      gg <- gg + theme(panel.spacing = unit(c(0, 0, 0, 0), "null"))
      gg <- gg + theme(legend.position = legend_pos)
      gg
    }