R/utils.R

Defines functions linear_model_df_output check_dir test_output_format is_hex_color plot_t_test plot_z_test make_names_append_list no_write_msg temp_file test_write_access exit_without_save exit_with_save spada_save update_waiter_text show_exit_screen show_startup_screen abort_run_local_modal abort_filter_modal remove_running_modal running_modal test_data_format is_spada_dt is_spada_df make_valid_cols stati_card obj_type test_all_equal make_var_names f_dec fun_help_modal get_help_file ttip filter_rows_2vars filter_rows convert try_convert bmsg msg_error msg mini_btn btn_task empty_plot test_dataset safe_env show_allowed_op gen_element_id gen_output register_output_events add_output_element spada_output spada_output_info_card printable_report_card report_card gen_table2 load_conf set_spada_theme spada_user_dir short_name

# ============================================================================.
# ---------------------------- OBJECTS ---------------------------------------
# ============================================================================.

# default conf ----------------------------------------------------------------
default_conf <- list(
  'theme' = 'spada_theme',
  'file_size' = 1000,
  'restore_session' = 'never',
  'save_session' = 'ask',
  'restore_data_status' = 0,
  'restore_output_status' = 0,
  'restore_status' = NULL,
  'plot_fill_color' = plot_fill_color,
  'plot_line_color' = plot_line_color,
  'plot_title_color' = plot_title_color,
  'plot_limit' = 1e5
)

# math functions --------------------------------------------------------------
math_funs <- c(
  'Mean' = 'mean_na',
  'Mean no NA' = 'mean_nona',
  'Geometric Mean' = 'Gmean',
  'Harmonic Mean' = 'Hmean',
  # 'Mode' = 'Mode',
  'Standard Deviation' = 'fsd',
  'Variance' = 'fvar',
  'Min' = 'min_na',
  'Min no NA' = 'min_nona',
  'Max' = 'max_na',
  'Max no NA' = 'max_nona',
  'First' = 'first_na',
  'First no NA' = 'first_nona',
  'Last' = 'last_na',
  'Last no NA' = 'last_nona',
  'Lag' = 'shift',
  # 'Range' = 'range',
  'IQR' = 'IQR',
  'Skewness' = 'Skew',
  'Kurtosis' = 'Kurt',
  'Sum' = 'sum_na',
  'Sum no NA' = 'sum_nona',
  'Cum Sum' = 'cumsum',
  'Prod' = 'prod',
  'Cum Prod' = 'cumprod',
  'Square Root' = 'sqrt',
  'Exponential' = 'exp',
  'Log' = 'log',
  'Log2' = 'log2',
  'Log10' = 'log10',
  'Ceiling' = 'ceiling',
  'Floor' = 'floor',
  'Trunc' = 'trunc',
  'Signif' = 'signif',
  'Round' = 'round',
  'Abs' = 'abs',
  'Sine' = 'sin',
  'Cosine' = 'cos',
  'Tangent' = 'tan',
  'Order' = 'order',
  'As Numeric' = 'as.numeric',
  'Is Numeric' = 'is.numeric',
  'As Double' = 'as.double',
  'Is Double' = 'is.double',
  'As Integer' = 'as.integer',
  'Is Integer' = 'is.integer',
  'Is Integer' = 'is.integer',
  'As Numeric' = 'as.numeric',
  'Is Numeric' = 'is.numeric'
  )

groupby_math_funs <- c(
  'Mean' = 'mean_na',
  'Mean no NA' = 'mean_nona',
  'Geometric Mean' = 'Gmean',
  'Harmonic Mean' = 'Hmean',
  'Standard Deviation' = 'sd',
  'Variance' = 'var',
  'Min' = 'min_na',
  'Min no NA' = 'min_nona',
  'Max' = 'max_na',
  'Max no NA' = 'max_nona',
  'First' = 'fina',
  'Last' = 'lana',
  'Skewness' = 'Skew',
  'Kurtosis' = 'Kurt',
  'Sum' = 'sum_na',
  'Sum no NA' = 'sum_nona',
  'Prod' = 'prod'
)

# char functions --------------------------------------------------------------
char_funs <- c(
  'To Upper' = 'toupper',
  'To Lower' = 'tolower',
  'N Char' = 'nchar',
  'As Char' = 'as.character',
  'Is Char' = 'is.character'
)

groupby_char_funs <- c(
  'First' = 'fina',
  'Last' = 'lana'
)

# date functions --------------------------------------------------------------
date_funs <- c(
  # data.table package
  'Year' = 'year',
  'Month' = 'month',
  'Month Day' = 'mday',
  'Week Day' = 'wday',
  'Year Day' = 'yday',
  'Week' = 'week',
  'Quarter' = 'quarter',
  'Hour' = 'hour',
  'Minute' = 'minute',
  'Second' = 'second',
  # spada package
  'Is Date' = 'is_date'
)

groupby_date_funs <- c(
  'Min' = 'min_na',
  'Min no NA' = 'min_nona',
  'Max' = 'max_na',
  'Max no NA' = 'max_nona',
  'First' = 'fina',
  'Last' = 'lana'
)

# factor functions ------------------------------------------------------------
factor_funs <- c(
  'Number of Levels' = 'nlevels',
  'As factor' = 'as.factor',
  'Is Factor' = 'is.factor',
  'Is Ordered' = 'is.ordered'
)

# logical functions -----------------------------------------------------------
logical_funs <- c(
  'All True' = 'all',
  'Any True' = 'any',
  'As Logical' = 'as.logical',
  'Is Logical' = 'is.logical',
  'Number of True' = 'sum_na',
  'Proportion of True' = 'mean_na'
)

groupby_logical_funs <- c(
  'Min' = 'min_na',
  'Min no NA' = 'min_nona',
  'Max' = 'max_na',
  'Max no NA' = 'max_nona',
  'First' = 'fina',
  'Last' = 'lana',
  'Sum' = 'sum_na',
  'Sum no NA' = 'sum_nona'
)

# complex functions -----------------------------------------------------------
complex_funs <- c(
  'Real Part' = 'Re',
  'Imaginary Part' = 'Im',
  'Is Complex' = 'is.complex'
)

# basic operations ------------------------------------------------------------
basic_operations <- c(
  # base package
  '+', '-', '+', '-', '/', '*', '^',
  '==', '!=', '!', '>', '>=', '<', '<=',
  '&', '|', 'xor',
  '(', '[', '<-', '$',
  '%in%', 'is.na', 'is.null', 'is.nan', 'na.omit',
  '::', ':', 'T', 'F',
  'list', 'try',
  'paste', 'paste0', 'substr',
  'isTRUE', 'isFALSE',
  'c',
  'as.Date', 'as.POSIXct', 'as.POSIXlt',
  'ifelse',
  # base math
  'mean', 'median', 'sd', 'var', 'min', 'max', 'sum',

  # data.table package
  '%notin%', 'between', '%between%', 'fifelse', 'fcase',

  # DescTools package
  'Outlier',

  #stats package
  'quantile',

  #spada utils functions
  'first_na', 'first_nona', 'last_na', 'last_nona', 'max_na', 'max_nona',
  'mean_na', 'mean_nona', 'min_na', 'min_nona', 'sum_na', 'sum_nona'
)

# dangerous operations --------------------------------------------------------
dangerous_operations <- c(
  # R base - code
  'assign', 'attach',
  'do.call',
  'eval', 'eval.parent', 'evalq',
  'get', 'globalenv', 'new.env', 'parent.env', 'parse',
  'remove', 'rm', 'setwd', 'substitute', 'Sys.setenv',

  # R base - files system
  'dir.create', 'dir.exists', 'dir.remove',
  'file.copy', 'file.create', 'file.remove', 'file.rename',
  'load', 'save', 'saveRDS', 'unlink',

  # R base - network
  'curl', 'download.file', 'httr::DELETE', 'httr::GET', 'httr::POST', 'httr::PUT',
  'RCurl::getURL', 'RCurl::postForm', 'socketConnection', 'url',

  # R base - IO
  'read.csv', 'readLines', 'read.table', 'readRDS',
  'write.csv', 'writeLines', 'write.table',

  # R base system
  'shell', 'shell.exec', 'system', 'system2', 'gc',

  # data.table
  ':=',
  'set', 'setattr', 'setcolorder', 'setnames', 'setDT',
  'setDF', 'setkey', 'setorder', 'substitute2'
)

# allowed operations ----------------------------------------------------------
allowed_operations <- c(
  basic_operations,
  math_funs,
  char_funs,
  date_funs,
  factor_funs,
  logical_funs
) |> unique() |> sort()

# list of filters -------------------------------------------------------------
equal_operators <- c('== (Equal)' = '==',
                     '!= (Not Equal)' = '!=')

compare_operators <- c('> (Greater)' = '>',
                       '>= (Greater or Equal)' = '>=',
                       '< (Less)' = '<',
                       '<= (Less or Equal)' = '<=')

na_operators <- c('Is NA (is.na)' = 'is_na',
                  'Not NA (! is.na)' = 'not_na')

in_operators <- c('In (%in%)' = 'in',
                  'Not In (! %in%)' = 'not_in')

