rggplot2ggiraphhtmltoolsggrough

How to precisely overlay two plots


As a starting point, I use the very helpful code from the answer by Kat to this question (How to add less roughness to the borders of a map than to the fill of the map) to create two graphs, with the intent that one of the graphs be laid on top of the other.

library(magrittr)
library(ggplot2)
#devtools::install_github("xvrdm/ggrough")
library(ggrough)
library(sf)
library(htmltools)    
library(ggiraph)      

trace(ggrough:::parse_rough, edit=TRUE)
#In the popup window, paste this so that parse_rough will use parse_sf for GeomSf.
function (svg, geom) 
{
  rough_els <- list()
  if (geom %in% c("GeomCol", "GeomBar", "GeomTile", 
                  "Background")) {
    rough_els <- append(rough_els, parse_rects(svg))
  }
  if (geom %in% c("GeomArea", "GeomViolin", "GeomSmooth", 
                  "Background")) {
    rough_els <- append(rough_els, parse_areas(svg))
  }
  if (geom %in% c("GeomPoint", "GeomJitter", "GeomDotPlot", 
                  "Background")) {
    rough_els <- append(rough_els, parse_circles(svg))
  }
  if (geom %in% c("GeomLine", "GeomSmooth", "Background")) {
    rough_els <- append(rough_els, parse_lines(svg))
  }
  if (geom %in% c("Background")) {
    rough_els <- append(rough_els, parse_texts(svg))
  }
  if (geom %in% c("GeomSf")) {
    rough_els <- append(rough_els, parse_sf(svg))
  }
  purrr::map(rough_els, ~purrr::list_modify(.x, geom = geom))
}

# Create the function parse_sf.
parse_sf <- function (svg) {
  shape <- "path"
  keys <- NULL
  ggrough:::parse_shape(svg, shape, keys) %>% {
    purrr::map(., 
               ~purrr::list_modify(.x, 
                                   points = .x$d, 
                                   shape = "path"
               ))
  }
}
nc <- sf::st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE)

b <- ggplot(nc) + geom_sf(color = "black") + theme_minimal() +
  theme(panel.grid = element_line(color = NA),  # not resized or removed! (keep spacing)
        axis.text = element_text(color = NA))

options <- list(GeomSf = list(fill_style = "hachure", angle = 60, angle_noise = 1,
                              gap_noise = 0, gap = 6, fill_weight = 2, bowing = 5,
                              roughness = 30))

(xx <- get_rough_chart(b, options))  # from your question
fixer <- function(ggr) {          # where ggr is the ggrough graph
  nd <- lapply(1:length(ggr$x$data), function(j) {
    if(!is.null(ggr$x$data[[j]]$lengthAdjust)) { # if a text element (axis label)
      ggr$x$data[[j]]$content <- ""              # remove text, but keep spacing
      ggr$x$data[[j]]                            # return modified data element
    } else {
      ggr$x$data[[j]]                            # not text, return orig data
    }
  })
  ggr$x$data <- nd                               # add mod data to graph
  ggr                                            # return mod graph
}
xx2 <- xx %>% fixer()  # modify the plot, to hide text

(g2 <- ggplot(nc) +
    geom_sf(fill = "transparent", color = "black", linewidth = 2) +
    theme_minimal() +
    theme(plot.background = element_rect(fill = NA, color = "transparent"), # no white background
          panel.background = element_rect(fill = NA, color = "transparent"),
          text = element_text(size = 9)))      # text size to match defaults in ggrough

gg <- girafe(ggobj = g2, width_svg = 7, height_svg = 5)  # h/w default w/ ggrough

browsable(div( # parent div, size matches ggrough's default
  style = css(width = "960px", height = "500px", position = "relative"),
  div(xx2, style = css(display = "block")),                           # ggrough graph
  div(gg, style = css(position = "absolute", top = 0, padding.top = "54.2px", # layer behind
                      width = "610px", height = "500px", z.index = -2))
              )) # size and padding found by trial and error with defaults for graph sizes

The graph in the answer appears to be overlaid properly. However, when I run the same code (in RStudio), the overlay I get is incorrect:

enter image description here

I have also tried it on RStudio on a different computer, where I also get imperfect overlay, but to a different extent.

I have two questions:

  1. How can I overlay the graphs properly, without having to resort to trial and error?
  2. How can I tweak the code so that the current background is switched to the foreground? In other words, how can I make the black borders be on top of the gray scribbles instead of vice versa? I tried swapping the order of the two divs near the end, but this didn't work.

