1

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?

Community
  • 1
  • 1
Amit Kohli
  • 2,860
  • 2
  • 24
  • 44
  • I find that question using the word "dynamic" are generally very unclear. The word has so many different possible meanings. It's better to make a simple example and say what the inputs and outputs should be. At this point we have the inputs, but then the desired outputs are either not there or so mixed in with the various error messages that I at least cannot find a proper description of the desired output. Trying to mix macro style processing with quoted `"eval"`'s seems far too convoluted. – IRTFM Dec 30 '16 at 20:40
  • Sorry 42, let me try to clarify. I'm STARTING from the desired output... I would just like to create a function that allows me to pass any number of conditional format statements to condformat – Amit Kohli Dec 30 '16 at 20:42
  • 1
    I think I figured out what you wanted to do, but after looking at the conventions used by the `condformat` package, I decided the confusing mixture of standard and nonstandard evaluation were too much for me. I'm not convinced that tagging as ggplot2 is really "honest". I don't think the `+` operator has the same evaluation mechanism. – IRTFM Dec 30 '16 at 21:20
  • Not certain, but the `purrr` package's map functions might help. – AdamKent Jan 04 '17 at 21:02
  • Yes bungo, thank you. See my elaboration on Gregor's excellent answer... it uses a purrr function to apply a slightly more complicated use case – Amit Kohli Jan 05 '17 at 16:40

3 Answers3

3

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")
zeehio
  • 4,023
  • 2
  • 34
  • 48
1

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.

Gregor Thomas
  • 136,190
  • 20
  • 167
  • 294
  • See my answer for an elaboration on this that worked for my slightly more complex use-case. – Amit Kohli Jan 03 '17 at 20:21
  • 1
    There seems to be a bug in how condformat deals with standard evaluation. I will look into it as soon as I can. rule_fill_discrete_("Species") should work – zeehio Jan 03 '17 at 23:56
0

@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"))
Amit Kohli
  • 2,860
  • 2
  • 24
  • 44