between_operators <- c('Between' = 'between',
                       'Not Between' = 'not_between')

outlier_operators <- c('Outlier' = 'outlier',
                       'Not Outlier' = 'not_outlier')

logical_operators <- c('TRUE' = 'is_true',
                       'FALSE' = 'is_false')

filter_operators <- c(
  equal_operators,
  compare_operators,
  na_operators,
  in_operators,
  between_operators,
  outlier_operators,
  logical_operators
)

# lits of date formats --------------------------------------------------------
date_formats <- c(
  # hifen
  'YYYY-MM-DD' = '%Y-%m-%d',
  'DD-MM-YYY' = '%d-%m-%Y',
  'MM-DD-YY' = '%m-%d-%y',
  'YYYY-MMM-DD' = '%Y-%b-%d',
  'YY-MMM-DD' = '%y-%b-%d',
  'YY-MM-DD' = '%y-%m-%d',
  # bars
  'YYYY/MM/DD' = '%Y/%m/%d',
  'DD/MM/YYYY' = '%d/%m/%Y',
  'MM/DD/YY' = '%m/%d/%y',
  'YYYY/MMM/DD' = '%Y/%b/%d',
  'YY/MMM/DD' = '%y/%b/%d',
  'YY/MM/DD' = '%y/%m/%d',
  # dots
  'YYYY.MM.DD' = '%Y.%m.%d',
  'DD.MM.YYYY' = '%d.%m.%Y',
  'MM.DD.YY' = '%m.%d.%y',
  'YYYY.MMM.DD' = '%Y.%b.%d',
  'YY.MMM.DD' = '%y.%b.%d',
  'YY.MM.DD' = '%y.%m.%d',
  # no separator
  'YYYYMMDD' = '%Y%m%d',
  'DDMMYYYY' = '%d%m%Y',
  'MMDDYY' = '%m%d%y',
  'YYYYMMMDD' = '%Y%b%d',
  'YYMMMDD' = '%y%b%d',
  'YYMMDD' = '%y%m%d'
)

# list of summarise functions -------------------------------------------------
summarise_functions <- c(
  'Distinct' = 'distinct',
  'Count' = 'count'
)

# close browser tab -----------------------------------------------------------
js_exit <- "Shiny.addCustomMessageHandler('closeWindow', function(m) {window.close();});"

tag_js_exit <- tags$head(tags$script(HTML(js_exit)))

# waiter screen ----------------------------------------------------------------
waiter_right_foot <- div('Built with R and Shiny', class = 'screen-footer-right')
waiter_spinner <- div(class = 'screen-spinner')

waiter_screen <- tags$style(
  HTML(
    "
    .waiter-overlay {
      font-family: 'Segoe UI', Ubuntu, system-ui;
      color: white;
      display: flex;
      justify-content: center;
      align-items: center;

      background: linear-gradient(
        55deg,
        #02517D,
	    	#0A101C,
        #001030,
		    #003452,
		    #02517D,
        #003452,
        #001030,
		    #0A101C,
		    #02517D
      );

      background-size: 400% 400%;
      animation: gradientMove 16s ease infinite;
    }

    @keyframes gradientMove {
      0%   { background-position: 0% 50%; }
      50%  { background-position: 100% 50%; }
      100% { background-position: 0% 50%; }
    }

    .waiter-overlay .screen-container {
      text-align: center;
      animation: fadeIn 1.2s ease;
    }

    .waiter-overlay .screen-title-box {
      position: relative;
      width: 100vw;
      padding: 40px 0;
      border-radius: 0;
      background: rgba(10, 16, 28, 0.25);
      backdrop-filter: blur(10px);
      -webkit-backdrop-filter: blur(10px);
      border: none;
      box-shadow: 0 15px 50px rgba(0, 0, 0, 0.4);
      display: flex;
      flex-direction: column;
      align-items: center;
    }

    .waiter-overlay .screen-title {
      font-size: 80px;
      font-weight: 450;
      letter-spacing: 10px;
    }

    .waiter-overlay .screen-subtitle {
      margin-top: 10px;
      font-size: 32px;
      font-weight: 450;
      opacity: 0.9;
      letter-spacing: 4px;
    }

    .waiter-overlay .screen-subtitle2 {
      margin-top: 15px;
      font-size: 20px;
      font-weight: 100;
      opacity: 0.85;
      letter-spacing: 4px;
    }

    .waiter-overlay .screen-subtitle3 {
      margin-top: 15px;
      font-size: 16px;
      font-weight: 100;
      opacity: 0.85;
      letter-spacing: 4px;
    }

    .waiter-overlay .screen-spinner {
      margin: 30px auto 0;
      width: 60px;
      height: 60px;
      border: 5px solid rgba(255,255,255,0.2);
      border-top: 5px solid white;
      border-radius: 50%;
      animation: spin 1.1s linear infinite;
    }

    .screen-footer-right {
      font-family: 'Segoe UI', Ubuntu, system-ui;
      color: white;
      position: fixed;
      bottom: 12px;
      right: 16px;
      font-size: 16px;
      letter-spacing: 1px;
    }

    @keyframes spin {
      to { transform: rotate(360deg); }
    }

    @keyframes fadeIn {
      from { opacity: 0; transform: translateY(8px); }
      to   { opacity: 1; transform: translateY(0); }
    }

    .waiter-overlay .waiter-log {
      position: absolute;
      left: 50%;
      transform: translateX(-50%);
      font-family: 'Segoe UI', Ubuntu, system-ui;
      margin-top: 15px;
      font-size: 16px;
      font-weight: 300;
      letter-spacing: 2px;
      height: 50px;
      width: 400px;
      text-align: center;
      align-items: center;
    }
  "
  )
)

# spada_output_css ------------------------------------------------------------
spada_output_css <- tags$head(tags$style(HTML(
  paste(
    "
      body {
        font-family: 'Segoe UI', Ubuntu sans-serif;
      }

      h2 {
        font-size: 1.5rem !important;
        margin-bottom: 12px ! important;
        color: ", main_color, " !important;
        font-weight: 400 !important;
      }

      .toc-title { color: ", main_color, ";}

      .toc ul{
        list-style:none;
        padding-left:0;
        margin:0;
      }

      .toc li{
        margin-bottom:4px;
      }

      .toc a{
        display:block;
        padding:8px 12px;

        color:#444;
        text-decoration:none;

        border-radius:6px;

        transition:
          background-color .25s ease,
          color .25s ease,
          transform .25s ease,
          box-shadow .25s ease;
      }

      .toc a:hover{
        background:", main_color, ";
        color:white;

        transform:translateX(5px);

        box-shadow:0 2px 8px rgba(13,110,253,.25);
      }
    "
  )
)))

# ============================================================================.
# ---------------------------- FUNCTIONS -------------------------------------
# ============================================================================.
# short name ------------------------------------------------------------------
short_name <- function(name, max_size = 20){
  stopifnot(is.character(name))
  if(nchar(name) <= max_size) name else paste0(substr(name, 1, max_size - 3) , '...')
}
# spada_user_dir --------------------------------------------------------------
spada_user_dir <- function(which){
  dir <- normalizePath(R_user_dir('spada', which), winslash = '/', mustWork = F)

  check_dir(dir)

  return(dir)
}

# set theme -------------------------------------------------------------------
set_spada_theme <- function(session, theme){

  session$setCurrentTheme(spada_bs_theme(theme))
  session$userData$conf$theme <- theme
}

# load conf -------------------------------------------------------------------
load_conf <- function(start_conf,
                      r_user_conf_dir,
                      themes_names) {

  conf_path <- file.path(r_user_conf_dir, 'conf.qs2')

  if (file.exists(conf_path)) {
    conf_saved <- qs_read(conf_path)

    required_fields <- c(
      'theme',
      'file_size',
      'restore_session',
      'save_session',
      'plot_fill_color',
      'plot_line_color',
      'plot_title_color',
      'plot_limit'
    )

    # if all TRUE copy saved conf
    if (
      # test required items
      is.list(conf_saved) &&
        all(required_fields %in% names(conf_saved)) &&

      # test theme
      length(conf_saved$theme) == 1 &&
        isTRUE(conf_saved$theme %in% themes_names) &&

      # file size
      length(conf_saved$file_size) == 1 &&
        is.numeric(conf_saved$file_size) && conf_saved$file_size > 0 &&

      # restore session
      length(conf_saved$restore_session) == 1 &&
        isTRUE(conf_saved$restore_session %in% c('always', 'ask', 'never')) &&

      # save session
      length(conf_saved$save_session) == 1 &&
        isTRUE(conf_saved$save_session %in% c('always', 'ask', 'never')) &&

      # fill color
      length(conf_saved$plot_fill_color) == 1 &&
        isTRUE(is_hex_color(conf_saved$plot_fill_color)) &&

      # line color
      length(conf_saved$plot_line_color) == 1 &&
        isTRUE(is_hex_color(conf_saved$plot_line_color)) &&

      # title color
      length(conf_saved$plot_title_color) == 1 &&
        isTRUE(is_hex_color(conf_saved$plot_title_color)) &&

      # plot limit
      length(conf_saved$plot_limit) == 1 &&
        is.numeric(conf_saved$plot_limit)
      ){
      # copy if test is passed
      start_conf$theme <- conf_saved$theme
      start_conf$file_size <- conf_saved$file_size
      start_conf$restore_session <- conf_saved$restore_session
      start_conf$save_session <- conf_saved$save_session
      start_conf$plot_fill_color <- conf_saved$plot_fill_color
      start_conf$plot_line_color <- conf_saved$plot_line_color
      start_conf$plot_title_color <- conf_saved$plot_title_color
      start_conf$plot_limit <- conf_saved$plot_limit
    }
  }
  if(test_write_access(conf_path)) qs_save(start_conf, conf_path)

  return(start_conf)
}

