2025-04-24 12:53:37 +02:00
#' @title Create new column
#'
#' @description
#' This module allow to enter an expression to create a new column in a `data.frame`.
#'
#'
#' @param id Module's ID.
#'
#' @return A [shiny::reactive()] function returning the data.
#'
#' @note User can only use a subset of function: `r paste(list_allowed_operations(), collapse=", ")`.
#' You can add more operations using the `allowed_operations` argument, for example if you want to allow to use package lubridate, you can do:
#' ```r
#' c(list_allowed_operations(), getNamespaceExports("lubridate"))
#' ```
#'
#' @export
#'
#' @importFrom htmltools tagList tags css
#'
#' @name create-column
#'
2025-04-28 08:31:23 +02:00
#' @example examples/create_column_module_demo.R
2025-04-24 12:53:37 +02:00
create_column_ui <- function ( id ) {
ns <- NS ( id )
2025-05-10 13:02:04 +02:00
htmltools :: tagList (
2025-04-24 12:53:37 +02:00
# datamods:::html_dependency_datamods(),
# html_dependency_FreesearchR(),
2025-05-10 13:02:04 +02:00
shiny :: tags $ head (
shiny :: tags $ link ( rel = " stylesheet" , type = " text/css" , href = " FreesearchR/inst/assets/css/FreesearchR.css" )
2025-04-24 12:53:37 +02:00
) ,
# tags$head(
# # Note the wrapping of the string in HTML()
# tags$style(HTML("
# /* modified from esquisse for data types */
# .btn-column-categorical {
# background-color: #EF562D;
# color: #FFFFFF;
# }
# .btn-column-continuous {
# background-color: #0C4C8A;
# color: #FFFFFF;
# }
# .btn-column-dichotomous {
# background-color: #97D5E0;
# color: #FFFFFF;
# }
# .btn-column-datetime {
# background-color: #97D5E0;
# color: #FFFFFF;
# }
# .btn-column-id {
# background-color: #848484;
# color: #FFFFFF;
# }
# .btn-column-text {
# background-color: #2E2E2E;
# color: #FFFFFF;
# }"))
# ),
fluidRow (
column (
width = 6 ,
textInput (
inputId = ns ( " new_column" ) ,
2025-09-10 12:00:03 +02:00
label = i18n $ t ( " New column name:" ) ,
2025-04-24 12:53:37 +02:00
value = " new_column1" ,
width = " 100%"
)
) ,
column (
width = 6 ,
shinyWidgets :: virtualSelectInput (
inputId = ns ( " group_by" ) ,
2025-09-10 12:00:03 +02:00
label = i18n $ t ( " Group calculation by:" ) ,
2025-04-24 12:53:37 +02:00
choices = NULL ,
multiple = TRUE ,
disableSelectAll = TRUE ,
hasOptionDescription = TRUE ,
width = " 100%"
)
)
) ,
2025-05-10 13:02:04 +02:00
shiny :: textAreaInput (
2025-04-24 12:53:37 +02:00
inputId = ns ( " expression" ) ,
2025-09-10 12:00:03 +02:00
label = i18n $ t ( " Enter an expression to define new column:" ) ,
2025-04-24 12:53:37 +02:00
value = " " ,
width = " 100%" ,
rows = 6
) ,
tags $ i (
class = " d-block" ,
phosphoricons :: ph ( " info" ) ,
2025-09-10 12:00:03 +02:00
i18n $ t ( " Click on a column name to add it to the expression:" )
2025-04-24 12:53:37 +02:00
) ,
uiOutput ( outputId = ns ( " columns" ) ) ,
uiOutput ( outputId = ns ( " feedback" ) ) ,
tags $ div (
2025-05-16 16:23:39 +02:00
style = htmltools :: css (
2025-04-24 12:53:37 +02:00
display = " grid" ,
gridTemplateColumns = " 3fr 1fr" ,
columnGap = " 10px" ,
margin = " 10px 0"
) ,
actionButton (
inputId = ns ( " compute" ) ,
label = tagList (
2025-10-08 11:13:30 +02:00
phosphoricons :: ph ( " pencil" ) , i18n $ t ( " Create column" )
2025-04-24 12:53:37 +02:00
) ,
class = " btn-outline-primary" ,
width = " 100%"
) ,
actionButton (
inputId = ns ( " remove" ) ,
label = tagList (
2025-10-08 11:13:30 +02:00
phosphoricons :: ph ( " x-circle" ) ,
i18n $ t ( " Cancel" )
2025-04-24 12:53:37 +02:00
) ,
class = " btn-outline-danger" ,
width = " 100%"
)
)
)
}
#' @param data_r A [shiny::reactive()] function returning a `data.frame`.
#' @param allowed_operations A `list` of allowed operations, see below for details.
#'
#' @export
#'
#' @rdname create-column
#'
create_column_server <- function ( id ,
data_r = reactive ( NULL ) ,
allowed_operations = list_allowed_operations ( ) ) {
moduleServer (
id ,
function ( input , output , session ) {
ns <- session $ ns
info_alert <- shinyWidgets :: alert (
status = " info" ,
phosphoricons :: ph ( " question" ) ,
2025-10-08 11:13:30 +02:00
i18n $ t ( " Choose a name for the column to be created or modified, then enter an expression before clicking on the button below to create the variable, or cancel to exit without saving anything." )
2025-04-24 12:53:37 +02:00
)
rv <- reactiveValues (
data = NULL ,
feedback = info_alert
)
observeEvent ( input $ hidden , rv $ feedback <- info_alert )
bindEvent ( observe ( {
data <- data_r ( )
shinyWidgets :: updateVirtualSelect (
inputId = " group_by" ,
choices = make_choices_with_infos ( data )
)
} ) , data_r ( ) , input $ hidden )
observeEvent ( data_r ( ) , rv $ data <- data_r ( ) )
output $ feedback <- renderUI ( rv $ feedback )
output $ columns <- renderUI ( {
data <- req ( rv $ data )
mapply (
label = names ( data ) ,
data = data ,
FUN = btn_column ,
MoreArgs = list ( inputId = ns ( " add_column" ) ) ,
SIMPLIFY = FALSE
)
} )
observeEvent ( input $ add_column , {
updateTextAreaInput (
session = session ,
inputId = " expression" ,
value = paste0 ( input $ expression , input $ add_column )
)
} )
observeEvent ( input $ new_column , {
if ( input $ new_column == " " ) {
rv $ feedback <- shinyWidgets :: alert (
status = " warning" ,
2025-09-10 12:00:03 +02:00
phosphoricons :: ph ( " warning" ) , i18n $ t ( " New column name cannot be empty" )
2025-04-24 12:53:37 +02:00
)
}
} )
observeEvent ( input $ remove , {
rv $ data [ [input $ new_column ] ] <- NULL
} )
observeEvent ( input $ compute , {
rv $ feedback <- try_compute_column (
expression = input $ expression ,
name = input $ new_column ,
rv = rv ,
allowed_operations = allowed_operations ,
by = input $ group_by
)
} )
return ( reactive ( rv $ data ) )
}
)
}
#' @export
#'
#' @rdname create-column
# @importFrom methods getGroupMembers
list_allowed_operations <- function ( ) {
c (
" (" , " c" ,
# getGroupMembers("Arith"),
c ( " +" , " -" , " *" , " ^" , " %%" , " %/%" , " /" ) ,
# getGroupMembers("Compare"),
c ( " ==" , " >" , " <" , " !=" , " <=" , " >=" ) ,
# getGroupMembers("Logic"),
c ( " &" , " |" ) ,
# getGroupMembers("Math"),
c (
" abs" , " sign" , " sqrt" , " ceiling" , " floor" , " trunc" , " cummax" ,
" cummin" , " cumprod" , " cumsum" , " exp" , " expm1" , " log" , " log10" ,
" log2" , " log1p" , " cos" , " cosh" , " sin" , " sinh" , " tan" , " tanh" ,
" acos" , " acosh" , " asin" , " asinh" , " atan" , " atanh" , " cospi" , " sinpi" ,
" tanpi" , " gamma" , " lgamma" , " digamma" , " trigamma"
) ,
# getGroupMembers("Math2"),
c ( " round" , " signif" ) ,
# getGroupMembers("Summary"),
c ( " max" , " min" , " range" , " prod" , " sum" , " any" , " all" ) ,
" pmin" , " pmax" , " mean" ,
" paste" , " paste0" , " substr" , " nchar" , " trimws" ,
" gsub" , " sub" , " grepl" , " ifelse" , " length" ,
" as.numeric" , " as.character" , " as.integer" , " as.Date" , " as.POSIXct" ,
" as.factor" , " factor"
)
}
2025-10-08 11:13:30 +02:00
2025-04-24 12:53:37 +02:00
#' @inheritParams shiny::modalDialog
#' @export
#'
#' @importFrom shiny showModal modalDialog textInput
#' @importFrom htmltools tagList
#'
#' @rdname create-column
modal_create_column <- function ( id ,
2025-09-10 12:00:03 +02:00
title = i18n $ t ( " Create a new column" ) ,
2025-04-24 12:53:37 +02:00
easyClose = TRUE ,
size = " l" ,
footer = NULL ) {
ns <- NS ( id )
showModal ( modalDialog (
title = tagList ( title , datamods ::: button_close_modal ( ) ) ,
create_column_ui ( id ) ,
tags $ div (
style = " display: none;" ,
textInput ( inputId = ns ( " hidden" ) , label = NULL , value = datamods ::: genId ( ) )
) ,
easyClose = easyClose ,
size = size ,
footer = footer
) )
}
#' @inheritParams shinyWidgets::WinBox
#' @export
#'
#' @importFrom shinyWidgets WinBox wbOptions wbControls
#' @importFrom htmltools tagList
#' @rdname create-column
winbox_create_column <- function ( id ,
2025-09-10 12:00:03 +02:00
title = i18n $ t ( " Create a new column" ) ,
2025-04-24 12:53:37 +02:00
options = shinyWidgets :: wbOptions ( ) ,
controls = shinyWidgets :: wbControls ( ) ) {
ns <- NS ( id )
WinBox (
title = title ,
ui = tagList (
create_column_ui ( id ) ,
tags $ div (
style = " display: none;" ,
textInput ( inputId = ns ( " hidden" ) , label = NULL , value = datamods ::: genId ( ) )
)
) ,
options = modifyList (
shinyWidgets :: wbOptions ( height = " 550px" , modal = TRUE ) ,
options
) ,
controls = controls ,
auto_height = FALSE
)
}
try_compute_column <- function ( expression ,
name ,
rv ,
allowed_operations ,
by = NULL ) {
parsed <- try ( parse ( text = expression , keep.source = FALSE ) , silent = TRUE )
if ( inherits ( parsed , " try-error" ) ) {
return ( datamods ::: alert_error ( attr ( parsed , " condition" ) $ message ) )
}
funs <- unlist ( c ( extract_calls ( parsed ) , lapply ( parsed , extract_calls ) ) , recursive = TRUE )
if ( ! are_allowed_operations ( funs , allowed_operations ) ) {
2025-09-10 12:00:03 +02:00
return ( datamods ::: alert_error ( i18n $ t ( " Some operations are not allowed" ) ) )
2025-04-24 12:53:37 +02:00
}
if ( ! isTruthy ( by ) ) {
result <- try (
rlang :: eval_tidy ( rlang :: parse_expr ( expression ) , data = rv $ data ) ,
silent = TRUE
)
} else {
result <- try (
{
dt <- as.data.table ( rv $ data )
new_col <- NULL
dt [ , new_col : = rlang :: eval_tidy ( rlang :: parse_expr ( expression ) , data = .SD ) , by = by ]
dt $ new_col
} ,
silent = TRUE
)
}
if ( inherits ( result , " try-error" ) ) {
return ( alert_error ( attr ( result , " condition" ) $ message ) )
}
adding_col <- try ( rv $ data [ [name ] ] <- result , silent = TRUE )
if ( inherits ( adding_col , " try-error" ) ) {
return ( alert_error ( attr ( adding_col , " condition" ) $ message ) )
}
code <- if ( ! isTruthy ( by ) ) {
rlang :: call2 ( " mutate" , ! ! ! rlang :: set_names ( list ( rlang :: parse_expr ( expression ) ) , name ) )
} else {
rlang :: call2 (
" mutate" ,
! ! ! rlang :: set_names ( list ( rlang :: parse_expr ( expression ) ) , name ) ,
! ! ! list ( .by = rlang :: expr ( c ( ! ! ! rlang :: syms ( by ) ) ) )
)
}
attr ( rv $ data , " code" ) <- Reduce (
f = function ( x , y ) rlang :: expr ( ! ! x %>% ! ! y ) ,
x = c ( attr ( rv $ data , " code" ) , code )
)
shinyWidgets :: alert (
status = " success" ,
2025-09-10 12:00:03 +02:00
phosphoricons :: ph ( " check" ) , i18n $ t ( " Column added!" )
2025-04-24 12:53:37 +02:00
)
}
are_allowed_operations <- function ( x , allowed_operations ) {
all (
x %in% allowed_operations
)
}
extract_calls <- function ( exp ) {
if ( is.call ( exp ) ) {
return ( list (
as.character ( exp [ [1L ] ] ) ,
lapply ( exp [ -1L ] , extract_calls )
) )
}
}
alert_error <- function ( text ) {
alert (
status = " danger" ,
2025-05-16 15:36:08 +02:00
phosphoricons :: ph ( " bug" ) , text
2025-04-24 12:53:37 +02:00
)
}
btn_column <- function ( label , data , inputId ) {
icon <- get_var_icon ( data , " class" )
type <- data_type ( data )
tags $ button (
type = " button" ,
class = paste0 ( " btn btn-column-" , type ) ,
2025-05-16 16:23:39 +02:00
style = htmltools :: css (
2025-04-24 12:53:37 +02:00
" --bs-btn-padding-y" = " .25rem" ,
" --bs-btn-padding-x" = " .5rem" ,
" --bs-btn-font-size" = " .75rem" ,
" margin-bottom" = " 5px"
) ,
if ( ! is.null ( icon ) ) icon ,
label ,
onclick = sprintf (
" Shiny.setInputValue('%s', '%s', {priority: 'event'})" ,
inputId , label
)
)
}
make_choices_with_infos <- function ( data ) {
lapply (
X = seq_along ( data ) ,
FUN = function ( i ) {
nm <- names ( data ) [i ]
values <- data [ [nm ] ]
icon <- get_var_icon ( values , " class" )
# icon <- if (inherits(values, "character")) {
# phosphoricons::ph("text-aa")
# } else if (inherits(values, "factor")) {
# phosphoricons::ph("list-bullets")
# } else if (inherits(values, c("numeric", "integer"))) {
# phosphoricons::ph("hash")
# } else if (inherits(values, c("Date"))) {
# phosphoricons::ph("calendar")
# } else if (inherits(values, c("POSIXt"))) {
# phosphoricons::ph("clock")
# } else {
# NULL
# }
description <- if ( is.atomic ( values ) ) {
2025-09-10 12:00:03 +02:00
paste ( i18n $ t ( " Unique values:" ) , data.table :: uniqueN ( values ) )
2025-04-24 12:53:37 +02:00
} else {
" "
}
list (
label = htmltools :: doRenderTags ( tagList (
icon , nm
) ) ,
value = nm ,
description = description
)
}
)
}