How to use htmltools::attachDependencies? - r

Here is an example of how I "manually" add a HTML dependency to a datatable. This works fine. When I try with attachDependencies, the dependency is not attached.
library(DT)
library(htmltools)
dep <- htmlDependency(
name = "colResize",
version = "1.6.1",
src = normalizePath("colResize"),
script = "jquery.dataTables.colResize.js",
stylesheet = "jquery.dataTables.colResize.css",
all_files = FALSE
)
dat <- iris
datatable(
dat,
options = list(
colResize = list()
)
) %>% attachDependencies(dep, append = TRUE)
Why does this code not work?

I don't think it has anything to do with the way you're using it. The function attachDependencies() adds the object as an attribute, not as a dependency:
function (x, value, append = FALSE)
{
value <- asDependencies(value)
if (append) {
old <- attr(x, "html_dependencies", TRUE)
htmlDependencies(x) <- c(old, value)
}
else { htmlDependencies(x) <- value }
return(x)
}
However, without this function, you could still add your dependency in one line instead of three. I know that's not what you're looking for, but it's an option.
dtable$dependencies <- append(dtable$dependencies, list(dep))
With my example:
dep <- htmlDependency(
name = "colResize", version = "1.6.1",
src = c(href = "https://cdn.jsdelivr.net/gh/dhobi/datatables.colResize/"),
script = "jquery.dataTables.colResize.js",
stylesheet = "jquery.dataTables.colResize.css",
all_files = FALSE)
dtable <- datatable(iris, options = list(colResize = list()))
dtable$dependencies <- append(dtable$dependencies, list(dep))
dtable

Related

Add / superimpose CSS to shiny app on the fly when running the app