# generate 2 column table in html ---------------------------------------------
gen_table2 <- function(element1, element2, w1 = '50%', w2 = '50%') {
  div(
    tags$table(
      style = 'width: 95%',
      tags$tr(
        tags$td(style = paste('padding: 10px; width:', w1, ';'), element1),
        tags$td(style = paste('padding: 10px; width:', w2, ';'), element2)
      )
    )
  )
}

# card to insert in output ----------------------------------------------------
report_card <- function(title = 'Spada - Output', annotation = NULL,
                        content = NULL){
  div(
      h2(title,
         style = paste0(
           " font-size: 1.5rem;",
           " margin-bottom: 12px;",
           " color:", main_color ,";",
           " text-align: center;"
           )
        ),
      p(annotation),
      content
  )
}

printable_report_card <- function(btns, card, id = NULL){
  div(
    style = paste0(
      "border: 2px solid", main_color, "; border-radius: 4px; ",
      "padding: 16px; margin-bottom: 8px; "
    ),
    div(style = "display: flex; width: 100%;",
        div(card, style = "width: 98%;"),
        tagList(
          lapply(btns,
               function(x){ div(x, style = 'width: 2%; text-align: right;') }
          )
        )
    ),
    br(),
    p(id)
  )
}

# output header ---------------------------------------------------------------
spada_output_info_card <- function(content){
  div(
    style = '
    padding: 20px 24px;

    background: linear-gradient(
      135deg,
      #003452 0%,
      #02517d 100%
    );

    border-radius: 4px;
    color: #ffffff;
    margin-bottom: 25px;
  ',
    content
  )
}

spada_output_header <- spada_output_info_card(
  div(
    style = 'display: flex; flex-direction: column;',
    tags$div(
      'Spada Output',
      style = ' font-size: 32px; font-weight: 450; letter-spacing: 4px;')
  )
)

spada_output_footer <- spada_output_info_card(
  div(
    style = '
      display: flex;
      justify-content: space-between;
      align-items: center;
      flex-wrap: wrap;
      gap: 15px;
    ',

    # left
    div(
      paste('Spada:', utils::packageVersion('spada')),
      tags$br(),

      tags$a(
        href = 'https://cran.r-project.org/package=spada',
        target = '_blank',
        style = 'color: white;',
        'CRAN'
      ),

      ' | ',

      tags$a(
        href = 'https://github.com/lgschuck/spada',
        target = '_blank',
        style = 'color: white;',
        'GitHub'
      ),

      ' | ',

      tags$a(
        href = 'https://lgschuck.github.io/spada/',
        target = '_blank',
        style = 'color: white;',
        'Documentation'
      ),

      ' | ',

      tags$a(
        href = 'https://lgschuck.github.io/spada_book/',
        target = '_blank',
        style = 'color: white;',
        'Book'
      )
    ),

    # right
    div(
      style = 'text-align: right;',
      format(Sys.time(), '%Y-%m-%d %H:%M:%S %Z')
    )
  )
)

spada_output <- function(output, header = spada_output_header, footer = spada_output_footer){
  toc <- tags$div(
    class = 'toc',
    style = '
    width:15vw;
    min-width:220px;
    height:100vh;
    padding:16px 16px 16px 24px;
    border-left:1px solid #ddd;
    box-sizing:border-box;
    overflow-y:auto;
    position:sticky;
    top:0;
  ',

    tags$h2(class = 'toc-title', 'Table of contents'),

    tags$ul(
      tagList(
        tags$li(tags$a(href = '#home', 'Home')),

        lapply(
          output,
          function(x) { tags$li(tags$a(href = paste0('#', x$id), x$title)) }
        )
      )
    )
  )

  cards <- tags$div(
    style = 'width:85vw; padding:24px; box-sizing:border-box;',

    lapply(
      output,
      function(x) {
        div(
          id = x$id,
          style = 'margin-bottom:32px;',
          printable_report_card(NULL, x$card, NULL)
        )

      }
    )
  )

  body_content <- tags$div(
    style = 'display:flex; align-items:flex-start;',
    cards,
    toc
  )

  tags$html(
    spada_output_css,
    tags$body(div(id = 'home', header), body_content, footer)
  )
}

# add output element ----------------------------------------------------------
add_output_element <- function(id, title, annotation, element, btn_xid, btn_eid){

  btn_x <- actionButton(btn_xid, '', icon('x'), class = 'micro-btn-cancel')
  btn_e <- actionButton(btn_eid, '', icon('pen-to-square'), class = 'micro-btn-cancel')

  list(
    'id' = id,
    'title' = title,
    'annotation' = annotation,
    'element' = element,
    'card' = report_card(title, annotation, element),
    'btn_x' = btn_x,
    'btn_e' = btn_e
  )
}

# register outputs events -----------------------------------------------------
register_output_events <- function(id, session, input, btn_xid, btn_eid) {
  observe({
    session$userData$out$elements[[id]] <- NULL
  }) |> bindEvent(input[[btn_xid]], once = TRUE)

  observe({
    session$userData$out_edit_trigger(id)
  }) |> bindEvent(input[[btn_eid]])
}

# generate output object ------------------------------------------------------
gen_output <- function(element = div(h2('Element'))){

  x <- sample(1e3, 1)
  title <- paste('Test title', x)
  annotation <- paste('Test annotation', x)
  id <- gen_element_id()
  btn_xid <- paste0('btn_xout_', id)
  btn_eid <- paste0('btn_eout_', id)

  element <- if(is.null(element)) data.frame(x = letters) |> gt() else element

  add_output_element(
    id,
    title,
    annotation,
    element,
    btn_xid,
    btn_eid
  )
}

# generate element id for outputs ---------------------------------------------
gen_element_id <- function(id = 'id', time_only = FALSE){
  if(time_only){
    gsub('.', '', format(Sys.time(), '%Y%m%d%H%M%OS8'), fixed = T)
  } else {
    paste0(id, '_', gsub('.', '', format(Sys.time(), '%Y%m%d%H%M%OS8'), fixed = T))
  }
}

# allowed operations function -------------------------------------------------
show_allowed_op <- function(){
  showModal(
    modalDialog(
      title = div(icon('check'), 'Allowed Operations'),
      selectInput(
        inputId = 'allowed_op_selected',
        label   = 'List of operations',
        choices = allowed_operations,
        selected = allowed_operations[1]
      ),
      easyClose = TRUE
    )
  )
}

# safe env function -----------------------------------------------------------
safe_env <- function(operations = NULL){

  e <- new.env(parent = emptyenv())

  lapply(operations, function(x) {
    assign(x, get(x), envir = e)
  })

  return(e)
}

# test dataset ----------------------------------------------------------------
test_dataset <- function(n_row = 1e3, n_col = 11){
  test_data <- data.table(
    integer_var = rep(sample(1:100, n_row, replace = T)),
    numeric_var = rnorm(n_row),
    char_var = rep(sample(letters, n_row, replace = T)),
    char_long_var = rep(paste(letters, collapse = ''), n_row),
    char_colors_var = rep(sample(colors(), n_row, replace = T)),
    date_var = Sys.Date() + rep(sample(-49:50, n_row, replace = T)),
    datetime_var =
      as.POSIXct(Sys.time()) + sample(0:(365 * 24 * 60 * 60), n_row, replace = TRUE),
    factor_var = as.factor(rep(sample(paste0('factor_', 1:10), n_row, replace = T))),
    num_nas_var = c(rep(NA, round(n_row/2)), rnorm(n_row - round(n_row/2))),
    int_nas_var = c(rep(NA, round(n_row/2)), sample(1:100, n_row - round(n_row/2), replace = T)),
    logical_var = rep(sample(c(TRUE, FALSE), n_row, replace = T)),
    all_na = rep(NA, n_row)
  )

  extra_cols <- n_col - ncol(test_data)

  if (extra_cols > 0) {
    for (i in seq_len(extra_cols)) {
      test_data[[paste0("extra_col_", i)]] <- rnorm(n_row)
    }
  }

  test_data
}

# empty plot function ---------------------------------------------------------
empty_plot <- function(msg = 'No plot', c = 2){
  plot(1:10, 1:10, type = 'n', xlab = '', ylab = '')
  text(5, 5, msg, cex = c)
}

# bslib btn task --------------------------------------------------------------
btn_task <- function(ID,
                     LABEL,
                     ICON = NULL,
                     LABEL_BUSY = 'Running...',
                     ...){
  input_task_button(
    id = ID,
    label = LABEL,
    icon = ICON,
    label_busy = LABEL_BUSY,
    class = 'btn-task',
    ...
  )
}

