firmo23
firmo23

Reputation: 8454

Color specific datatable cells based on their factor values

I have the dataframe below:

product<-c("ab","ab","ab","ac","ac","ac")
shop<-c("sad","sad","sad","sadas","fghj","xzzv")
category<-c("a","a","a","b","b","b")
tempr<-c(35,35,14,24,14,5)
value<-c(0,0,-6,8,4,0)
store<-data.frame(product,shop,category,tempr,value)

from which I create store2 with:

store2 <- matrix(NA,ncol=length(unique(store$shop)),nrow=length(unique(store$product)))
colnames(store2) <- unique(store$shop)
rownames(store2) <- unique(store$product)

for(i in 1:ncol(store)) {
  store2[store[i,'product'],store[i,'shop']] <- paste0(store[i,c('tempr')],'(',store[i,'value'],')')
}

I would like to create a datatable with the DT package colored according to the values of this new dataframe. More specifically if the number inside the parenthesis is positive then the cell should be colored green. In any other case (negative, 0 or NA its should be colored red. This is an example : enter image description here

Upvotes: 3

Views: 1619

Answers (2)

Jamie
Jamie

Reputation: 6124

I don't necessarily recommend this approach because I am fairly against embedding one language in another, but I think it solves your ask. You can probably save the JavaScript to a file and load that into a variable, and that would be a better approach I think, but, in the interest of a self-contained solution, I have inlined it.

Based on the DT documentation here, https://rstudio.github.io/DT/functions.html, it is clear that the desired approach is to use formatStyle in some way.

DT provides a few convenience methods to be used with formatStyle, whose source is here: https://github.com/rstudio/DT/blob/0b9710f5a9391c634a3865961083740f1cbf657b/R/format.R, which I have based my solution on.

Basically, we need to pass some JavaScript to formatStyle which will do all of the table styling based on a variable called value. We will need to do something like this:

datatable(store2) %>% formatStyle(colnames(store2), backgroundColor=JS(jsFunc))

Where the variable jsFunc is some JavaScript string. Since the source indicates this string must be an expression rather than a statement, and because this will be somewhat complicated, we will use an anonymous function which is immediately evaluated to perform the logic. This function must take a value and return a color based on that value. Here is the function we need.

function(value){
  // find a number preceeded by an open parenthesis with an optional minus sign
  var matches = /\((-?\d+)/.exec(value);
  // ignore values which do not match our pattern, returning white as the background color
  if(!matches || matches.length < 2) { 
    return 'white'; 
  }
  // attempt to convert the match we found into a number
  var int = parseInt(matches[1]); 
  // if we can't ignore it and return a white color
  if(isNaN(int)) { 
    return 'white';
  } 
  // if the value is negative, return red
  if(int < 0) { 
    return 'red' 
  }
  // otherwise, by default, return green
  return 'green';
}

We want to call this function immediately, so we wrap it in parenthesis, and pass the value argument to it.

(function(value){
  // find a number preceeded by an open parenthesis with an optional minus sign
  var matches = /\((-?\d+)/.exec(value);
  // ignore values which do not match our pattern, returning white as the background color
  if(!matches || matches.length < 2) { 
    return 'white'; 
  }
  // attempt to convert the match we found into a number
  var int = parseInt(matches[1]); 
  // if we can't ignore it and return a white color
  if(isNaN(int)) { 
    return 'white';
  } 
  // if the value is negative, return red
  if(int < 0) { 
    return 'red';
  }
  // otherwise, by default, return green
  return 'green';
})(value)

We wrap this value in a multi-line R string, escape any backslashes and double-quotes (I avoided using them), and assign it to the value jsFunc.

jsFunc <- "(function(value){
  // find a number preceeded by an open parenthesis with an optional minus sign
  var matches = /\\((-?\\d+)/.exec(value);
  // ignore values which do not match our pattern, returning white as the background color
  if(!matches || matches.length < 2) { 
    return 'white'; 
  }
  // attempt to convert the match we found into a number
  var int = parseInt(matches[1]); 
  // if we can't ignore it and return a white color
  if(isNaN(int)) { 
    return 'white';
  } 
  // if the value is negative, return red
  if(int < 0) { 
    return 'red' 
  }
  // otherwise, by default, return green
  return 'green';
})(value)"

Finally, we can call formatStyle using this variable

datatable(store2) %>% formatStyle(colnames(store2), backgroundColor=JS(jsFunc))

That should give us a result like:

Colored DataTable

Upvotes: 3

denis
denis

Reputation: 5673

I have an answer using data.table and DT, its a bit tricky. Here it is:

library(data.table)
library(DT)

store <- setDT(store)
store[,plouf := paste0(tempr,"(",value,")")]
store[,color := ifelse(value > 0,1,0)]

table1 <- dcast(store[,.SD[1],.SDcols = c("product","shop"),by = plouf],product ~ shop,value.var = "plouf")
table2 <- dcast(store[,.SD[1],.SDcols = c("product","shop","color"),by = plouf],product ~ shop,value.var = "color")
table2[,names(table2)[-1] :=  lapply(.SD,function(x){ifelse(is.na(x),0,x)}),.SDcols = names(table2)[-1] ]
setnames(table2,paste0(names(table1),"_col"))

plouf <- cbind(table1,table2[,-1])

datatable(plouf) %>% 
  formatStyle(names(table1)[-1],paste0(names(table1)[-1],"_col"), backgroundColor = styleEqual(c(0, 1), c('red', 'green')))

The explanation: first, i redo your store2 table using dacst (passing to large format), which is more convenient, and that i ll reuse to define the colors:

store <- setDT(store)
store[,plouf := paste0(tempr,"(",value,")")]
table1 <- dcast(store[,.SD[1],.SDcols = c("product","shop"),by = plouf],product ~ shop,value.var = "plouf")

   product  fghj    sad sadas xzzv
1:      ab    NA  35(0)    NA   NA
2:      ac 14(4) 14(-6) 24(8) 5(0)

Here store[,.SD[1],.SDcols = c("product","shop"),by = plouf] allow to have only one line per data you want, to avoid replica

Then I do the same, but with a variable that gives the color (1 for green, 0 for red):

store[,color := ifelse(value > 0,1,0)]
table2 <- dcast(store[,.SD[1],.SDcols = c("product","shop","color"),by = plouf],product ~ shop,value.var = "color")

   product fghj sad sadas xzzv
1:      ab   NA   0    NA   NA
2:      ac    1   0     1    0

I tranform Nas into 0 in all column exept the first one:

table2[,names(table2)[-1] :=  lapply(.SD,function(x){ifelse(is.na(x),0,x)}),.SDcols = names(table2)[-1] ]

   product fghj sad sadas xzzv
1:      ab    0   0     0    0
2:      ac    1   0     1    0

and modify the names of the second table (giving the colors):

setnames(table2,paste0(names(table1),"_col"))

you then bind the two

plouf <- cbind(table1,table2[,-1])

and use DT where you specify different column : one for the target, one for the background color

datatable(plouf) %>% 
  formatStyle(names(table1)[-1],paste0(names(table1)[-1],"_col"), backgroundColor = styleEqual(c(0, 1), c('red', 'green')))

enter image description here

I had to change your data, because there wasn't an unique set for each shop. Here are the data:

product<-c("ab","ab","ac","ac","ac","ac")
shop<-c("sad","sad","sad","sadas","fghj","xzzv")
category<-c("a","a","a","b","b","b")
tempr<-c(35,35,14,24,14,5)
value<-c(0,0,-6,8,4,0)
store<-data.frame(product,shop,category,tempr,value)

Upvotes: 2

Related Questions