Functional programming around leaflet with NSE

I'm hoping to lure somebody who knows more than i do about rlang into helping me understand it better. I have a weird workaround for programming around leaflet, but I'd like to better understand 1) why this works, 2) why the traditional inline !! doesn't work, and 3) how I can debug quosure errors on my own without coming up with weird hacks. I'm not sure whether this is an rlang question, a leaflet question, a combination of the two, or something else entirely.

Whenever I've tried to program around a leaflet object (e.g., by creating a function that takes a column in a dataframe as an argument, and maps that to some aesthetic on a leaflet map), I get the following error:

Error in is_quosure(e2) : argument "e2" is missing, with no default

Here's an example of a reprex that produces a plot successfully. I first have to define a palette based on the domain of values i want to map, and then map them onto the leaflet. I'm using the nc dataset that comes loaded with the sf package.

library(sf)
library(leaflet)
library(tidyverse)

pal <- colorFactor(palette = "plasma", domain = nc$BIR74)

nc %>% 
    leaflet(width = "100%") %>% 
    addPolygons(color = ~pal(BIR74))

When i wrap it in a function - even when i wrap the variable name in enquo - i get the is_quosure(e2) error.

plot_leaf_columns <- function(df, var) {
    
    v <- enquo(var)
    pal <- colorFactor(palette = "plasma", domain = pull(df, !!v) )
    
    df %>% 
        leaflet(width = "100%") %>% 
        addPolygons(color = ~!!v)
}
Error in is_quosure(e2) : argument "e2" is missing, with no default

This feels like it has something to do with the interaction of !! and the ~ that leaflet uses to map variables onto the graph.

HOWEVER, if i create a new "col" variable before creating the map, it works a treat. i can reproduce the graph from the first bit perfectly, and extend that to other columns in nc, if i please.

plot_leaf_columns <- function(df, var) {
    
    v <- enquo(var)
    pal <- colorFactor(palette = "plasma", domain = pull(df, !!v))
    
    df %>% 
        mutate(col = pal(!!v)) %>% 
        mutate(ugh = as.character(!!v)) %>% 
        leaflet(width = "100%") %>% 
        addPolygons(color = ~col)
}

plot_leaf_columns(nc, BIR74)

I have to do this with everything that relies on the input variable that i want to call in a leaflet function. For example, if i want to create labels, i have to first create a new character variable from the bang bang'd enquo(), and call it later. This works:

plot_leaf_columns <- function(df, var) {
    
    v <- enquo(var)
    pal <- colorFactor(palette = "plasma", domain = pull(df, !!v))
    
    df %>% 
        mutate(col = pal(!!v)) %>% 
        mutate(ugh = as.character(!!v)) %>% 
        leaflet(width = "100%") %>% 
        addPolygons(color = ~col, 
                    label = ~ugh)
}

but if i were to replace the last line with

         label = ~as.character(!!v))

it throws the same quosure error.

This is very curious to me!! I would greatly appreciate being pointed in the direction of any resources that might help me grok this behavior. it might just be a different way of calling !! that I don't know yet. If not - is this the desired behavior by rlang / leaflet ? If so, I would be happy to help create vignettes or resources to point people toward this workaround. If there is a simpler way to program around leaflet using NSE / rlang, I would love to learn about it.

Thanks very much!

1 Like

The plotly leaflet R package was created a little before rlang, and has its own non-standard evaluation (NSE) system, that as far as I can tell is mostly only documented in the examples. It's pretty temperamental, too—passing it a variable with the correct formula ~pal(BIR74) fails.

When NSE systems go sideways, the quickest way to get it to work is to rewrite all the code dynamically and then evaluate it. Here, wrap the whole plotly pipeline in quo with !! substitution wherever you like, then call quo_squash on it to collapse it to a single expression (instead of nested quosures), and then call eval_tidy on the whole lot to actually run it:

library(sf)
library(leaflet)
library(tidyverse)

nc <- st_read(system.file("shape/nc.shp", package="sf"))

plot_leaf_columns <- function(df, variable) {
    v <- enquo(variable)
    pal <- colorFactor(palette = "plasma", domain = pull(df, !!v) )
    
    rlang::eval_tidy(rlang::quo_squash(quo({
        df %>% 
            leaflet(width = "100%") %>% 
            addPolygons(color = ~pal(!!v))
    })))
}

plot_leaf_columns(nc, BIR74)
#> Warning: sf layer has inconsistent datum (+proj=longlat +datum=NAD27 +no_defs).
#> Need '+proj=longlat +datum=WGS84'

This is sort of a nuclear option, though; there's likely a simpler alternative if the leaflet NSE system is documented somewhere (beyond this). Really the leaflet interface should get updated to use rlang, but it'd be hard to do without breaking a lot of existing code.

5 Likes

Thanks, @alistaire, definitely never would have figured that out on my own! (no sarcasm)

FYI, @brooke, I'm gonna add the plotly tag to this to ⇧ the chances of someone with more plotly expertise seeing it.

Thanks both! I did not know about quo_squash - that's really useful. Nor did I know that leaflet is built on plotly - I've run down similar trial-and-error rabbit holes with both packages.

The Rstudio leaflet docs are really useful for creating maps, but agreed - if there is more somewhere about the actual pipes that underly it + plotly, I'd love to read up.

Thank you!

1 Like

I know Carson Sievert has been working on a plotly book— not sure if the relevant material is in they're yet (:flushed: I'm not much of a plotly user), but…

I'm not sure why @alistaire brought up plotly in their answer -- also, leaflet does not build on plotly.

Nevertheless, @alistaire is right that leaflet/plotly's NSE model pre-dates rlang. And, as far as I know, that NSE model is essentially the same (use ~ to denote a reference to a column in the dataset to be visualized). In other words, the general approach in the function that @alistaire wrote would also work for plotly:

library(sf)
library(plotly)
library(tidyverse)

nc <- st_read(system.file("shape/nc.shp", package="sf"))

plot_leaf_columns <- function(df, variable) {
  v <- enquo(variable)
  pal <- leaflet::colorFactor(palette = "plasma", domain = pull(df, !!v) )
  
  rlang::eval_tidy(rlang::quo_squash(quo({
    plot_ly(df, color = ~pal(!!v), split = ~NAME)
  })))
}

plot_ly_columns(nc, BIR74)
3 Likes

Oops, sorry, yep, that was a typo. Thought-po? But yes, the NSE systems are similar. The ~ quoting thing was on the edge of becoming a common idiom before rlang. Really, that's what quosures are underneath, but with a complete framework built around the class.

3 Likes

This topic was automatically closed 21 days after the last reply. New replies are no longer allowed.

If you have a query related to it or one of the replies, start a new topic and refer back with a link.