# mini_btn --------------------------------------------------------------------
mini_btn <- function(ID, LABEL, ICON){
  actionButton(
    inputId = ID,
    label = LABEL,
    icon = ICON,
    class = 'mini-btn'
  )
}

# messages - shownotification -------------------------------------------------
msg <- function(TEXT, DURATION = 2.3){
  show_toast(
    title = TEXT,
    type = 'info',
    position = 'center',
    timer = DURATION * 1000,
    timerProgressBar = F,
    width = '650px'
  )
}

msg_error <- function(TEXT, DURATION = 2.3){
  show_toast(
    title = TEXT,
    type = 'error',
    position = 'center',
    timer = DURATION * 1000,
    timerProgressBar = F,
    width = '650px'
  )
}

bmsg <- function(TEXT, DURATION = 2.3){
  bslib::show_toast(

    bslib::toast(
      TEXT,
      header = bslib::toast_header(
        title = TEXT,
        status = "Spada"
      ),
      type = NULL,
      duration_s = DURATION,
      position = c('middle', 'center')
    )
  )
}

# try convert -----------------------------------------------------------------
try_convert <- function(x, fun){
  tryCatch(fun(x),
           error = function(e) rep(NA, x |> length()),
           warning = function(w) rep(NA, x |> length()))
}

# convert function ------------------------------------------------------------
convert <- function(x, type, date_format = '%Y-%m-%d',
                    date_origin = '1970-01-01'){
  if(x |> is.raw()) x <- as.numeric(x)

  if(type == 'as.numeric'){
    suppressWarnings(as.numeric(x))
  } else if(type == 'as.integer'){
    suppressWarnings(as.integer(x))
  } else if(type == 'as.character'){
    as.character(x)
  } else if(type == 'as.Date'){
    if(x |> inherits('Date')) {
      x
    } else if(is.numeric(x)){
      as.Date(x, origin = date_origin)
    } else if(is.raw(x) || is.complex(x)){
      suppressWarnings(
        as.Date(x |> as.numeric(), origin = date_origin)
      )
    } else as.Date(x, format = date_format)
  } else if(type == 'as.factor'){
    as.factor(x)
  } else if(type == 'as.double'){
    suppressWarnings(as.double(x))
  } else if(type == 'as.complex'){
    if(is.complex(x)){
      x
    } else {
      x1 <- suppressWarnings(as.numeric(x))
      as.complex(x1)
    }
  }
}

# filter rows function --------------------------------------------------------
filter_rows <- function(dt, var, operator, filter_value){

  stopifnot(is.data.table(dt))

  if(operator == '=='){
    dt[var1 == filter_value, , env = list(var1 = var)]
  } else if(operator == '!='){
    dt[var1 != filter_value, , env = list(var1 = var)]
  } else if(operator == '>'){
    dt[var1 > filter_value, , env = list(var1 = var) ]
  } else if(operator == '>='){
    dt[var1 >= filter_value, , env = list(var1 = var) ]
  } else if(operator == '<'){
    dt[var1 < filter_value, , env = list(var1 = var) ]
  } else if(operator == '<='){
    dt[var1 <= filter_value, , env = list(var1 = var) ]
  } else if(operator == 'is_na'){
    dt[is.na(var1), , env = list(var1 = var) ]
  } else if(operator == 'not_na'){
    dt[!is.na(var1), , env = list(var1 = var) ]
  } else if(operator == 'in'){
    dt[var1 %in% filter_value,  , env = list(var1 = var)]
  } else if(operator == 'not_in'){
    dt[!var1 %in% filter_value, , env = list(var1 = var) ]
  } else if(operator == 'between'){
    dt[var1 %between% filter_value, , env = list(var1 = var) ]
  } else if(operator == 'not_between'){
    dt[!(var1 %between% filter_value), , env = list(var1 = var) ]
  } else if(operator == 'outlier'){
    dt[Outlier(var1, value = F, na.rm = T), , env = list(var1 = var)]
  } else if(operator == 'not_outlier'){
    dt[!Outlier(var1, value = F, na.rm = T), , env = list(var1 = var)]
  } else if(operator == 'is_true'){
    dt[var1 == TRUE,  , env = list(var1 = var)]
  } else if(operator == 'is_false'){
    dt[var1 == FALSE,  , env = list(var1 = var)]
  }
}

# filter rows 2 vars ----------------------------------------------------------
filter_rows_2vars <- function(dt, var1, var2, operator){

  stopifnot(is.data.table(dt))

  v1 <- dt[[var1]]
  v2 <- dt[[var2]]

  # Convert to char if factor
  if (is.factor(v1) && is.factor(v2)) {
    v1 <- as.character(v1)
    v2 <- as.character(v2)
  }

  if(operator == '=='){
    dt[v1 == v2, ]
  } else if(operator == '!='){
    dt[v1 != v2, ]
  } else if(operator == '>'){
    dt[v1 > v2, ]
  } else if(operator == '>='){
    dt[v1 >= v2, ]
  } else if(operator == '<'){
    dt[v1 < v2, ]
  } else if(operator == '<='){
    dt[v1 <= v2, ]
  } else {
    stop('Invalid Operator')
  }
}

# tip place top by default ----------------------------------------------------
ttip <- function(TRIGGER, ..., ID = NULL, PLACE = 'top'){
  tooltip(trigger = TRIGGER, ... = ..., id = ID, placement = PLACE)
}

# get function help -----------------------------------------------------------
get_help_file <- function(pak, fun){
  paste(
    utils::capture.output(
      tools::Rd2HTML(tools::Rd_db(pak)[[paste0(fun, '.Rd')]])
    ),
    collapse = '\n'
  )
}

# fun help modal --------------------------------------------------------------
fun_help_modal <- function(pak, fun) {
  showModal(modalDialog(
    HTML(get_help_file(pak, fun)),
    title = div(icon('circle-question'), paste('Help:', pak, '-', fun)),
    easyClose = TRUE,
    size = 'xl'
  ))
}

# format decimals -------------------------------------------------------------
f_dec <- function(x, dig = 0){
  if(is.numeric(x) && !is.na(x) |> all()){
    format(round(x, dig), nsmall = dig, scientific = F)
  } else {
    NA
  }
}

# make var names --------------------------------------------------------------
make_var_names <- function(df){
  stopifnot(df |> is.data.frame())
  names(df) <- names(df) |> make.names(unique = T)
  return(df)
}

# test all equal --------------------------------------------------------------
test_all_equal <- function(x){
  all(x == x[1])
}

# obj type --------------------------------------------------------------------
obj_type <- function(x){
  if(x |> is.numeric()) 'numeric'
  else if (x |> is_date()) 'date'
  else if (x |> is.factor()) 'factor'
  else if (x |> is.character()) 'char'
  else if (x |> is.logical()) 'logical'
  else if (x |> is.complex()) 'complex'
  else 'other'
}

# stati_card ------------------------------------------------------------------
stati_card <- function(VALUE, SUBTITLE, ICON = NULL, LEFT = T,
                       COLOR = '#FFFFFF', BACKGROUND = main_color,
                       ANIMATE = T, DURATION = 30, ID = NULL){
  statiCard(value = VALUE,
            subtitle = SUBTITLE,
            icon = ICON,
            left = LEFT,
            color = COLOR,
            background = BACKGROUND,
            animate = ANIMATE,
            duration = DURATION,
            id = ID)
}

# make valid cols -------------------------------------------------------------
make_valid_cols <- function(x){
  if(is.raw(x) || is.complex(x)){
    as.character(x)
  } else {
    x
  }
}

# is spada data frame ---------------------------------------------------------
is_spada_df <- function(df){
  is.data.frame(df) && all(sapply(df, is.atomic)) && ncol(df) > 0 && nrow(df) > 0
}

# is spada data table ---------------------------------------------------------
is_spada_dt <- function(dt){
  is.data.table(dt) && is_spada_df(dt)
}

# test data format-------------------------------------------------------------
test_data_format <- function(data){
  is.list(data) &&
    length(data) > 0 &&
    (sapply(data, is_spada_df) |> all())
}


# running modal ---------------------------------------------------------------
running_modal <- function(text = 'Running...'){
  showModal(
    modalDialog(
      div(
        icon('spinner', class='fa fa-spinner fa-spin fa-1x'),
        span(text, style = 'color:#585c5e;')
      ),
      footer = NULL,
      easyClose = FALSE,
      size = 'm',
      fade = T
    )
  )
}

remove_running_modal <- function(time = 0.7){
  Sys.sleep(time)
  removeModal()
}

# not spada df modal ----------------------------------------------------------
abort_filter_modal <- function(){
  showModal(modalDialog(
    'The resultant dataset must be valid (nrow and ncol > 0)',
    title = div(icon('triangle-exclamation'), 'Operation Aborted')
  ))
}

# abort save session/output ---------------------------------------------------
abort_run_local_modal <- function(msg = 'Spada must run locally'){
  showModal(modalDialog(
    msg,
    title = div(icon('triangle-exclamation'), 'Operation Aborted'),
    easyClose = T
  ))
}

