Amit Kohli
Amit Kohli

Reputation: 2950

How can I pass character strings as independent parameters after a `+`?

Before you mark as dup, I know about Use character string as function argument, but my use case is slightly different. I don't need to pass a parameter INSIDE the function, I would like to pass a dynamic number of parameters after a + (think ggplot2).

(Note: Please don't format and remove the extra-looking ####, I have left them in so people can copy paste the code into R for simplicity).

This has been my process:

#### So let's reproduce this example:

library(condformat)
condformat(iris[c(1:5,70:75, 120:125),]) +
   rule_fill_discrete(Species) +
   rule_fill_discrete(Petal.Width) 

enter image description here

#### I would like to be able to pass the two rule_fill_discrete() functions dynamically (in my real use-case I have a variable number of possible inputs and it's not possible to hardcode these in).

#### First, create a function to generalize:

PlotSeries <- function(x){
   b=NULL
   for (i in 1:length(x)){
     a <- paste('rule_fill_discrete(',x[i],')',sep="")
     b <- paste(paste(b,a,sep="+")) 
     }
   b <- gsub("^\\+","",b)
   eval(parse(text = b))
 }

#### Which works with one argument

condformat(iris[c(1:5,70:75, 120:125),]) +
   PlotSeries("Species")

#### But not if we pass two arguments:

condformat(iris[c(1:5,70:75, 120:125),]) +
   PlotSeries(c("Species","Petal.Width"))

Error in rule_fill_discrete(Species) + rule_fill_discrete(Petal.Width) : non-numeric argument to binary operator

#### It will work if we call each individually

condformat(iris[c(1:5,70:75, 120:125),]) +
   PlotSeries("Species") +
   PlotSeries("Petal.Width")

#### Which gives us an indication as to what the problem is... the fact that it doesn't like when the rule_fill_discrete statements are passed in as one statement. Let's test this:

condformat(iris[c(1:5,70:75, 120:125),]) +
   eval(rule_fill_discrete(Species) +
          rule_fill_discrete(Petal.Width) )

Error in rule_fill_discrete(Species) + rule_fill_discrete(Petal.Width) : non-numeric argument to binary operator

#### Fails. But:

condformat(iris[c(1:5,70:75, 120:125),]) +
   eval(rule_fill_discrete(Species)) +
   eval(rule_fill_discrete(Petal.Width) )

#### This works. But we need to be able to pass in a GROUP of statements (that's kinda the whole point). So let's try to get the eval statements in:

Nasty <- "eval(rule_fill_discrete(Species)) eval(rule_fill_discrete(Petal.Width))"

 condformat(iris[c(1:5,70:75, 120:125),]) + Nasty                   #### FAIL

Error in +.default(condformat(iris[c(1:5, 70:75, 120:125), ]), Nasty) : non-numeric argument to binary operator

condformat(iris[c(1:5,70:75, 120:125),]) + eval(Nasty)             #### FAIL

Error in +.default(condformat(iris[c(1:5, 70:75, 120:125), ]), eval(Nasty)) : non-numeric argument to binary operator

condformat(iris[c(1:5,70:75, 120:125),]) + parse(text=Nasty)       #### FAIL

Error in +.default(condformat(iris[c(1:5, 70:75, 120:125), ]), parse(text = Nasty)) : non-numeric argument to binary operator

condformat(iris[c(1:5,70:75, 120:125),]) + eval(parse(text=Nasty)) #### FAIL

Error in eval(rule_fill_discrete(Species)) + eval(rule_fill_discrete(Petal.Width)) : non-numeric argument to binary operator

So how can we do it?

Upvotes: 1

Views: 400

Answers (3)

Gregor Thomas
Gregor Thomas

Reputation: 145805

NOTE: This answer provides a workaround for a bug in an old version of condformat. The bug has since been fixed, see @zeehio's answer for the current version after this bug was fixed.


I think you have two mostly separate questions. That are all mixed together in your post. I will attempt to restate and answer them individually, and then put things together - which doesn't work all the way at this point but gets close.

First, let's save some typing by defining a couple variables:

ir = iris[c(1:5,70:75, 120:125), ]
cf = condformat(ir) 

Q1: How do I use + on a vector or list of inputs?

This is the easy question. The base answer is Reduce. The following are all equivalent:

10 + 1 + 2 + 5 
"+"("+"("+"(10, 1), 2), 5)
Reduce("+", c(1, 2, 5), init = 10))

