library(shiny.i18n) # file with translations i18n <- Translator$new(translation_csvs_path = "../translations") # change this to zh i18n$set_translation_language("zh") ## based on https://github.com/rstudio/shiny/issues/1237 suppressWarnings( try( rm("registerShinyDebugHook", envir = as.environment("tools:rstudio")), silent = TRUE ) ) # options(shiny.trace = TRUE) # options(radiant.autosave = c(1, 5)) ## set volumes if sf_volumes was preset (e.g., on a server) or ## we are running in Rstudio or if we are running locally if (isTRUE(getOption("radiant.sf_volumes", "") != "") || isTRUE(getOption("radiant.shinyFiles", FALSE)) || isTRUE(Sys.getenv("RSTUDIO") != "") || isTRUE(Sys.getenv("SHINY_PORT") == "")) { if (isTRUE(getOption("radiant.sf_volumes", "") != "")) { sf_volumes <- getOption("radiant.sf_volumes") if (length(names(sf_volumes)) == 0) { warning("\nOption radiant.sf_volumes should be a named vector set in .Rprofile\n\n") options(radiant.sf_volumes = "") } else if (any(sapply(sf_volumes, function(x) !dir.exists(x)))) { warning("\nOne or more directories listed in option radiant.sf_volumes do not exists. Please fix the option in .Rprofile and restart radiant.\n\n") options(radiant.sf_volumes = "") } rm(sf_volumes) } if (isTRUE(getOption("radiant.sf_volumes", "") == "")) { sf_volumes <- c(Home = radiant.data::find_home()) if (dir.exists(paste0(sf_volumes["Home"], "/Desktop"))) { sf_volumes <- c(sf_volumes, Desktop = paste0(sf_volumes["Home"], "/Desktop")) } if (dir.exists(paste0(sf_volumes["Home"], "/Downloads"))) { sf_volumes <- c(sf_volumes, Downloads = paste0(sf_volumes["Home"], "/Downloads")) } Dropbox <- try(radiant.data::find_dropbox(), silent = TRUE) if (!inherits(Dropbox, "try-error")) { sf_volumes <- c(sf_volumes, Dropbox = Dropbox) } GoogleDrive <- try(radiant.data::find_gdrive(), silent = TRUE) if (!inherits(GoogleDrive, "try-error")) { sf_volumes <- c(sf_volumes, `Google Drive` = GoogleDrive) } sf_volumes <- c(sf_volumes, shinyFiles::getVolumes()()) options(radiant.sf_volumes = sf_volumes) } options(radiant.shinyFiles = TRUE) } else { options(radiant.shinyFiles = FALSE) } ## determining how radiant was launched ## should this be set in global? if (is.null(getOption("radiant.launch"))) { ## also use Rstudio's file dialog if opening in Window if (exists(".rs.readUiPref")) { if (is.null(.rs.readUiPref("shiny_viewer_type"))) { .rs.writeUiPref("shiny_viewer_type", 2) options(radiant.launch = "viewer") } else if (.rs.readUiPref("shiny_viewer_type") == 2) { options(radiant.launch = "viewer") } else if (.rs.readUiPref("shiny_viewer_type") == 3) { options(radiant.launch = "window") } else { # options(radiant.launch = "external") options(radiant.launch = "browser") } } else { options(radiant.launch = "browser") } } ## function to load/import required packages and functions import_fs <- function(ns, libs = c(), incl = c(), excl = c()) { tmp <- sapply(libs, library, character.only = TRUE) rm(tmp) if (length(incl) > 0 || length(excl) > 0) { import_list <- getNamespaceImports(ns) if (length(incl) == 0) { import_list[names(import_list) %in% c("base", "methods", "stats", "utils", libs, excl)] <- NULL } else { import_list <- import_list[names(import_list) %in% incl] } import_names <- names(import_list) for (i in seq_len(length(import_list))) { fun <- import_list[[i]] lib <- import_names[[i]] ## replace with character.only option when new version of import is posted to CRAN ## https://github.com/smbache/import/issues/11 eval( parse( text = paste0("import::from(", lib, ", '", paste0(fun, collapse = "', '"), "')") ) ) } } invisible() } ## list of function to suggest during autocomplete in Report > Rmd and Report > R ## moved to init.R init_data <- function(env = r_data) { ## Based on discussion with Joe Cheng: Datasets can change over time ## so the data needs to be reactive value so the other reactive ## functions and outputs that depend on these datasets will know when ## they are changed ## Using an environment to assign data ## http://adv-r.had.co.nz/Environments.html#explicit-envs ## using a reactiveValues list to keep track of relevant app info ## that needs to be reactive r_info <- reactiveValues() strip_ext <- function(x) sub(paste0("\\.", tools::file_ext(x), "$"), "", x) datasetlist <- c() df_names <- getOption("radiant.init.data") if (length(df_names) == 0) df_names <- c("diamonds", "titanic") for (dn in df_names) { if (file.exists(dn)) { df <- load(dn) %>% get() if (!inherits(df, "data.frame")) next # only keep data.frames dn_path <- dn dn <- basename(dn) %>% strip_ext() r_info[[paste0(dn, "_lcmd")]] <- glue::glue('{dn} <- load("{dn_path}") %>% get()\nregister("{dn}")') } else { df <- data(list = dn, package = "radiant.data", envir = environment()) %>% get() r_info[[paste0(dn, "_lcmd")]] <- glue::glue('{dn} <- data({dn}, package = "radiant.data", envir = environment()) %>% get()\nregister("{dn}")') } env[[dn]] <- df if (!bindingIsActive(as.symbol(dn), env = env)) { makeReactiveBinding(dn, env = env) } r_info[[paste0(dn, "_descr")]] <- attr(df, "description") datasetlist <- c(datasetlist, dn) } r_info[["datasetlist"]] <- datasetlist r_info[["url"]] <- NULL r_info } ## running local, on a server, or from JupyterLab if (getOption("radiant.jupyter", default = FALSE)) { options(radiant.local = FALSE) options(radiant.report = getOption("radiant.report", default = TRUE)) ## no limit to file size when launched through jupyter options(shiny.maxRequestSize = getOption("radiant.maxRequestSize", default = -1)) } else if (Sys.getenv("SHINY_PORT") == "") { options(radiant.local = TRUE) options(radiant.report = getOption("radiant.report", default = TRUE)) ## no limit to file size locally options(shiny.maxRequestSize = getOption("radiant.maxRequestSize", default = -1)) } else { options(radiant.local = FALSE) options(radiant.report = getOption("radiant.report", default = FALSE)) ## limit upload file size on server (10MB) options(shiny.maxRequestSize = getOption("radiant.maxRequestSize", default = 10 * 1024^2)) if (Sys.getlocale(category = "LC_ALL") == "C") { ret <- Sys.setlocale("LC_CTYPE", "en_US.UTF-8") rm(ret) } } ## encoding options(radiant.encoding = "UTF-8") ## hack for rmarkdown from Report > Rmd and Report > R options(radiant.rmarkdown = FALSE) ## path to use for local or server use options( radiant.path.data = ifelse(grepl("radiant.data", getwd()) && file.exists("../../inst"), "..", system.file(package = "radiant.data")) ) ## import required functions and packages ## if radiant.data is not in search main function from dplyr etc. won't be available if (!"package:radiant.data" %in% search() && # isTRUE(Sys.getenv("SHINY_PORT") == "") && isTRUE(getOption("radiant.development")) && getOption("radiant.path.data") == "..") { import_fs("radiant.data", libs = c("magrittr", "ggplot2", "lubridate", "tidyr", "dplyr", "broom", "tibble", "glue")) options(radiant.from.package = FALSE) } else { options(radiant.from.package = TRUE) library(radiant.data) } ## basic options when run on server if (getOption("width") != 250) { options( width = max(getOption("width"), 250), scipen = max(getOption("scipen"), 100), max.print = max(getOption("max.print"), 5000), stringsAsFactors = FALSE ) } options(dctrl = if (getRversion() > "3.4.4") c("keepNA", "niceNames") else "keepNA") options( radiant.functions = list( "样本量(n_obs)" = "n_obs", "缺失值数(n_missing)" = "n_missing", "唯一值数(n_distinct)" = "n_distinct", "均值(mean)" = "mean", "中位数(median)" = "median", "众数(modal)" = "modal", "最小值(min)" = "min", "最大值(max)" = "max", "总和(sum)" = "sum", "方差(var)" = "var", "标准差(sd)" = "sd", "标准误(se)" = "se", "误差边际(me)" = "me", "变异系数(cv)" = "cv", "比例(prop)" = "prop", "比例方差(varprop)" = "varprop", "比例标准差(sdprop)" = "sdprop", "比例标准误(seprop)" = "seprop", "比例误差边际(meprop)" = "meprop", "总体方差(varpop)" = "varpop", "总体标准差(sdpop)" = "sdpop", "1%分位数(p01)" = "p01", "2.5%分位数(p025)" = "p025", "5%分位数(p05)" = "p05", "10%分位数(p10)" = "p10", "25%分位数(p25)" = "p25", "75%分位数(p75)" = "p75", "90%分位数(p90)" = "p90", "95%分位数(p95)" = "p95", "97.5%分位数(p975)" = "p975", "99%分位数(p99)" = "p99", "偏度(skew)" = "skew", "峰度(kurtosi)" = "kurtosi", "四分位距(IQR)" = "IQR" ) ) ## see https://github.com/tidyverse/ggplot2/issues/2655 ## requires XQuartz! # if(!identical(getOption("bitmapType"), "cairo") && isTRUE(capabilities()[["cairo"]])) { # options(bitmapType = "cairo") # } ## for report and code in menu R knitr::opts_knit$set(progress = TRUE) knitr::opts_chunk$set( echo = FALSE, comment = NA, # fig.cap = "", cache = FALSE, message = FALSE, warning = FALSE, error = TRUE, # fig.path = normalizePath(tempdir(), winslash = "/"), dpi = 144, screenshot.force = FALSE # dev = "svg" ## too slow with big scatter plots on server-side ) ## environment to hold session information r_sessions <- new.env(parent = emptyenv()) ## create directory to hold session files "~/.radiant.sessions" %>% (function(x) if (!file.exists(x)) dir.create(x)) ## adding the figures path to avoid making a copy of all figures in www/figures addResourcePath("www", file.path(getOption("radiant.path.data"), "app/www/")) addResourcePath("figures", file.path(getOption("radiant.path.data"), "app/tools/help/figures/")) addResourcePath("imgs", file.path(getOption("radiant.path.data"), "app/www/imgs/")) addResourcePath("js", file.path(getOption("radiant.path.data"), "app/www/js/")) ## cdn.mathjax.org has been retired ## use local MathJax if available ## doesn't current work on Linux local_mathjax <- Sys.getenv("RMARKDOWN_MATHJAX_PATH") ## from https://github.com/rstudio/rmarkdown/blob/master/R/shiny.R if (Sys.info()["sysname"] != "Linux" && nzchar(local_mathjax)) { addResourcePath("latest", local_mathjax) options(radiant.mathjax.path = "latest") ## override shiny::withMathJax to use local MathJax withMathJax <- function(...) { path <- "latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML" tagList( tags$head(singleton(tags$script(src = path, type = "text/javascript"))), ..., tags$script(HTML("if (window.MathJax) MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);")) ) } } else { options(radiant.mathjax.path = "https://mathjax.rstudio.com/latest") } rm(local_mathjax) get_zip_info <- function() { flags <- "-r9X" zip_util <- Sys.getenv("R_ZIPCMD", "zip") if (Sys.info()["sysname"] == "Windows") { wz <- suppressWarnings(system("where zip", intern = TRUE)) if (!isTRUE(grepl("zip", wz))) { wz <- suppressWarnings(system("where 7z", intern = TRUE)) if (isTRUE(grepl("7z", wz))) { zip_util <- "7z" flags <- "a" if (Sys.getenv("R_ZIPCMD") == "") { Sys.setenv(R_ZIPCMD = wz) } } else { zip_util <- "" flags <- "" } } else { if (Sys.getenv("R_ZIPCMD") == "") { Sys.setenv(R_ZIPCMD = wz) } } } options(radiant.zip = c(flags, zip_util)) } get_zip_info() rm(get_zip_info) ## function to generate help, must be in global because used in ui.R help_menu <- function(hlp) { tagList( navbarMenu( "", icon = icon("question-circle", verify_fa = FALSE), # tabPanel(i18n$t("Help"), uiOutput(hlp), icon = icon("question", verify_fa = FALSE)), tabPanel(actionLink("help_keyboard", i18n$t("Keyboard shortcuts"), icon = icon("keyboard", verify_fa = FALSE))) #tabPanel("Videos", uiOutput("help_videos"), icon = icon("film")), # tabPanel(tags$a( # "", # href = "https://radiant-rstats.github.io/docs/tutorials.html", target = "_blank", # list(icon("film", verify_fa = FALSE), i18n$t("Videos")) # )), # tabPanel(i18n$t("About"), uiOutput("help_about"), icon = icon("info", verify_fa = FALSE)), # tabPanel(tags$a( # "", # href = "https://radiant-rstats.github.io/docs/", target = "_blank", # list(icon("globe", verify_fa = FALSE), i18n$t("Radiant docs")) # )), # tabPanel(tags$a( # "", # href = "https://github.com/radiant-rstats/radiant/issues", target = "_blank", # list(icon("github", verify_fa = FALSE), i18n$t("Report issue")) # )) ), # bslib::nav_item(checkboxInput("dark_mode", label = "Dark Mode", width="100px")), tags$head( tags$script(src = "js/session.js"), tags$script(src = "js/returnTextAreaBinding.js"), tags$script(src = "js/returnTextInputBinding.js"), tags$script(src = "js/video_reset.js"), tags$script(src = "js/run_return.js"), # tags$script('TogetherJSConfig_hubBase = "https://togetherjs-hub.glitch.me/"; TogetherJSConfig_cloneClicks = true;'), # tags$script(src = "https://togetherjs.com/togetherjs-min.js"), # tags$script(src = "js/message-handler.js"), # tags$script(src = "js/draggable_modal.js"), tags$link(rel = "stylesheet", type = "text/css", href = "www/style.css"), tags$link(rel = "shortcut icon", href = "imgs/icon.png") ) ) } ## copy-right text options(radiant.help.cc = "© Vincent Nijs (2023) Creative Commons License
") options(radiant.help.cc.sa = "© Vincent Nijs (2023) Creative Commons License
") ##################################### ## url processing to share results ##################################### ## relevant links # http://stackoverflow.com/questions/25306519/shiny-saving-url-state-subpages-and-tabs/25385474#25385474 # https://groups.google.com/forum/#!topic/shiny-discuss/Xgxq08N8HBE # https://gist.github.com/jcheng5/5427d6f264408abf3049 ## try http://127.0.0.1:3174/?url=multivariate/conjoint/plot/&SSUID=local options( radiant.url.list = list("Data" = list("tabs_data" = list( "Manage" = "data/", "View" = "data/view/", "Visualize" = "data/visualize/", "Pivot" = "data/pivot/", "Explore" = "data/explore/", "Transform" = "data/transform/", "Combine" = "data/combine/", "Rmd" = "report/rmd/", "R" = "report/r/" ))) ) make_url_patterns <- function(url_list = getOption("radiant.url.list"), url_patterns = list()) { for (i in names(url_list)) { res <- url_list[[i]] if (!is.list(res)) { url_patterns[[res]] <- list("nav_radiant" = i) } else { tabs <- names(res) for (j in names(res[[tabs]])) { url <- res[[tabs]][[j]] url_patterns[[url]] <- setNames(list(i, j), c("nav_radiant", tabs)) } } } url_patterns } ## generate url patterns options(radiant.url.patterns = make_url_patterns()) ## installed packages versions tmp <- grep("radiant\\.", installed.packages()[, "Package"], value = TRUE) if ("radiant" %in% installed.packages()) { tmp <- c("radiant" = "radiant", tmp) } if (length(tmp) > 0) { radiant.versions <- sapply(names(tmp), function(x) paste(x, paste(packageVersion(x), sep = "."))) %>% unique() if ("shiny" %in% installed.packages()) { radiant.versions <- c(radiant.versions, paste("shiny ", packageVersion("shiny"))) } } else { radiant.versions <- "Unknown" } options(radiant.versions = paste(radiant.versions, collapse = ", ")) rm(tmp, radiant.versions) if (is.null(getOption("radiant.theme", default = NULL))) { options(radiant.theme = bslib::bs_theme(version = 4)) } ## bslib theme and version has_bslib_theme <- function() { if (rlang::is_installed("bslib")) bslib::is_bs_theme(getOption("radiant.theme")) else FALSE } bslib_current_version <- function() { if (rlang::is_installed("bslib")) bslib::theme_version(getOption("radiant.theme", default = bslib::bs_theme(version = 4))) } navbar_proj <- function(navbar) { pdir <- radiant.data::find_project(mess = FALSE) if (radiant.data::is.empty(pdir)) { if (getOption("radiant.shinyFiles", FALSE) && !radiant.data::is.empty(getOption("radiant.sf_volumes", ""))) { proj <- paste0(i18n$t("Base dir: "), names(getOption("radiant.sf_volumes"))[1]) } else { proj <- "Project: (None)" } options(radiant.project_dir = NULL) } else { proj <- paste0("Project: ", basename(pdir)) %>% { if (nchar(.) > 35) paste0(strtrim(., 31), " ...") else . } options(radiant.project_dir = pdir) options(radiant.launch_dir = pdir) } proj_brand <- tags$span(class = "nav navbar-brand navbar-right", proj) navbar_ <- htmltools::tagQuery(navbar)$find(".navbar-collapse")$append(proj_brand)$allTags() htmltools::attachDependencies(navbar_, htmltools::findDependencies(navbar)) } if (getOption("radiant.shinyFiles", FALSE)) { if (!radiant.data::is.empty(getOption("radiant.sf_volumes", "")) && radiant.data::is.empty(getOption("radiant.project_dir"))) { launch_dir <- getOption("radiant.launch_dir", default = radiant.data::find_home()) if (!launch_dir %in% getOption("radiant.sf_volumes", "")) { sf_volumes <- c(setNames(launch_dir, basename(launch_dir)), getOption("radiant.sf_volumes", "")) options(radiant.sf_volumes = sf_volumes) rm(sf_volumes) } else if (!launch_dir == getOption("radiant.sf_volumes", "")[1]) { dir_ind <- which(getOption("radiant.sf_volumes") == launch_dir)[1] options(radiant.sf_volumes = c(getOption("radiant.sf_volumes")[dir_ind], getOption("radiant.sf_volumes")[-dir_ind])) rm(dir_ind) } rm(launch_dir) } if (radiant.data::is.empty(getOption("radiant.launch_dir"))) { if (radiant.data::is.empty(getOption("radiant.project_dir"))) { options(radiant.launch_dir = radiant.data::find_home()) options(radiant.project_dir = getOption("radiant.launch_dir")) } else { options(radiant.launch_dir = getOption("radiant.project_dir")) } } if (radiant.data::is.empty(getOption("radiant.project_dir"))) { options(radiant.project_dir = getOption("radiant.launch_dir")) } else { options(radiant.launch_dir = getOption("radiant.project_dir")) } dbdir <- try(radiant.data::find_dropbox(), silent = TRUE) dbdir <- if (inherits(dbdir, "try-error")) "" else paste0(dbdir, "/") options(radiant.dropbox_dir = dbdir) rm(dbdir) gddir <- try(radiant.data::find_gdrive(), silent = TRUE) gddir <- if (inherits(gddir, "try-error")) "" else paste0(gddir, "/") options(radiant.gdrive_dir = gddir) rm(gddir) } else { options(radiant.launch_dir = radiant.data::find_home()) options(radiant.project_dir = getOption("radiant.launch_dir")) } ## formatting data.frames printed in Report > Rmd and Report > R knit_print.data.frame <- function(x, ...) { paste(c("", "", knitr::kable(x)), collapse = "\n") %>% knitr::asis_output() } ## not clear why this doesn't work # knit_print.data.frame = function(x, ...) { # res <- rmarkdown:::print.paged_df(x) # knitr::asis_output(res) # knitr::asis_output( # rmarkdown:::paged_table_html(x), # meta = list(dependencies = rmarkdown:::html_dependency_pagedtable()) # ) # } ## not clear why this doesn't work ## https://github.com/yihui/knitr/issues/1399 # knit_print.datatables <- function(x, ...) { # res <- shiny::knit_print.shiny.render.function( # DT::renderDataTable(x) # ) # knitr::asis_output(res) # } # registerS3method("knitknit_print", "datatables", knit_print.datatables) # knit_print.datatables <- function(x, ...) { # # res <- shiny::knit_print.shiny.render.function( # # shiny::knit_print.shiny.render.function( # DT::renderDataTable(x) # # ) # # knitr::asis_output(res) # } # knit_print.datatables <- function(x, ...) { # # shiny::knit_print.shiny.render.function( # res <- shiny::knit_print.shiny.render.function( # DT::renderDataTable(x) # ) # knitr::asis_output(res) # } load_html2canvas <- function() { # adapted from https://github.com/yonicd/snapper/blob/master/R/load.R # SRC URL "https://html2canvas.hertzen.com/dist/html2canvas.min.js" asset_src <- "assets/html2canvas/" asset_script <- "html2canvas.min.js" dir.exists(asset_src) shiny::tagList( htmltools::htmlDependency( name = "html2canvas-js", version = "1.4.1", src = asset_src, script = asset_script, package = "radiant.data" ) ) } options( radiant.nav_ui = list( windowTitle = i18n$t("Radiant for R"), id = "nav_radiant", theme = getOption("radiant.theme"), inverse = TRUE, collapsible = TRUE, position = "fixed-top", tabPanel(i18n$t("Data"), withMathJax(), uiOutput("ui_data"), load_html2canvas()) ) ) options( radiant.shared_ui = tagList( navbarMenu( i18n$t("Report"), tabPanel("Rmd", uiOutput("rmd_view"), uiOutput("report_rmd"), icon = icon("edit", verify_fa = FALSE) ), tabPanel("R", uiOutput("r_view"), uiOutput("report_r"), icon = icon("code", verify_fa = FALSE) ) ), navbarMenu("", icon = icon("save", verify_fa = FALSE), ## inspiration for uploading state https://stackoverflow.com/a/11406690/1974918 ## see also function in www/js/run_return.js "Server", tabPanel(actionLink("state_save_link", i18n$t("Save radiant state file"), icon = icon("download", verify_fa = FALSE))), tabPanel(actionLink("state_load_link", i18n$t("Load radiant state file"), icon = icon("upload", verify_fa = FALSE))), tabPanel(actionLink("state_share", i18n$t("Share radiant state"), icon = icon("share", verify_fa = FALSE))), tabPanel(i18n$t("View radiant state"), uiOutput("state_view"), icon = icon("user", verify_fa = FALSE)), "----", "Local", tabPanel(downloadLink("state_download", tagList(icon("download", verify_fa = FALSE), i18n$t("Download radiant state file")))), tabPanel(actionLink("state_upload_link", i18n$t("Upload radiant state file"), icon = icon("upload", verify_fa = FALSE))) ), ## stop app *and* close browser window navbarMenu("", icon = icon("power-off", verify_fa = FALSE), tabPanel( actionLink( "stop_radiant", i18n$t("Stop"), icon = icon("stop", verify_fa = FALSE), onclick = "setTimeout(function(){window.close();}, 100);" ) ), tabPanel(tags$a( id = "refresh_radiant", href = "#", class = "action-button", list(icon("sync", verify_fa = FALSE), i18n$t("Refresh")), onclick = "window.location.reload();" )), ## had to remove class = "action-button" to make this work tabPanel(tags$a( id = "new_session", href = "./", target = "_blank", list(icon("plus", verify_fa = FALSE), i18n$t("New session")) )) ) ) ) ## cleanup the global environment if stop button is pressed in Rstudio ## based on barbara's reply to ## https://community.rstudio.com/t/rstudio-viewer-window-not-closed-on-shiny-stopapp/4158/7?u=vnijs onStop(function() { ## don't run if the stop button was pressed in Radiant if (!exists("r_data")) { unlink("~/r_figures/", recursive = TRUE) clean_up_list <- c( "r_sessions", "help_menu", "make_url_patterns", "import_fs", "init_data", "navbar_proj", "knit_print.data.frame", "withMathJax", "Dropbox", "sf_volumes", "GoogleDrive", "bslib_current_version", "has_bslib_theme", "load_html2canvas" ) suppressWarnings( suppressMessages({ res <- try(sapply(clean_up_list, function(x) if (exists(x, envir = .GlobalEnv)) rm(list = x, envir = .GlobalEnv)), silent = TRUE) rm(res) }) ) options(radiant.launch_dir = NULL) options(radiant.project_dir = NULL) message("Stopped Radiant\n") stopApp() } }) ## Show NA and Inf in DT tables ## https://github.com/rstudio/DT/pull/513 ## See also https://github.com/rstudio/DT/issues/533 ## Waiting for DT.OPTION for TOJSON_ARGS # options(htmlwidgets.TOJSON_ARGS = list(na = "string")) # options("DT.TOJSON_ARGS" = list(na = "string")) ## Sorting on client-side would be as a string, not a numeric ## https://github.com/rstudio/DT/pull/536#issuecomment-385223433 if (getRversion() < "4.4.0") `%||%` <- function(x, y) if (is.null(x)) y else x