# show startup function -------------------------------------------------------
show_startup_screen <- function() {
  waiterShowOnLoad(html = tagList(
    div(
      class = 'screen-container',
      div(
        class = 'screen-title-box',
        div('Spada', class = 'screen-title'),
        div('a Shiny Package for Data Analysis', class = 'screen-subtitle2')
      ),
      waiter_spinner,
      div(id = 'spada_startup_text', class = 'waiter-log', 'Building UI')
    ),
    waiter_right_foot
  ))
}

# exit screen function --------------------------------------------------------
show_exit_screen <- function(save = TRUE) {
  waiter_show(
    html = {
      if(save){
        tagList(
          div(
            class = 'screen-container',
            div(class = 'screen-title-box',
            div('Spada is saving your work', class = 'screen-subtitle'),
            div('Please do not close this window', class = 'screen-subtitle2')
            ),
            waiter_spinner,
            div(id = 'spada_exit_text', class = 'waiter-log', '')
          ),
          waiter_right_foot
        )
      } else {
        html = tagList(
          div(
            class = 'screen-container',
            div(class = 'screen-title-box',
              div('Closing Spada', class = 'screen-subtitle')
            ),
            waiter_spinner,
            div(id = 'spada_exit_text', class = 'waiter-log', 'Saving conf')
          ),
          waiter_right_foot
        )
      }
    }
  )
}

# update startup text ---------------------------------------------------------
update_waiter_text <- function(session, id, text) {
  session$sendCustomMessage('update-text', list(id = id, text = text))
}

# update start up text-----------------------------------------------------------
waiter_text <- "
  Shiny.addCustomMessageHandler(
    'update-text',
    function(data){

      const el = document.getElementById(data.id);

      if(el){
        const line = document.createElement('div');

        line.innerText = data.text;

        el.appendChild(line);
      }
    }
  )
"

tag_js_waiter_text <- tags$head(tags$script(HTML(waiter_text)))

# spada save function ---------------------------------------------------------
spada_save <- function(dir, object, file_name){
  out_file <- file.path(dir, file_name)

  check_dir(dir)

  try_save <- try(qs_save(object, out_file), silent = T)

  if(inherits(try_save, 'try-error')){
    tmp_file <- temp_file(file_name)
    qs_save(object, tmp_file)
    no_write_msg(out_file, tmp_file)
  }
}

# exit spada with saving session ----------------------------------------------
exit_with_save <- function(session){

  show_exit_screen()
  update_waiter_text(session, 'spada_exit_text', 'Saving data')
  spada_save(session$userData$conf$data_dir,
             session$userData$dt$dt,
             'data.qs2')
  Sys.sleep(0.3)
  update_waiter_text(session, 'spada_exit_text', 'Saving output')
  spada_save(session$userData$conf$data_dir,
             session$userData$out$elements,
             'output.qs2')
  Sys.sleep(0.4)
  update_waiter_text(session, 'spada_exit_text', 'Saving conf')
  spada_save(session$userData$conf$conf_dir,
             reactiveValuesToList(session$userData$conf),
             'conf.qs2')
  Sys.sleep(1)
  session$sendCustomMessage(type = 'closeWindow', message = 'message')
  if(session$userData$run_local) stopApp()
}

# exit spada without saving session -------------------------------------------
exit_without_save <- function(session){

  show_exit_screen(F)

  spada_save(session$userData$conf$conf_dir,
             reactiveValuesToList(session$userData$conf),
             'conf.qs2')

  Sys.sleep(3)
  session$sendCustomMessage(type = 'closeWindow', message = 'message')
  if(session$userData$run_local) stopApp()
}

# test write access -----------------------------------------------------------
test_write_access <- function(file){
  all(!file.exists(file) || isTRUE(file.access(file, 2) == 0))
}

# temp normalized file path ---------------------------------------------------
temp_file <- function(file_name = ''){
  file.path(normalizePath(tempdir(), winslash = '/'), file_name)
}

# no access to write message --------------------------------------------------
no_write_msg <- function(file1, file2){
  message(paste(
    Sys.time(),
    '- Spada could not write on:\n',
    file1,
    '\n Data saved in',
    file2
    )
  )
}

# make names append lists -----------------------------------------------------
make_names_append_list <- function(new_list, actual_names,
                                   suffix = '_previous', index = 1) {

    names(new_list) <- make.names(names(new_list), unique = TRUE)

    while (any(grepl(paste0("^.*", suffix, "$"), actual_names))) {
      suffix <- paste0(suffix, "_", i)
      index <- index + 1
    }

    conflict_names <- names(new_list) %in% actual_names
    names(new_list)[conflict_names] <- paste0(names(new_list)[conflict_names], suffix)

    return(new_list)
}

# plot z test -----------------------------------------------------------------
plot_z_test <- function(confidence = 0.95, test_type = 'two.sided',
                        z_value = qnorm(confidence),
                        color_fill = 'brown3', color_line = 'steelblue') {
  # Define the standardized x-axis (Z-scores)
  x <- seq(-4, 4, length.out = 1000)  # Standardized Z-values
  y <- dnorm(x, mean = 0, sd = 1)  # Standard normal distribution

  alpha <- 1 - confidence

  # Determine critical values in standardized scale
  if (test_type == 'two.sided') {
    z_critical <- qnorm(1 - alpha / 2)
    critical_left <- -z_critical
    critical_right <- z_critical
  } else if (test_type == 'greater') {
    critical_right <- qnorm(1 - alpha)
  } else if (test_type == 'less') {
    critical_left <- qnorm(alpha)
  } else {
    stop("Invalid 'test_type'. Use 'two.sided', 'greater', or 'less'.")
  }

  # Create the plot
  plot(x, y, type = 'l', lwd = 2, col = color_line,
       xlab = 'Standardized Values (Z)', ylab = 'Density',
       main = paste('Z Test\n Type:', test_type, '- Conf.',
                    confidence * 100, '% | Z value:', z_value |> f_num(dig = 3)))

  # Highlight the critical regions
  if (test_type == 'two.sided') {
    polygon(c(x[x <= critical_left], critical_left),
            c(y[x <= critical_left], 0), col = color_fill, border = NA)
    polygon(c(x[x >= critical_right], critical_right),
            c(y[x >= critical_right], 0), col = color_fill, border = NA)

  } else if (test_type == 'less') {
    polygon(c(x[x <= critical_left], critical_left),
            c(y[x <= critical_left], 0), col = color_fill, border = NA)

  } else if (test_type == 'greater') {
    polygon(c(x[x >= critical_right], critical_right),
            c(y[x >= critical_right], 0), col = color_fill, border = NA)
  }

  if(abs(z_value) <= 4){
    abline(v = z_value, col = 'black', lwd = 2, lty = 2)
  }

}

# plot t test -----------------------------------------------------------------
plot_t_test <- function(confidence = 0.95, test_type = 'two.sided',
                        df = 10, t_value = qt(confidence, df = 10),
                        color_fill = 'brown3', color_line = 'steelblue') {
    x <- seq(-4, 4, length.out = 1000)
  y <- dt(x, df = df)

  alpha <- 1 - confidence

  # Determine critical values in standardized scale
  if (test_type == 'two.sided') {
    t_critical <- qt(1 - alpha / 2, df = df)
    critical_left <- -t_critical
    critical_right <- t_critical
  } else if (test_type == 'greater') {
    critical_right <- qt(1 - alpha, df = df)
  } else if (test_type == 'less') {
    critical_left <- qt(alpha, df = df)
  } else {
    stop("Invalid 'test_type'. Use 'two.sided', 'greater', or 'less'.")
  }

  # Create the plot
  plot(x, y, type = 'l', lwd = 2, col = color_line,
       xlab = 't-value', ylab = 'Density',
       main = paste(
         'One-sample t Test\n',
         'Type:', test_type,
         '- Conf.', confidence * 100,
         '% | t value =', t_value |> f_num(dig = 3),
        '(df =', df |> f_int(), ')'
       )
  )

  # Highlight the critical regions
  if (test_type == 'two.sided') {
    polygon(c(x[x <= critical_left], critical_left),
            c(y[x <= critical_left], 0), col = color_fill, border = NA)
    polygon(c(x[x >= critical_right], critical_right),
            c(y[x >= critical_right], 0), col = color_fill, border = NA)

  } else if (test_type == 'less') {
    polygon(c(x[x <= critical_left], critical_left),
            c(y[x <= critical_left], 0), col = color_fill, border = NA)

  } else if (test_type == 'greater') {
    polygon(c(x[x >= critical_right], critical_right),
            c(y[x >= critical_right], 0), col = color_fill, border = NA)
  }

  if(abs(t_value) <= 4){
    abline(v = t_value, col = 'black', lwd = 2, lty = 2)
  }

}

# test if is hex color --------------------------------------------------------
is_hex_color <- function(x) {
  grepl("^#[A-Fa-f0-9]{6}$", x)
}