Solution

  • I could find no clear information as to how ggrough translates to literal sizes.

    In this answer I've used an aspect ratio of width x height of 7 x 5 (inches) which is pretty standard (browser, knitr, and the like). I did not test to see if these formulas would hold up under different aspect ratios.

    Because I couldn't nail down what looks like throwing mud at a ceiling fan for ggrough, I built hundreds of graphs, aligned them, then determined if there was a forumla I could create to accurately identify the right dimensions for the ggplot object.

    When I refer to the ggplot I mean the border layer that sits on top. When I refer to the ggrough I mean the fill layer that is on the bottom.

    The following metrics change depending on the height and width assigned to the ggrough.

    Because these elements are scattered throughout different functions, I created a UDF that relies on 3 inputs: ggrough's width & height & the data to plot.

    The value you assign for width & height...I believe these represent inches, but I wouldn't assume that 1 = 1 inch. There is nothing in their documentation that identifies what these values represent, but 1 equates to about 72 pixels, which is accurate if 1 represents an inch.

    There are 2 error catching calls written into the function. 1) If the height exceeds the width: which only creates more useless white space above the ggrough. 2) If the values given for height/width exceed the browser window size. (If it does the ggrough plot loses its aspect ratio and hides part of the plot - nothing good there!) If you trigger either you'll see a message in the console letting you know what happened.

    I left a message I was using for validation in the code, because you may find it to be useful information. It just spits out all the calculated metrics into the console when processing the data.

    aligner <- function(grw, grh, nc) {   # set rough chart width/height (8, 5, for example), data used in plot
      if(isTRUE(grh > grw)) {
        return(cat("\033[0;37;101mThe height should not exceed the width for this plot.\033[0m\n"))
      }
             # hide the text in the ggrough object 
      fixer <- function(ggr) {                         # where ggr is the ggrough graph
        nd <- lapply(1:length(ggr$x$data), function(j) {
          if(!is.null(ggr$x$data[[j]]$lengthAdjust)) { # if a text element (axis label)
            ggr$x$data[[j]]$content <- ""              # remove text, but keep spacing
            ggr$x$data[[j]]                            # return modified data element
          } else {
            ggr$x$data[[j]]                            # not text, return orig data
          }
        })
        ggr$x$data <- nd                               # add mod data to graph
        ggr                                            # return mod graph
      }
      tpad <- function(grw, grh) {   # calculate the top padding, given ggrough width, height
        delta <- grh - grw + 1
        widD <<- 8 - grw
        widH <- 7 - grh
        45.5 - widD * 10.0 + delta * 35.5 + .5
      }
      #---------- set the variables -----------
      pltW = 7; pltH = 5            # setting aspect ratio; if this changes nothing else may work correctly!
      browsW = 960                  # browser width (default is 960) for htmlwidgets/ used for error checking
      browsH = 500                  # browser height (default is 500) for htmlwidgets/ used for error checking
      
      sfs <- list(-0.325, 6.1)      # slope formula metrics for calculating font size of ggplot object
      sdw <- list(72.5, -3)         # slope formula metrics for calculating width of div
      
      #---------- calculate metrics -----------
      fs <- sfs[[1]] * grw + sfs[[2]]        # calculate the approriate font size for ggplot object
      dw <- sdw[[1]] * grw + sdw[[2]]        # calculate the approriate div width for ggplot object
      
      if(any(pltH/pltW * dw > browsH)) {     # validate aspect ratio by width fits in browser window
        return(cat("\033[0;37;101mWidth of", grw, "is too high to fit. Reduce width & try again.\033[0m\n"))
      } 
      
      tp <- tpad(grw, grh)                   # calculate top padding 
      if(isTRUE(tp < 0)) {                   # validate aspect ratio fits in browser window
        return(cat("\033[0;37;101mWith the given height and width, the plot doesn't fit. Try increasing the height.\033[0m\n"))
      } 
      
      lp <- ifelse(isTRUE(widD > 0), 1 - .5 * widD, 1)  # calculate the left padding
      
      #------------ create graphs ------------
      b <- ggplot(nc) + geom_sf(color = "black") + theme_minimal() +
        theme(panel.grid = element_line(color = NA),  # not resized or removed! (keep spacing)
              axis.text = element_text(color = NA))
      
      options <- list(GeomSf = list(fill_style = "hachure", angle = 60, angle_noise = 1,
                                    gap_noise = 0, gap = 6, fill_weight = 2, bowing = 5,
                                    roughness = 30))
      xx <- get_rough_chart(b, options, width = grw, height = grh) %>% fixer() # create gg rough graph
      
      g2 <- ggplot(nc) +
        geom_sf(fill = "transparent", color = "black", linewidth = 2) +
        theme_minimal() +
        theme(plot.background = element_rect(fill = NA, color = "transparent"), # no white background
              panel.background = element_rect(fill = NA, color = "transparent"),
              text = element_text(size = rel(fs)))      # text size to match defaults in ggrough
      
      gg <- girafe(ggobj = g2, width_svg = pltW, height_svg = pltH)  # great ggplot graph HTML
      
      #-------- notify user of calcs ---------
      message(paste0("Entered size: ", grw, ", ", grh, "; calculated dims are: ", 
                     "div width ", dw, "; font size ", fs, "; top padding ", tp, 
                     " and left padding ", lp))
      
      #-------- create graph overlay ---------
      browsable(div(                                # parent div, size matches ggrough's default
        style = css(width = "960px", height = paste0(browsH, "pt"), position = "relative"),
        div(xx, style = css(display = "block", padding.left = paste0(lp, "px"))), # ggrough graph
        div(gg, style = css(position = "absolute", top = 0,                       # ggplot graph
                            padding.top = paste0(tp, "px"), 
                            width = paste0(dw, "px"), z.index = -2))
      ))
    }
    
    aligner(7, 7, nc)
    

    at 7, 7

    While SO naturally maxes the image size to fit, you can at least see the variation in the font sizes.

    Or at 5, 5:

    at 5, 5

    Or at 9, 7:

    9, 7

    This is an example of the output dims and an error message that you may see.

    error msg