I want to run a local shiny app, for example with shinyAppDir. I have a CSS file that I want to add to the app "on the fly". I want to avoid changing the app.R file by adding the CSS manually, but instead somehow superimpose the CSS when running shinyAppDir.
Are there any existing options or packages that have this kind of functionality? Maybe {golem}? Or would I need to read in the source file, add the needed code via regex and then run the app (which seems to be a very ugly workaround)?
Here is a minimal example:
Lets say this is my app:
library(shiny)
shinyApp(ui = fluidPage(
sliderInput("bins", "Number of bins:", min = 1, max = 50, value = 30)
),
server = function(input, output) {}
)
And this would be the CSS file called custom.css. This CSS code should be integrated into the app when it is called:
.control-label {
color: #ff0000;
}
I’d like to call this app with a function like shinyAppDir. Any other function that allows this kind of argument is fine as well.
shinyAppDir(
file.path("/somepath/goeshere/"),
options=list(
add_css = "custom.css" # this argument does not exist
)
)
The result should be the same as:
library(shiny)
shinyApp(ui = fluidPage(
tags$head(
tags$style(HTML("
.control-label {
color: #ff0000;
}"))
),
sliderInput("bins", "Number of bins:", min = 1, max = 50, value = 30)
),
server = function(input, output) { }
)
I found one way to do it by rewriting the shiny:::sourceUTF8 function:
# this is the function that needs to be rewritten
dressSourceUTF8 <- function (file, css_string, envir = globalenv()) {
lines <- shiny:::readUTF8(file)
enc <- if (any(Encoding(lines) == "UTF-8")) "UTF-8" else "unknown"
src <- srcfilecopy(file, lines, isFile = TRUE)
if (shiny:::isWindows() && enc == "unknown") {
file <- tempfile()
on.exit(unlink(file), add = TRUE)
writeLines(lines, file)
}
exprs <- try(parse(file, keep.source = FALSE, srcfile = src,
encoding = enc))
## this part is new ##
if (!is.null(css_string)) {
idx <- vapply(exprs,
FUN = function(x) grepl("^shinyApp", x[1], perl = TRUE),
FUN.VALUE = logical(1))
# if ui argument is unnamed
if (is.null((exprs[idx][[1]][["ui"]]))) {
ui_idx <- 2
# if named
} else {
ui_idx <- "ui"
}
ui_len <- length(exprs[idx][[1]][[ui_idx]])
# workaround for `append`
for (i in seq_len(ui_len)[-1]){
exprs[idx][[1]][[ui_idx]][[1 + i]] <- exprs[idx][[1]][[ui_idx]][[i]]
}
exprs[idx][[1]][[ui_idx]][[2]] <- bquote(tags$style(.(css_string)))
}
## rest unchanged ##
if (inherits(exprs, "try-error")) {
shiny:::diagnoseCode(file)
stop("Error sourcing ", file)
}
exprs <- shiny:::makeCall(`{`, exprs)
exprs <- shiny:::makeCall(..stacktraceon.., list(exprs))
eval(exprs, globalenv())
}
Then we need to update all functions up the tree:
dressShinyAppDir <- function(appDir, css_string = NULL, options = list()) {
if (!utils::file_test("-d", appDir)) {
stop("No Shiny application exists at the path \"", appDir,
"\"")
}
appDir <- normalizePath(appDir, mustWork = TRUE)
if (shiny:::file.exists.ci(appDir, "server.R")) {
shiny:::shinyAppDir_serverR(appDir, options = options)
}
else if (shiny:::file.exists.ci(appDir, "app.R")) {
# for now this only works for shinyApp files:
dressShinyAppDir_appR("app.R", appDir, .css_string = css_string, options = options)
}
else {
stop("App dir must contain either app.R or server.R.")
}
}
dressShinyAppDir_appR <- function (fileName, appDir, .css_string, options = list()) {
fullpath <- shiny:::file.path.ci(appDir, fileName)
if (getOption("shiny.autoload.r", TRUE)) {
sharedEnv <- new.env(parent = globalenv())
}
else {
sharedEnv <- globalenv()
}
appObj <- shiny:::cachedFuncWithFile(appDir, fileName, case.sensitive = FALSE,
function(appR) {
# here the new sourceUTF8 function is added, the rest is unchanced:
result <- dressSourceUTF8(fullpath, css_string = .css_string, envir = new.env(parent = sharedEnv))
if (!is.shiny.appobj(result))
stop("app.R did not return a shiny.appobj object.")
shiny:::unconsumeAppOptions(result$appOptions)
return(result)
})
dynHttpHandler <- function(...) {
appObj()$httpHandler(...)
}
dynServerFuncSource <- function(...) {
appObj()$serverFuncSource(...)
}
wwwDir <- shiny:::file.path.ci(appDir, "www")
if (shiny:::dirExists(wwwDir)) {
staticPaths <- list(`/` = httpuv::staticPath(wwwDir, indexhtml = FALSE,
fallthrough = TRUE))
}
else {
staticPaths <- list()
}
fallbackWWWDir <- system.file("www-dir", package = "shiny")
oldwd <- NULL
monitorHandle <- NULL
onStart <- function() {
oldwd <<- getwd()
setwd(appDir)
if (getOption("shiny.autoload.r", TRUE)) {
shiny:::loadSupport(appDir, renv = sharedEnv, globalrenv = NULL)
}
if (!is.null(appObj()$onStart))
appObj()$onStart()
monitorHandle <<- shiny:::initAutoReloadMonitor(appDir)
invisible()
}
onStop <- function() {
setwd(oldwd)
if (is.function(monitorHandle)) {
monitorHandle()
monitorHandle <<- NULL
}
}
structure(list(staticPaths = staticPaths, httpHandler = shiny:::joinHandlers(c(dynHttpHandler,
wwwDir, fallbackWWWDir)), serverFuncSource = dynServerFuncSource,
onStart = onStart, onStop = onStop, options = options),
class = "shiny.appobj")
}
This allows us to do the following:
dressShinyAppDir(
file.path("/somepath/here"),
css_string = ".control-label {color: #00ff00;}"
)
The app will be called and the CSS string in css_string will be added inline.

How can I add a baseplot with no fixed values to a document

I want to add a baseplot to a word document.
In the documentation for the officer package there's an example that uses the plot_instr function:
anyplot <- plot_instr(code = {
barplot(1:5, col = 2:6)
})
doc <- read_docx()
doc <- body_add(doc, anyplot, width = 5, height = 4)
print(doc, target = tempfile(fileext = ".docx"))
I want to add a plot to a word document inside a function so I need variable input for the plot function like this:
x=1:5
cols=2:6
anyplot <- plot_instr(code = {
barplot(x,col=cols)
})
doc <- read_docx()
doc <- body_add(doc, anyplot, width = 5, height = 4)
print(doc, target = tempfile(fileext = ".docx"))
But the code above doesn't work and I can't find any other examples of plot_instr usage.
I think I found a solution!
When I set a breakpoint before the barplot(...) call, I could see the code when body_add is called with a plot_instr wrapper function. The code creates a temporary png file. I copied this code and adapted it:
x=1:5
cols=2:6
doc <- read_docx()
file <- tempfile(fileext = ".png")
options(bitmapType = "cairo")
png(filename = file, width = 5, height = 5, units = "in", res = 300)
tryCatch({
barplot(x,col=cols)
}, finally = {
dev.off()
})
on.exit(unlink(file))
value <- external_img(src = file, width = 5, height = 5)
body_add(doc, value)
print(doc, target = tempfile(fileext = ".docx"))
The code is generating the following error
Error in barplot.default(x, col = cols) :
'height' must be a vector or a matrix
The issue here is that function body_add has an argument x and you are defining an x before the call. Changing the name to something else solves the issue:
z <- 1:5
cols=2:6
anyplot <- plot_instr(code = {
barplot(z, col = cols)
})
doc <- read_docx()
doc <- body_add(doc, anyplot, width = 5, height = 4)
print(doc, target = "temp.docx")

How to modify pre-existing function in local environment in R

I am trying to modify an existing function by copy and pasting it to an R script, and assigning it to a new function object in my local environment. However the new function cannot find functions that are called to within the original function. How can I fix this without looking up and finding each function individually? I am guessing that the original function is somehow linked to the package or its dependencies and 'knows where to look' for the missing function, but I cannot figure out how to do this with my new copy-and-pasted function.
library("camtrapR")
Print the function name
activityDensity
The output here is the code for this function. I have omitted it here because it is long (and I have pasted it below), but I copy and paste the output of the function code exactly (see below where I assign this exact code to a new function), except for the last two lines of output, which I think are important:
<bytecode: 0x000000002a2d1e20>
<environment: namespace:camtrapR>
So now I assign the copy and pasted code from the output above to a new function with New <-
New <- function (recordTable, species, allSpecies = FALSE, speciesCol = "Species",
recordDateTimeCol = "DateTimeOriginal", recordDateTimeFormat = "%Y-%m-%d %H:%M:%S",
plotR = TRUE, writePNG = FALSE, plotDirectory, createDir = FALSE,
pngMaxPix = 1000, add.rug = TRUE, ...)
{
wd0 <- getwd()
mar0 <- par()$mar
on.exit(setwd(wd0))
on.exit(par(mar = mar0), add = TRUE)
recordTable <- dataFrameTibbleCheck(df = recordTable)
timeZone <- "UTC"
checkForSpacesInColumnNames(speciesCol = speciesCol, recordDateTimeCol = recordDateTimeCol)
if (!is.data.frame(recordTable))
stop("recordTable must be a data frame", call. = FALSE)
if (!speciesCol %in% colnames(recordTable))
stop(paste("speciesCol = \"", speciesCol, "\" is not a column name in recordTable",
sep = ""), call. = FALSE)
if (!recordDateTimeCol %in% colnames(recordTable))
stop(paste("recordDateTimeCol = \"", recordDateTimeCol,
"\" is not a column name in recordTable", sep = ""),
call. = FALSE)
stopifnot(is.logical(c(allSpecies, writePNG, plotR, createDir)))
if (allSpecies == FALSE) {
stopifnot(species %in% recordTable[, speciesCol])
stopifnot(hasArg(species))
}
recordTable$DateTime2 <- parseDateTimeObject(inputColumn = recordTable[,
recordDateTimeCol], dateTimeFormat = recordDateTimeFormat,
timeZone = timeZone)
recordTable$Time2 <- format(recordTable$DateTime2, format = "%H:%M:%S",
usetz = FALSE)
recordTable$Time.rad <- (as.numeric(as.POSIXct(strptime(recordTable$Time2,
format = "%H:%M:%S", tz = timeZone))) - as.numeric(as.POSIXct(strptime("0",
format = "%S", tz = timeZone))))/3600 * (pi/12)
if (isTRUE(writePNG)) {
if (hasArg(plotDirectory)) {
if (isTRUE(createDir)) {
dir.create(plotDirectory, recursive = TRUE, showWarnings = FALSE)
setwd(plotDirectory)
}
else {
stopifnot(file.exists(plotDirectory))
setwd(plotDirectory)
}
}
else {
stop("writePNG is TRUE. Please set plotDirectory",
call. = FALSE)
}
}
pngWidth <- pngMaxPix
pngHeight <- round(pngMaxPix * 0.8)
if (allSpecies == FALSE) {
subset_species <- subset(recordTable, recordTable[, speciesCol] ==
species)
if (nrow(subset_species) == 1)
stop(paste(species, "had only 1 record. Cannot estimate density."),
call. = FALSE)
try_error_tmp <- try({
if (isTRUE(writePNG))
png(filename = paste("activity_density_",
species, "_", Sys.Date(), ".png",
sep = ""), width = pngWidth, height = pngHeight,
units = "px", res = 96, type = "cairo")
if (isTRUE(writePNG) | isTRUE(plotR)) {
densityPlot(subset_species$Time.rad, main = paste("Activity of",
species), rug = add.rug, ...)
mtext(paste("number of records:", nrow(subset_species)),
side = 3, line = 0)
}
if (isTRUE(writePNG))
dev.off()
}, silent = TRUE)
if (class(try_error_tmp) == "try-error")
warning(paste(toupper(species), ": ", try_error_tmp[1],
" - SKIPPED", sep = ""), call. = FALSE)
}
else {
subset_species_list <- list()
for (i in 1:length(unique(recordTable[, speciesCol]))) {
spec.tmp <- unique(recordTable[, speciesCol])[i]
subset_species <- subset(recordTable, recordTable[,
speciesCol] == spec.tmp)
plot_main_title <- paste("Activity of", spec.tmp)
if (nrow(subset_species) == 1) {
warning(paste(toupper(spec.tmp), ": It had only 1 record. Cannot estimate density. - SKIPPED",
sep = ""), call. = FALSE)
next
}
else {
try_error_tmp <- try({
if (isTRUE(writePNG))
png(filename = paste("activity_density_",
spec.tmp, "_", Sys.Date(), ".png",
sep = ""), width = pngWidth, height = pngHeight,
units = "px", res = 96, type = "cairo")
if (isTRUE(writePNG) | isTRUE(plotR)) {
densityPlot(subset_species$Time.rad, main = plot_main_title,
rug = add.rug, ...)
mtext(paste("number of records:", nrow(subset_species)),
side = 3, line = 0)
}
if (isTRUE(writePNG))
dev.off()
}, silent = TRUE)
if (class(try_error_tmp) == "try-error")
warning(paste(toupper(spec.tmp), ": ",
try_error_tmp[1], " - SKIPPED",
sep = ""), call. = FALSE)
}
subset_species_list[[i]] <- subset_species$Time.rad
names(subset_species_list)[i] <- spec.tmp
}
}
if (allSpecies == FALSE) {
return(invisible(subset_species$Time.rad))
}
else {
return(invisible(subset_species_list))
}
}
Yet, when I try to run this new function (arguments omitted here for clarity), it can't find a function embedded within.
How can I somehow assign this function to look within the original package camtrapR for any dependencies, etc.? and why does the code output from the function not already do this?
New()
Error in dataFrameTibbleCheck(df = recordTable) :
could not find function "dataFrameTibbleCheck"
This answer here: https://stackoverflow.com/a/49277036/9096420 allows one to manually edit and save a function's code for each R session, but it is non-reproducible (not code) that can be shared or re-used.
If New is the new function copied from camtrapR then use
environment(New) <- asNamespace("camtrapR")
to ensure that the function calls in its body are looked up in the correct places.

Only Table in rpivotTable

I'm using the rpivotTable package in Shiny application and I'd like to have only the choice of 'Table' for the users (no charts)
The RenderName argument is only used to choose the default display...
output$pivot <- renderRpivotTable(
rpivotTable(iris,
rendererName = "Table" )
)
Many thanks in advance !
There are multiple issues here.
you can specify renderers via the anonymos renderers argument in rpivotTable(). I have the JS code form here.
however, there is a bug when only selecting one option. In this case, rpivotTable() wraps the argument in a list again (see the Map() call in the original function code) and the forwarding to JS fails.
Therefore, I accounted for this issue and extended the function a bit. Play around with aggregators/renderers to see how it behaves differently to the original rpivotTable() function.
# define own function
my_rpivotTable <- function (data, rows = NULL, cols = NULL, aggregatorName = NULL,
vals = NULL, rendererName = NULL, sorter = NULL, exclusions = NULL,
inclusions = NULL, locale = "en", subtotals = FALSE, ...,
width = 800, height = 600, elementId = NULL)
{
if (length(intersect(class(data), c("data.frame", "data.table",
"table", "structable", "ftable"))) == 0) {
stop("data should be a data.frame, data.table, or table",
call. = F)
}
if (length(intersect(c("table", "structable", "ftable"),
class(data))) > 0)
data <- as.data.frame(data)
params <- list(rows = rows, cols = cols, aggregatorName = aggregatorName,
vals = vals, rendererName = rendererName, sorter = sorter,
...)
params <- Map(function(p) {
# added to the class check -------------------------------------------------
if (length(p) == 1 && class(p[[1]]) != "JS_EVAL") {
p = list(p)
}
return(p)
}, params)
par <- list(exclusions = exclusions, inclusions = inclusions)
params <- c(params, par)
params <- Filter(Negate(is.null), params)
x <- list(data = data, params = params, locale = locale,
subtotals = subtotals)
htmlwidgets::createWidget(name = "rpivotTable", x, width = width,
height = height, elementId = elementId, package = "rpivotTable")
}
# create the pivot table
my_rpivotTable(
expand.grid(LETTERS, 1:3),
aggregatorName = "Count",
aggregators = list(Sum = htmlwidgets::JS('$.pivotUtilities.aggregators["Sum"]'),
Count = htmlwidgets::JS('$.pivotUtilities.aggregators["Count"]')),
rendererName = "fancyTable",
renderers = list(fancyTable = htmlwidgets::JS('$.pivotUtilities.renderers["Table"]'))
)

Optional argument parsing in R for working directory

I am parsing argument in a rscript (merge_em.r) below. Let's say I run the code below using commandline Rscript merge_em.r dataframe1, dataframe2 which gives me this error: Error in setwd(working.dir) : character argument expected. I want to keep working directory argument optional. How do I do it?
library("argparse")
merge_em <- function (x, y, working.dir){
mergedfile <- merge (x, y, by = intersect(names(x), names(y)))
if (missing(working.dir)) {
print ("Working directory not specified! Will use present working directory.")
working.dir <- as.character(file.path(getwd()))
} else {
working.dir <- working.dir
}
setwd(working.dir)
write.table (mergedfile, "merged.txt",
col.names = FALSE,
row.names = FALSE,
sep = "\t",
quote = FALSE
)
}
main <- function() {
# breaks if you set warn = 2
options(error = traceback,
warn = 1)
parser <- ArgumentParser(prog = "merge_em.r",
description = "Merge dataframes")
parser <- ArgumentParser()
parser$add_argument("x")
parser$add_argument("y")
parser$add_argument(
"--working_dir",
dest = "working.dir",
type = "character",
metavar = "DIR",
required = FALSE,
help = "Working directory where files are present"
)
args <- parser$parse_args()
working.dir <- args$working.dir
x <- args$x
if (!R.utils::isAbsolutePath(x))
x <- file.path(working.dir, x)
y <- args$y
if (!R.utils::isAbsolutePath(y))
y <- file.path(working.dir, y)
tryCatch(
merge_em (x, y, working.dir)
,
finally = setwd(working.dir)
)
}
main()
You could exchange the missing() conditional to this:
if (working.dir=="") {
print ("Working directory not specified! Will use present working directory.")
working.dir <- as.character(file.path(getwd()))
} else {
print ("Working directory is specified!")
working.dir <- working.dir
}
And change the argument for working_dir to (default=""):
parser$add_argument(
"--working_dir",
dest = "working.dir",
type = "character",
metavar = "DIR",
default="",
required = FALSE,
help = "Working directory where files are present"
)
And change the tryCatch to:
tryCatch(merge_em(x, y, working.dir), finally = print("Fin"))
Why are you using setwd() io the finally part? If the argument is not given, there is nothing to set or?
Like that you can call the script like this, for example:
Rscript merge_em.r data_frame1, data_frame2
Or with a directory:
Rscript merge_em.r data_frame1, data_frame2, --working_dir "path_to_folder"
Full code:
library(argparse)
merge_em <- function (x, y, working.dir){
mergedfile <- merge (x, y, by = intersect(names(x), names(y)))
if (working.dir=="") {
print ("Working directory not specified! Will use present working directory.")
working.dir <- as.character(file.path(getwd()))
} else {
print ("Working directory is specified!")
working.dir <- working.dir
}
setwd(working.dir)
write.csv(x = mergedfile, file = "merged.txt",
row.names = FALSE,
quote = FALSE
)
}
main <- function() {
# breaks if you set warn = 2
options(error = traceback,
warn = 1)
parser <- ArgumentParser(prog = "merge_em.r",
description = "Merge dataframes")
parser <- ArgumentParser()
parser$add_argument("x")
parser$add_argument("y")
parser$add_argument(
"--working_dir",
dest = "working.dir",
type = "character",
metavar = "DIR",
default="",
required = FALSE,
help = "Working directory where files are present"
)
args <- parser$parse_args()
working.dir <- args$working.dir
x <- args$x
if (!R.utils::isAbsolutePath(x))
x <- file.path(working.dir, x)
y <- args$y
if (!R.utils::isAbsolutePath(y))
y <- file.path(working.dir, y)
tryCatch(merge_em(x, y, working.dir), finally = print("Fin"))
}
main()
You can set it as a default and override it when necessary.
merge_em <- function (x, y, working.dir = getwd()){
mergedfile <- merge (x, y, by = intersect(names(x), names(y)))
setwd(working.dir)
write (mergedfile, "merged.txt",
col.names = FALSE,
row.names = FALSE,
sep = "\t",
quote = FALSE
)
}
And override it with some other value:
merger_em(x, y, 'another/path/dir')
I haven't tested this, but default parameters are a standard in many languages.
Also, you can setwd with getwd like: setwd(getwd())

Resources