# test output format-----------------------------------------------------------
test_output_format <- function(output){

  # if list of len 0 is OK
  (is.list(output) && length(output) == 0) ||

    # if len > 0 must pass
    (is.list(output) && length(output) > 0 &&

       # all inside elements must be lists
       all(sapply(output, class) == 'list') &&

       # all names of inside lists must match
       all(
         sapply(output, \(x) {
           all(names(x) %in% c('id', 'title', 'annotation', 'element', 'card', 'btn_x', 'btn_e'))
         })
       ) &&
       # test class of each element in the inside lists
       (
         all(sapply(output, \(x){ 'character' %in% class(x$id)})) &&
           all(sapply(output, \(x){ 'character' %in% class(x$title) })) &&
           all(sapply(output, \(x){ 'character' %in% class(x$annotation) })) &&
           all(sapply(output, \(x){ any(c('gt_tbl', 'html', 'shiny.tag') %in% class(x$element)) })) &&
           all(sapply(output, \(x){ 'shiny.tag' %in% class(x$card) })) &&
           all(sapply(output, \(x){ 'shiny.tag' %in% class(x$btn_x) })) &&
           all(sapply(output, \(x){ 'shiny.tag' %in% class(x$btn_e) }))
       )
    )
}

# check existence of directory ------------------------------------------------
check_dir <- function(dir){
  if(!dir.exists(dir)) dir.create(dir, recursive = T)
}

# linear model df output ------------------------------------------------------
linear_model_df_output <- function(model_summary){
  stopifnot(model_summary |> class() == 'summary.lm')

  table_summary <- as.data.frame(model_summary$coefficients)

  table_summary$Variable <- rownames(table_summary)
  rownames(table_summary) <- NULL

  table_summary <- table_summary[, c("Variable", "Estimate", "Std. Error",
                                     "t value", "Pr(>|t|)")]

  table_summary <- table_summary |> as.data.table()
  table_summary[, `Sig Levels` := fcase(
    `Pr(>|t|)` < 0.001, '***',
    `Pr(>|t|)` < 0.01, '**',
    `Pr(>|t|)` < 0.05, '*',
    `Pr(>|t|)` < 0.1, '.',
    default = ''
  )]
}

# linear model gt metrics -----------------------------------------------------
linear_model_df_metrics <- function(model_summary){
  stopifnot(model_summary |> class() == 'summary.lm')

  f <- model_summary$fstatistic
  f_p_value <- pf(f['value'], f['numdf'], f['dendf'], lower.tail = FALSE)
  data.frame(
    Metric = c('Residual Std Error', 'R-squared', 'Adjusted R-squared',
               'F-statistic', 'F p-value'),
    Value = c(
      model_summary$sigma,
      model_summary$r.squared,
      model_summary$adj.r.squared,
      model_summary$fstatistic[1],
      f_p_value
    )
  )
}

# get active dt ---------------------------------------------------------------
get_act_dt <- function(session){
  session$userData$dt$dt[[ session$userData$dt$act_name ]]
}

# update active dt ------------------------------------------------------------
update_act_dt <- function(session, new_dt, data_changed = TRUE,
                          updated_cols = NULL,
                          change_type = c('new_data',
                                          'convert_cols',
                                          'rename_cols',
                                          'order_cols',
                                          'select_cols')){

  stopifnot(new_dt |> is.data.table())

  change_type <- match.arg(change_type)

  new_dt <- lapply(new_dt, make_valid_cols) |> as.data.table()

  session$userData$dt$dt[[ session$userData$dt$act_name ]] <- new_dt

  if(data_changed) {

    session$userData$dt$updated_cols <- updated_cols
    session$userData$dt$data_changed_type <- change_type
    session$userData$data_changed(session$userData$data_changed() + 1)
  }
}

# append dt -------------------------------------------------------------------
append_dt <- function(session, new_dt, new_dt_name) {

  stopifnot(new_dt |> is.data.frame())
  stopifnot(new_dt_name |> is_valid_name() &&
              new_dt_name %notin% names(session$userData$dt$dt))

  new_dt <- lapply(new_dt, make_valid_cols) |> as.data.table() |> make_var_names()

  new_list <- list(new_dt)
  names(new_list) <- new_dt_name

  session$userData$dt$dt <- c(session$userData$dt$dt, new_list)
}

# append meta data ------------------------------------------------------------
append_meta <- function(session, new_meta, new_dt_name){
  stopifnot(new_meta |> is.data.frame())

  meta_list <- list(new_meta)
  names(meta_list) <- new_dt_name

  session$userData$dt$meta <- c(
    session$userData$dt$meta,
    meta_list
  )
}

# update meta data ------------------------------------------------------------
update_meta <- function(dt = data.table(),
                        previous_meta, col_names, updated_cols, ncols,
                        change_type = c('new_data',
                                        'convert_cols',
                                        'rename_cols',
                                        'order_cols',
                                        'select_cols')){

  stopifnot(dt |> is.data.table())
  stopifnot(previous_meta |> is.data.table())

  change_type <- match.arg(change_type)

  if(change_type %in% c('new_data', 'convert_cols')){
    new_meta <- dt |> df_info()

    # delete updated vars from previous meta
    previous_meta <- previous_meta[var %notin% updated_cols, ]

    new_meta <- rbindlist(list(previous_meta, new_meta), fill = T)

    order_df <- data.table(var = col_names, index = 1:length(col_names))

    new_meta <- merge(new_meta, order_df, by = 'var', all.x = TRUE)

    setorderv(new_meta, c('index'))

    new_meta$index <- NULL
    new_meta$cols <- ncols

    return(new_meta)
  } else if (change_type == 'rename_cols'){
    previous_meta$var <- col_names
    return(previous_meta)
  } else if (change_type == 'order_cols'){

    order_df <- data.table(var = col_names, index = 1:length(col_names))

    meta_reordered <- merge(previous_meta, order_df, by = 'var', all.x = TRUE)

    setorderv(meta_reordered, c('index'))

    meta_reordered$index <- NULL

    return(meta_reordered)

  } else if (change_type == 'select_cols'){
    previous_meta <- previous_meta[var %in% col_names, ]

    previous_meta$cols <- ncols
    return(previous_meta)
  }
}

# descriptive stats -----------------------------------------------------------
desc_stats <- function(df = NULL,
                       fmt_digits = 9,
                       central_tendency = c('mean', 'gmean', 'hmean', 'median', 'mode'),
                       dispersion = c('min', 'max', 'IQR', 'range', 'var', 'sd'),
                       shape = c('skew', 'kurt')) {

  desc_stats <- list()

  # central tendency
  if('mean' %in% central_tendency){
    desc_stats$Mean <- sapply(
      df,
      \(x) {if(x |> is.numeric()) mean_nona(x) |> f_num(dig = fmt_digits) else NA })
  }

  if('gmean' %in% central_tendency){
    desc_stats$Gmean <- sapply(
      df,
      \(x) {if(x |> is.numeric()) suppressWarnings(Gmean(x, na.rm = T)) |> f_num(dig = fmt_digits) else NA })
  }

  if('hmean' %in% central_tendency){
    desc_stats$Hmean <- sapply(
      df,
      \(x) {if(x |> is.numeric()) Hmean(x, na.rm = T) |> f_num(dig = fmt_digits) else NA })
  }

  if('median' %in% central_tendency){
    desc_stats$Median <- sapply(
      df,
      \(x) {if(x |> is.numeric()) fmedian(x, na.rm = T) |> f_num(dig = fmt_digits)  else NA })
  }

  if('mode' %in% central_tendency){
    desc_stats$Mode <- sapply(
      df,
      \(x) {if(x |> is.numeric() ||
               x |> is.character() ||
               x |> is.factor()){
        x_mode <- fmode(x, na.rm = T) |> f_num(dig = fmt_digits)
        if(is.na(x_mode) |> all()) NA else paste(x_mode, collapse = ' | ')
      } else { NA }
      })
  }

  # dispersion
  if('min' %in% dispersion){
    desc_stats$Min <- sapply(
      df,
      \(x) {if(x |> is.numeric()) min_nona(x) |> f_num(dig = fmt_digits) else NA })
  }

  if('max' %in% dispersion){
    desc_stats$Max <- sapply(
      df,
      \(x) {if(x |> is.numeric()) max_nona(x) |> f_num(dig = fmt_digits) else NA })
  }

  if('IQR' %in% dispersion){
    desc_stats$IQR <- sapply(
      df,
      \(x) {if(x |> is.numeric()) IQR(x, na.rm = T) |> f_num(dig = fmt_digits) else NA })
  }

  if('range' %in% dispersion){
    desc_stats$Range <- sapply(
      df,
      \(x) {
        if(x |> is.numeric()){
          paste('[', frange(x, na.rm = T) |> f_num(dig = fmt_digits) , ']', collapse = '--->')
        } else { NA }
      }
    )
  }

  if('var' %in% dispersion){
    desc_stats$Variance <- sapply(
      df,
      \(x) {if(x |> is.numeric()) fvar(x, na.rm = T) |> f_num(dig = fmt_digits) else NA })
  }

  if('sd' %in% dispersion){
    desc_stats[['Standard Deviation']] <- sapply(
      df,
      \(x) {if(x |> is.numeric()) fsd(x, na.rm = T) |> f_num(dig = fmt_digits) else NA })
  }

  if('skew' %in% shape){
    desc_stats[['Skewness']] <- sapply(
      df,
      \(x) {if(x |> is.numeric()) Skew(x, na.rm = T) |> f_num(dig = fmt_digits) else NA })
  }

  if('kurt' %in% shape){
    desc_stats[['Kurtosis']] <- sapply(
      df,
      \(x) {if(x |> is.numeric()) Kurt(x, na.rm = T) |> f_num(dig = fmt_digits) else NA })
  }

  desc_stats
}