More pertinent to your case, we can do this to replicate your desired output:

fills = list(rule_fill_discrete(Species), rule_fill_discrete(Petal.Width))
res = Reduce(f = "+", x = fills, init = cf)
res

Q2: How do I use string inputs with rule_fill_discrete?

This is my first time using condformat, but it looks to be written in the lazyeval paradigm with rule_fill_discrete_ as a standard-evaluating counterpart to the non-standard-evaluating rule_fill_discrete. This example is even given in ?rule_fill_discrete, but it doesn't work as expected

cf + rule_fill_discrete_(columns = "Species")
# bad: Species column colored entirely red, not colored by species
# possibly a bug? At the very least misleading documentation...

cf + rule_fill_discrete_(columns = "Species", expression = expression(Species))
# bad: works as expected, but still uses an unquoted Species

# other failed attempts
cf + rule_fill_discrete_(columns = "Species", expression = expression("Species"))
cf + rule_fill_discrete_(columns = "Species", expression = "Species")
# bad: single color still single color column

There is also an env environment argument in the SE function, but I had no luck with that either. Maybe someone with more lazyeval/expression experience can point out something I'm overlooking or doing wrong.

Work-around: What we can do is pass the column directly. This works because we're not doing any fancy functions of the column, just using it's values directly to determine the coloring:

cf + rule_fill_discrete_(columns = c("Species"), expression = ir[["Species"]])
# hacky, but it works

Putting it together

Using the NSE version with Reduce is easy:

fills = list(rule_fill_discrete(Species), rule_fill_discrete(Petal.Width))
res = Reduce(f = "+", x = fills, init = cf)
res
# works!

Using SE with input strings, we can use the hacky workaround.

input = c("Species", "Petal.Width")
fills_ = lapply(input, function(x) rule_fill_discrete_(x, expression = ir[[x]]))
res_ = Reduce(f = "+", x = fills_, init = cf)
res_
# works!

And this, of course, you could wrap up into a custom function that takes a data frame and a string vector of column names as input.

Upvotes: 1

zeehio
zeehio

Reputation: 4138

Thanks to this stackoverflow question and thanks to the bug report from @amit-kohli, I was made aware that there was a bug in the condformat package.

Update: Answer updated to reflect the new condformat API introduced in condformat 0.7.

Here I show how to (using condformat 0.7.0). Note that the syntax I use in the standard evaluation function is derived from the rlang package.

Install condformat:

install.packages("condformat)"

A simple example, asked in the question:

# Reproduce the example
library(condformat)
condformat(iris[c(1:5,70:75, 120:125),]) %>%
   rule_fill_discrete(Species) %>%
   rule_fill_discrete(Petal.Width) 

# With variables:
col1 <- rlang::quo(Species)
col2 <- rlang::quo(Petal.Width)
condformat(iris[c(1:5,70:75, 120:125),]) %>%
  rule_fill_discrete(!! col1) %>%
  rule_fill_discrete(!! col2)

# Or even with character strings to give the column names:
col1 <- "Species"
col2 <- "Petal.Width"

condformat(iris[c(1:5,70:75, 120:125),]) %>%
  rule_fill_discrete(!! col1) %>%
  rule_fill_discrete(!! col2) 

# Do it programmatically (In a function)
#' @importFrom magrittr %>%
some_color <- function(data, col1, col2) {
  condformat::condformat(data) %>%
    condformat::rule_fill_discrete(!! col1) %>%
    condformat::rule_fill_discrete(!! col2)
}
some_color(iris[c(1:5,70:75, 120:125),], "Species", "Petal.Width")

A more general example, using an expression:

# General example, using an expression:
condformat(iris[c(1:5,70:75, 120:125),]) %>% 
  rule_fill_gradient(Species, expression = Sepal.Width - Sepal.Length)

# General example, using a column given as character and an
# expression given as character as well:
expr <- rlang::parse_expr("Sepal.Width - Sepal.Length")
condformat(iris[c(1:5,70:75, 120:125),]) %>%
  rule_fill_gradient("Species", expression = !! expr)


# General example, in a function, everything given as a character:
two_column_difference <- function(data, col_to_colour, col1, col2)  {
  expr1 <- rlang::parse_expr(col1)
  expr2 <- rlang::parse_expr(col2)
  condformat::condformat(data) %>%
    condformat::rule_fill_gradient(
      !! col_to_colour,
      expression = (!!expr1) - (!!expr2))
}
two_column_difference(iris[c(1:5,70:75, 120:125),],
                      col_to_colour = "Species",
                      col1 = "Sepal.Width",
                      col2 = "Sepal.Length")

Custom discretized scales for continuous values

Custom discrete color values can be specified with a function that preprocesses a continuous column into a discrete scale:

discretize <- function(column) {
  sapply(column,
    FUN = function(value) {
      if (value < 4.7) {
        return("low")
      } else if (value < 5.0) {
        return("mid")
      } else {
        return("high")
      }
    })
}

And we can specify the colors for each of the levels of the scale using colours =:

condformat(head(iris)) %>%
  rule_fill_discrete(
    "Sepal.Length",
    expression = discretize(Sepal.Length),
    colours = c("low" = "red", "mid" = "yellow", "high" = "green"))

If we want, the discretize function can return colours:

discretize_colours <- function(column) {
  sapply(column,
    FUN = function(value) {
      if (value < 4.7) {
        return("red")
      } else if (value < 5.0) {
        return("yellow")
      } else {
        return("green")
      }
    })
}

The code to use it:

condformat(head(iris)) %>%
  rule_fill_discrete(
    "Sepal.Length",
    expression = discretize_colours(Sepal.Length),
    colours = identity)

Note that as expression returns the colours we use colours = identity. identity is just function(x) x.

Finally, using some rlang tidy evaluation we can create a function:

colour_based_function <- function(data, col1) {
  col <- rlang::parse_expr(col1)
  condformat::condformat(data) %>%
    condformat::rule_fill_discrete(
      columns = !! col1,
      expression = discretize_colours(!! col),
      colours = identity)
}
colour_based_function(head(iris), "Sepal.Length")

Upvotes: 3

Amit Kohli
Amit Kohli

Reputation: 2950

@Gregor's answer was perfect. A bit hacky, but works excellently.

In my use-case, I needed a bit more complication, I will post it here in case it's useful to somebody else.

In my use-case, I needed to be able to color multiple columns based on the values of one column. condformat allows us to do this already, but again we run into the parametrization problem. Here's my solution to that, based on the response by Gregor:

CondFormatForInput <- function(Table,VectorToColor,VectorFromColor) {
        cf <- condformat(Table)
        input = data.frame(Val=VectorToColor,
                           Comp=VectorFromColor)
        fills2_ = map2(input$Val,.y = input$Comp,.f = function(x,y) rule_fill_discrete_(x, expression = 
                                                                                          iris[[y]]))
        res_ = Reduce(f = "+", x = fills2_, init = cf)
        res_
      }

      CondFormatForInput(iris,
                        c("Sepal.Length","Sepal.Width","Petal.Length","Petal.Width"),
                        c("Sepal.Width","Sepal.Width","Petal.Width","Petal.Width"))

Upvotes: 0

Related Questions