# plots -----------------------------------------------------------------------
spada_plot <- function(
    type = 'hist',
    df = NULL,
    xvar = NULL,
    yvar = NULL,
    zvar = NULL,
    xlab = '',
    ylab = '',
    fill_color = '#0099F8',
    line_color = '#000000',
    title_color = '#02517d',
    title = NULL,
    bins = 25,
    vertical_line = NULL,
    point_shape = '.',
    line_type = 1,
    mean_value = NULL,
    sd_value = NULL,
    sample_limit = 1e5,
    na_rm = TRUE
  ){

  if(nrow(df) > sample_limit){
    df <- df[sample.int(nrow(df), sample_limit), , drop = FALSE]
  }

  stopifnot(is.data.frame(df))

  if(type == 'hist'){

    ggplot(data = df, aes(x = .data[[xvar]])) +
      geom_histogram(
        bins = bins,
        fill = fill_color,
        color = '#000000',
        na.rm = na_rm
      ) +
      geom_vline(xintercept = vertical_line, color = line_color, linetype = line_type) +
      labs(title = title, x = xlab, y = ylab) +
      theme_classic() +
      theme(axis.text.x = element_text(size = 14),
            axis.text.y = element_text(size = 14),
            axis.title.y = element_text(size = 16),
            plot.title = element_text(color = title_color, size = 16, face = 'bold')
      )
  } else if (type == 'hist_density'){

    ggplot(data = df, aes(x = .data[[xvar]])) +
      geom_histogram(aes(y = after_stat(density)),
                     bins = bins,
                     fill = fill_color,
                     color = 'black',
                     na.rm = na_rm) +
      stat_function(fun = dnorm,
                    args = list(mean = mean_value,
                                sd = sd_value),
                    color = line_color,
                    linewidth = 1) +
      labs(title = title, x = xlab, y = ylab) +
      theme_classic() +
      theme(axis.text.x = element_text(size = 14),
            axis.text.y = element_text(size = 14),
            axis.title.x = element_text(size = 16),
            axis.title.y = element_text(size = 16),
            plot.title = element_text(color = title_color, size = 16, face = 'bold')
      )

  } else if (type == 'boxplot'){

    ggplot(data = df, aes(x = .data[[xvar]])) +
      stat_boxplot(geom = 'errorbar', width = 0.3, na.rm = na_rm) +
      geom_boxplot(fill = fill_color, na.rm = na_rm) +
      ylim(-1.2, 1.2) +
      geom_vline(xintercept = vertical_line, color = line_color, linetype = line_type) +
      labs(title = title, x = xlab, y = ylab) +
      theme_classic() +
      theme(
        axis.ticks.y = element_blank(),
        axis.text.y  = element_blank(),
        axis.line.y  = element_blank(),
        panel.border = element_rect(color = '#000000', fill = NA),
        axis.text.x = element_text(size = 14),
        plot.title = element_text(color = title_color, size = 16, face = 'bold')
      )
  } else if(type == 'dots'){

    ggplot(data = df, aes(x = .data[[xvar]], y = .data[[yvar]])) +
      geom_point(shape = point_shape, color = fill_color, na.rm = na_rm) +
      geom_hline(yintercept = vertical_line, color = line_color, linetype = line_type) +
      labs(title = title, x = xlab, y = ylab) +
      theme_classic() +
      theme(axis.text.x = element_text(size = 14),
            axis.text.y = element_text(size = 14),
            axis.title.x = element_text(size = 16),
            axis.title.y = element_text(size = 16),
            plot.title = element_text(color = title_color, size = 16, face = 'bold')
      )
  } else if (type == 'barplot'){

    ggplot(data = df, aes(x = factor(.data[[xvar]]))) +
      geom_bar(fill = fill_color, na.rm = na_rm) +
      labs(title = title, x = xlab, y = ylab) +
      theme_classic() +
      theme(axis.text.x = element_text(size = 14),
            axis.text.y = element_text(size = 14),
            axis.title.y = element_text(size = 16),
            plot.title = element_text(color = title_color, size = 16, face = 'bold')
      )

  } else if (type == 'boxplot_group'){

    ggplot(data = df, aes(x = .data[[xvar]], y = .data[[yvar]], fill = .data[[xvar]])) +
      stat_boxplot(geom = 'errorbar', width = 0.3, na.rm = na_rm) +
      geom_boxplot(orientation = 'x', na.rm = na_rm) +
      geom_hline(yintercept = vertical_line,
                 color = line_color) +
      coord_flip() +
      labs(title = title, x = xlab, y = ylab) +
      theme_classic() +
      theme(
        legend.position = 'none',
        axis.ticks.y = element_blank(),
        axis.line.y  = element_blank(),
        panel.border = element_rect(color = 'black', fill = NA),
        axis.text.x = element_text(size = 14),
        axis.text.y = element_text(size = 14),
        plot.title = element_text(color = title_color, size = 16, face = 'bold')
      )
  } else if (type == 'scatter'){

    ggplot(data = df, aes(x = .data[[xvar]], y = .data[[yvar]])) +
      geom_point(color = fill_color, shape = point_shape, na.rm = na_rm) +
      labs(title = title, x = xlab, y = ylab) +
      theme_classic() +
      theme(axis.text.x = element_text(size = 14),
            axis.text.y = element_text(size = 14),
            axis.title.x = element_text(size = 16),
            axis.title.y = element_text(size = 16),
            plot.title = element_text(color = title_color, size = 16, face = 'bold')
      )
  } else if (type == 'qq_plot'){
    ggplot(data = df, aes(sample = .data[[xvar]])) +
      stat_qq(color = fill_color, na.rm = na_rm) +
      stat_qq_line(color = line_color, na.rm = na_rm) +
      labs(title = title, x = xlab, y = ylab) +
      theme_classic() +
      theme(axis.text.x = element_text(size = 14),
            axis.text.y = element_text(size = 14),
            axis.title.x = element_text(size = 16),
            axis.title.y = element_text(size = 16),
            plot.title = element_text(color = title_color, size = 16, face = 'bold')
      )
  }
}

# plotTag with attributes -----------------------------------------------------
plot_tag <- function(plot, w = 900, h = 300){
  plotTag(
    plot,
    '',
    width = w,
    height = h,
    suppressSize = 'xy',
    attribs = list(style = 'max-width:90%; height:auto;')
  )
}

# status row for restore session ----------------------------------------------
status_row <- function(icon_name, color, text) {
  div(
    style = paste0(
      'display:flex; align-items:center; gap:12px;
       padding:12px 16px;
       border-radius:2px;
       background-color:', color, '15;'
    ),
    icon(icon_name, style = paste0('font-size:20px; color:', color)),
    tags$span(text, style = 'font-size:20px; font-weight:400;')
  )
}

# display restore status ------------------------------------------------------

display_restore_status <- function(session_restore_status, btn_ok){
  list_check_restore <- div(
    style = 'display:flex; flex-direction:column; gap:12px;',

    switch(
      substr(session_restore_status, 1, 1),
      '1' = status_row('check', '#2e7d32', 'Data restored successfully'),
      '2' = status_row('times', '#c62828', 'Data not found'),
      '3' = status_row('circle-question', '#ed6c02', 'Data in invalid format')
    ),

    switch(
      substr(session_restore_status, 3, 3),
      '1' = status_row('check', '#2e7d32', 'Output restored successfully'),
      '2' = status_row('times', '#c62828', 'Output not found'),
      '3' = status_row('circle-question', '#ed6c02', 'Output in invalid format')
    )
  )

  # show modal
  showModal(modalDialog(
    title = div(h2(
      bs_icon('database-up', size = '40px',
              style = 'margin-right: 8px; color:#FFFFFF'),
      'Session Status'
    )),
    div(style = 'padding:12px; border-radius:0px;', list_check_restore),
    size = 'l',
    easyClose = T,
    footer = div(style = 'text-align:right;', btn_ok)

  ))
}

# summarise dataset -----------------------------------------------------------
summarise_dt <- function(dt, fun = c('count', 'distinct'), vars){

  stopifnot(is.data.table(dt))
  stopifnot(all(vars %in% (dt |> names())))

  fun <- match.arg(fun)
  if(fun == 'distinct'){
    dt[, .SD, .SDcols = vars] |> unique()
  } else if(fun == 'count'){
    dt[, .N, by = vars]
  }
}

# build calls -----------------------------------------------------------------
build_calls <- function(new, funs, vars){

  vars_as_symbols <- lapply(vars, as.name)

  calls <- Map(
    \(v, f) call(f, v),
    vars_as_symbols,
    funs
  )

  as.call(c(as.name('.'), setNames(calls, new)))
}

# dt join ---------------------------------------------------------------------
dt_join <- function(dt1, dt2, vars1, vars2,
                     join_type = c('left', 'right', 'full', 'inner')){

  stopifnot(is_spada_dt(dt1))
  stopifnot(is_spada_dt(dt2))
  stopifnot(length(vars1) > 0)
  stopifnot(length(vars2) > 0)

  join_type <- match.arg(join_type)

  types_x <- sapply(dt1[, .SD, .SDcols = vars1], obj_type)
  types_y <- sapply(dt2[, .SD, .SDcols = vars2], obj_type)

  stopifnot('The variables types must match' = all(types_x == types_y))

  temp <- merge(
    dt1,
    dt2,
    by.x = vars1,
    by.y = vars2,
    all = join_type == 'full',
    all.x = any(c('left', 'full') %in% join_type),
    all.y = any(c('right', 'full') %in% join_type),
    allow.cartesian = T,
    suffixes = c('_x', '_y')
  )
  return(temp)
}


# math and other functions ----------------------------------------------------

df_info <- function(df, type = c('basic', 'complete')){
  stopifnot(is.data.frame(df))

  type = match.arg(type)

  if (fncol(df) == 0) {
    dt <- data.table(
      var = 'v1',
      type = NA,
      class = NA,
      min = NA,
      max = NA,
      n_valid = NA,
      perc_valid = NA,
      n_zero = NA,
      perc_zero = NA,
      n_nas = NA,
      perc_nas = NA,
      rows = NA,
      cols = NA
    )
    if(type == 'complete'){
      dt[, `:=`(n_unique = NA, perc_unique = NA, size = 0)]
    }

  } else {
    rows <- fnrow(df)
    cols <- fncol(df)

    res <- lapply(seq_len(cols), function(j) {
      x <- df[[j]]

      nas <- whichNA(x) |> NROW()
      valid <- rows - nas

      if(type == 'complete'){ uniq <- fnunique(x) }

      if (is.numeric(x)){
        zeros <- NROW(x %==% 0)

        rangev <- frange(x, na.rm = T)
        minv <- rangev[1]
        maxv <- rangev[2]
      } else {
        zeros <- 0
        minv <- NA
        maxv <- NA
      }

      if(type == 'basic'){
        list(
          type = typeof(x),
          class = paste(class(x), collapse = '/'),
          min = minv,
          max = maxv,
          n_valid = valid,
          perc_valid = valid / rows,
          n_zero = zeros,
          perc_zero = zeros / rows,
          n_nas = nas,
          perc_nas = nas / rows
        )
      } else if (type == 'complete'){
        list(
          type = typeof(x),
          class = paste(class(x), collapse = '/'),
          size = as.numeric(object.size(x)),
          min = minv,
          max = maxv,
          n_valid = valid,
          perc_valid = valid / rows,
          n_unique = uniq,
          perc_unique = uniq / rows,
          n_zero = zeros,
          perc_zero = zeros / rows,
          n_nas = nas,
          perc_nas = nas / rows
        )
      }
    })

    dt <- data.table::rbindlist(res)
    dt$var <- names(df)
    dt[, `:=`(rows = rows, cols = cols)]

  }

  if(type == 'basic'){
    setcolorder(
      dt,
      neworder = c(
        'var',
        'type',
        'class',
        'min',
        'max',
        'n_valid',
        'perc_valid',
        'n_zero',
        'perc_zero',
        'n_nas',
        'perc_nas',
        'rows',
        'cols')
    )
  } else if(type =='complete'){
    setcolorder(
      dt,
      neworder = c(
        'var',
        'type',
        'class',
        'size',
        'min',
        'max',
        'n_valid',
        'perc_valid',
        'n_unique',
        'perc_unique',
        'n_zero',
        'perc_zero',
        'n_nas',
        'perc_nas',
        'rows',
        'cols')
    )
  }
  return(dt)
}

f_num <- function(x, big = ',', dec = '.', thousand = 'K',
                  million = 'M', billion = 'B', dig = 0){

  if(x |> is.numeric()){
    fcase(
      is.infinite(x), paste(x),
      x > 1e9,
      paste(format(round(x/1e9, digits = dig), nsmall = dig,
                   decimal.mark = dec, big.mark = big, scientific = F), billion),
      x > 1e6,
      paste(format(round(x/1e6, digits = dig), nsmall = dig,
                   decimal.mark = dec, big.mark = big, scientific = F), million),
      x > 1e3,
      paste(format(round(x/1e3, digits = dig), nsmall = dig,
                   decimal.mark = dec, big.mark = big, scientific = F), thousand),
      default = format(round(x, dig), nsmall = dig, scientific = F)
    )
  } else {
    x
  }
}

f_int <- function(int){
  formatC(int, format = 'd', big.mark = ',', digits = 0)
}

gt_info <- function(df, df_name){
  stopifnot(is.data.frame(df))

  dt <- df |> as.data.table()

  dt[, f_icon := fcase(
    class == 'integer', '1',
    class == 'character', 'a',
    class == 'numeric', 'calculator',
    class %in% c('Date', 'IDate/Date', 'POSIXt', 'POSIXct', 'POSIXlt',
                 'POSIXct/POSIXt', 'POSIXlt/POSIXt'), 'calendar',
    class == 'factor', 'sitemap',
    class == 'raw', 'sd-card',
    class == 'complex', 'info',
    class == 'logical', 'puzzle-piece')]

  dt_gt <- dt[, `:=` (n_valid = f_num(n_valid, dig = 3),
                      n_zero = f_num(n_zero, dig = 3),
                      n_nas = f_num(n_nas, dig = 3))] |>
    gt()

  # if all NA do nothing
  if(!allNA(dt$min)){
    dt_gt <- dt_gt |>
      data_color(columns = 'min', palette = yl_palette)
  }

  # if all NA do nothing
  if(!allNA(dt$max)){
    dt_gt <- dt_gt |>
      data_color(columns = 'max', palette = pk_palette)
  }

  dt_gt |>
    tab_header(
      title = df_name,
      subtitle = paste(f_int(dt$rows[1]), 'rows -', f_int(dt$cols[1]), 'variables')
    ) |>
    cols_hide(columns = c('rows', 'cols')) |>
    fmt_percent(columns = c('perc_valid', 'perc_zero', 'perc_nas')) |>
    data_color(columns = 'n_valid', palette = lg_palette) |>
    data_color(columns = 'n_zero', palette = gray_palette) |>
    data_color(columns = 'n_nas', palette = red_palette) |>
    fmt_integer(columns = c('n_valid', 'n_nas')) |>
    fmt_number(columns = c('min', 'max', 'n_valid', 'n_zero', 'n_nas')) |>
    sub_missing() |>
    cols_move(columns = 'f_icon', after = 'var') |>
    fmt_icon(columns = 'f_icon') |>
    cols_label(
      var = 'Variable',
      type = 'Type',
      class = 'Class',
      min = 'Min',
      max = 'Max',
      n_valid = 'Valid',
      n_zero = 'Zeros',
      n_nas = "NA's",
      f_icon = ''
    ) |>
    cols_width(f_icon ~ px(30)) |>
    cols_merge(columns = c('class', 'f_icon'), pattern = "{2} {1}") |>
    cols_merge(columns = c('n_valid', 'perc_valid'), pattern = "{1} / {2}") |>
    cols_merge(columns = c('n_zero', 'perc_zero'), pattern = "{1} / {2}") |>
    cols_merge(columns = c('n_nas', 'perc_nas'), pattern = "{1} / {2}") |>
    tab_options(table.background.color = '#ffffff', heading.title.font.size = 20)
}

is_date <- function(x){
  inherits(x, c('Date', 'POSIXt', 'POSIXct', 'POSIXlt'))
}

is_categorical <- function(x) {
  is.character(x) || is.factor(x) || is.logical(x)
}

is_valid_name <- function(x){
  x == make.names(x)
}

is_name_available <- function(n, names){
  is_valid_name(n) && n %notin% names
}

is_var_available <- function(var_name, df){
  is_valid_name(var_name) && var_name %notin% names(df)
}

# wrapper functions from collapse package for speed ---------------------------
first_na <- function(x){
  ffirst(x, na.rm = F)
}

first_nona <- function(x){
  ffirst(x, na.rm = T)
}

last_na <- function(x){
  flast(x, na.rm = F)
}

last_nona <- function(x){
  flast(x, na.rm = T)
}

max_na <- function(x){
  fmax(x, na.rm = F)
}

max_nona <- function(x){
  fmax(x, na.rm = T)
}

mean_na <- function(x){
  fmean(x, na.rm = F)
}

mean_nona <- function(x){
  fmean(x, na.rm = T)
}

min_na <- function(x){
  fmin(x, na.rm = F)
}

min_nona <- function(x){
  fmin(x, na.rm = T)
}

sum_na <- function(x){
  fsum(x, na.rm = F)
}

sum_nona <- function(x){
  fsum(x, na.rm = T)
}

Try the spada package in your browser

Any scripts or data that you put into this service are public.

spada documentation built on July 10, 2026, 9:07 a.m.