mirror of
https://github.com/rstudio/shiny.git
synced 2026-01-11 16:08:19 -05:00
Compare commits
2 Commits
create-sco
...
joe/bugfix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e5895da73 | ||
|
|
624fcfba45 |
10
DESCRIPTION
10
DESCRIPTION
@@ -1,7 +1,7 @@
|
||||
Package: shiny
|
||||
Type: Package
|
||||
Title: Web Application Framework for R
|
||||
Version: 1.1.0.9001
|
||||
Version: 1.1.0.9000
|
||||
Authors@R: c(
|
||||
person("Winston", "Chang", role = c("aut", "cre"), email = "winston@rstudio.com"),
|
||||
person("Joe", "Cheng", role = "aut", email = "joe@rstudio.com"),
|
||||
@@ -103,10 +103,7 @@ Collate:
|
||||
'map.R'
|
||||
'utils.R'
|
||||
'bootstrap.R'
|
||||
'cache-context.R'
|
||||
'cache-disk.R'
|
||||
'cache-memory.R'
|
||||
'cache-utils.R'
|
||||
'cache.R'
|
||||
'diagnose.R'
|
||||
'fileupload.R'
|
||||
'graph.R'
|
||||
@@ -145,7 +142,6 @@ Collate:
|
||||
'priorityqueue.R'
|
||||
'progress.R'
|
||||
'react.R'
|
||||
'render-cached-plot.R'
|
||||
'render-plot.R'
|
||||
'render-table.R'
|
||||
'run-url.R'
|
||||
@@ -162,4 +158,4 @@ Collate:
|
||||
'test-export.R'
|
||||
'timer.R'
|
||||
'update-input.R'
|
||||
RoxygenNote: 6.1.0
|
||||
RoxygenNote: 6.0.1.9000
|
||||
|
||||
@@ -25,7 +25,6 @@ S3method(as.tags,shiny.render.function)
|
||||
S3method(format,reactiveExpr)
|
||||
S3method(format,reactiveVal)
|
||||
S3method(names,reactivevalues)
|
||||
S3method(print,key_missing)
|
||||
S3method(print,reactive)
|
||||
S3method(print,shiny.appobj)
|
||||
S3method(str,reactivevalues)
|
||||
@@ -68,7 +67,6 @@ export(dateRangeInput)
|
||||
export(dblclickOpts)
|
||||
export(debounce)
|
||||
export(dialogViewer)
|
||||
export(diskCache)
|
||||
export(div)
|
||||
export(downloadButton)
|
||||
export(downloadHandler)
|
||||
@@ -92,7 +90,6 @@ export(fluidRow)
|
||||
export(formatStackTrace)
|
||||
export(freezeReactiveVal)
|
||||
export(freezeReactiveValue)
|
||||
export(getCurrentOutputInfo)
|
||||
export(getDefaultReactiveDomain)
|
||||
export(getQueryString)
|
||||
export(getShinyOption)
|
||||
@@ -124,7 +121,6 @@ export(insertTab)
|
||||
export(insertUI)
|
||||
export(installExprFunction)
|
||||
export(invalidateLater)
|
||||
export(is.key_missing)
|
||||
export(is.reactive)
|
||||
export(is.reactivevalues)
|
||||
export(is.shiny.appobj)
|
||||
@@ -132,7 +128,6 @@ export(is.singleton)
|
||||
export(isRunning)
|
||||
export(isTruthy)
|
||||
export(isolate)
|
||||
export(key_missing)
|
||||
export(knit_print.html)
|
||||
export(knit_print.reactive)
|
||||
export(knit_print.shiny.appobj)
|
||||
@@ -143,7 +138,6 @@ export(mainPanel)
|
||||
export(makeReactiveBinding)
|
||||
export(markRenderFunction)
|
||||
export(maskReactiveContext)
|
||||
export(memoryCache)
|
||||
export(modalButton)
|
||||
export(modalDialog)
|
||||
export(navbarMenu)
|
||||
@@ -195,7 +189,6 @@ export(removeModal)
|
||||
export(removeNotification)
|
||||
export(removeTab)
|
||||
export(removeUI)
|
||||
export(renderCachedPlot)
|
||||
export(renderDataTable)
|
||||
export(renderImage)
|
||||
export(renderPlot)
|
||||
@@ -233,7 +226,6 @@ export(showTab)
|
||||
export(sidebarLayout)
|
||||
export(sidebarPanel)
|
||||
export(singleton)
|
||||
export(sizeGrowthRatio)
|
||||
export(sliderInput)
|
||||
export(snapshotExclude)
|
||||
export(snapshotPreprocessInput)
|
||||
|
||||
8
NEWS.md
8
NEWS.md
@@ -1,18 +1,14 @@
|
||||
shiny 1.1.0.9001
|
||||
shiny 1.1.0.9000
|
||||
===========
|
||||
|
||||
## Full changelog
|
||||
|
||||
### Minor new features and improvements
|
||||
|
||||
* Added `renderCachedPlot()`, which stores plots in a cache so that they can be served up almost instantly. ([#1997](https://github.com/rstudio/shiny/pull/1997))
|
||||
|
||||
* Support for selecting variables of a data frame with the output values to be used within tidy evaluation. Added functions: `varSelectInput`, `varSelectizeInput`, `updateVarSelectInput`, `updateVarSelectizeInput`. ([#2091](https://github.com/rstudio/shiny/pull/2091))
|
||||
|
||||
* Addressed [#2042](https://github.com/rstudio/shiny/issues/2042): dates outside of `min`/`max` date range are now a lighter shade of grey to highlight the allowed range. ([#2087](https://github.com/rstudio/shiny/pull/2087))
|
||||
|
||||
* Added support for plot interaction when the plot is scaled. ([#2125](https://github.com/rstudio/shiny/pull/2125))
|
||||
|
||||
* Fixed [#1933](https://github.com/rstudio/shiny/issues/1933): extended server-side selectize to lists and optgroups. ([#2102](https://github.com/rstudio/shiny/pull/2102))
|
||||
|
||||
* Fixed [#1935](https://github.com/rstudio/shiny/issues/1935): correctly returns plot coordinates when using outer margins. ([#2108](https://github.com/rstudio/shiny/pull/2108))
|
||||
@@ -23,8 +19,6 @@ shiny 1.1.0.9001
|
||||
|
||||
* Fixed [#2138](https://github.com/rstudio/shiny/issues/2138): Inputs that are part of a `renderUI` were no longer restoring correctly from bookmarked state. [#2139](https://github.com/rstudio/shiny/pull/2139)
|
||||
|
||||
* Fixed [#2093](https://github.com/rstudio/shiny/issues/2093): Make sure bookmark scope directory does not exist before trying to create it. [#2168](https://github.com/rstudio/shiny/pull/2168)
|
||||
|
||||
### Documentation Updates
|
||||
|
||||
* Addressed [#1864](https://github.com/rstudio/shiny/issues/1864) by changing `optgroup` documentation to use `list` instead of `c`. ([#2084](https://github.com/rstudio/shiny/pull/2084))
|
||||
|
||||
561
R/cache-disk.R
561
R/cache-disk.R
@@ -1,561 +0,0 @@
|
||||
#' Create a disk cache object
|
||||
#'
|
||||
#' A disk cache object is a key-value store that saves the values as files in a
|
||||
#' directory on disk. Objects can be stored and retrieved using the \code{get()}
|
||||
#' and \code{set()} methods. Objects are automatically pruned from the cache
|
||||
#' according to the parameters \code{max_size}, \code{max_age}, \code{max_n},
|
||||
#' and \code{evict}.
|
||||
#'
|
||||
#'
|
||||
#' @section Missing Keys:
|
||||
#'
|
||||
#' The \code{missing} and \code{exec_missing} parameters controls what happens
|
||||
#' when \code{get()} is called with a key that is not in the cache (a cache
|
||||
#' miss). The default behavior is to return a \code{\link{key_missing}}
|
||||
#' object. This is a \emph{sentinel value} that indicates that the key was not
|
||||
#' present in the cache. You can test if the returned value represents a
|
||||
#' missing key by using the \code{\link{is.key_missing}} function. You can
|
||||
#' also have \code{get()} return a different sentinel value, like \code{NULL}.
|
||||
#' If you want to throw an error on a cache miss, you can do so by providing a
|
||||
#' function for \code{missing} that takes one argument, the key, and also use
|
||||
#' \code{exec_missing=TRUE}.
|
||||
#'
|
||||
#' When the cache is created, you can supply a value for \code{missing}, which
|
||||
#' sets the default value to be returned for missing values. It can also be
|
||||
#' overridden when \code{get()} is called, by supplying a \code{missing}
|
||||
#' argument. For example, if you use \code{cache$get("mykey", missing =
|
||||
#' NULL)}, it will return \code{NULL} if the key is not in the cache.
|
||||
#'
|
||||
#' If your cache is configured so that \code{get()} returns a sentinel value
|
||||
#' to represent a cache miss, then \code{set} will also not allow you to store
|
||||
#' the sentinel value in the cache. It will throw an error if you attempt to
|
||||
#' do so.
|
||||
#'
|
||||
#' Instead of returning the same sentinel value each time there is cache miss,
|
||||
#' the cache can execute a function each time \code{get()} encounters missing
|
||||
#' key. If the function returns a value, then \code{get()} will in turn return
|
||||
#' that value. However, a more common use is for the function to throw an
|
||||
#' error. If an error is thrown, then \code{get()} will not return a value.
|
||||
#'
|
||||
#' To do this, pass a one-argument function to \code{missing}, and use
|
||||
#' \code{exec_missing=TRUE}. For example, if you want to throw an error that
|
||||
#' prints the missing key, you could do this:
|
||||
#'
|
||||
#' \preformatted{
|
||||
#' diskCache(
|
||||
#' missing = function(key) {
|
||||
#' stop("Attempted to get missing key: ", key)
|
||||
#' },
|
||||
#' exec_missing = TRUE
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' If you use this, the code that calls \code{get()} should be wrapped with
|
||||
#' \code{\link{tryCatch}()} to gracefully handle missing keys.
|
||||
#'
|
||||
#' @section Cache pruning:
|
||||
#'
|
||||
#' Cache pruning occurs when \code{set()} is called, or it can be invoked
|
||||
#' manually by calling \code{prune()}.
|
||||
#'
|
||||
#' The disk cache will throttle the pruning so that it does not happen on
|
||||
#' every call to \code{set()}, because the filesystem operations for checking
|
||||
#' the status of files can be slow. Instead, it will prune once in every 20
|
||||
#' calls to \code{set()}, or if at least 5 seconds have elapsed since the last
|
||||
#' prune occurred, whichever is first. These parameters are currently not
|
||||
#' customizable, but may be in the future.
|
||||
#'
|
||||
#' When a pruning occurs, if there are any objects that are older than
|
||||
#' \code{max_age}, they will be removed.
|
||||
#'
|
||||
#' The \code{max_size} and \code{max_n} parameters are applied to the cache as
|
||||
#' a whole, in contrast to \code{max_age}, which is applied to each object
|
||||
#' individually.
|
||||
#'
|
||||
#' If the number of objects in the cache exceeds \code{max_n}, then objects
|
||||
#' will be removed from the cache according to the eviction policy, which is
|
||||
#' set with the \code{evict} parameter. Objects will be removed so that the
|
||||
#' number of items is \code{max_n}.
|
||||
#'
|
||||
#' If the size of the objects in the cache exceeds \code{max_size}, then
|
||||
#' objects will be removed from the cache. Objects will be removed from the
|
||||
#' cache so that the total size remains under \code{max_size}. Note that the
|
||||
#' size is calculated using the size of the files, not the size of disk space
|
||||
#' used by the files -- these two values can differ because of files are
|
||||
#' stored in blocks on disk. For example, if the block size is 4096 bytes,
|
||||
#' then a file that is one byte in size will take 4096 bytes on disk.
|
||||
#'
|
||||
#' Another time that objects can be removed from the cache is when
|
||||
#' \code{get()} is called. If the target object is older than \code{max_age},
|
||||
#' it will be removed and the cache will report it as a missing value.
|
||||
#'
|
||||
#' @section Eviction policies:
|
||||
#'
|
||||
#' If \code{max_n} or \code{max_size} are used, then objects will be removed
|
||||
#' from the cache according to an eviction policy. The available eviction
|
||||
#' policies are:
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{\code{"lru"}}{
|
||||
#' Least Recently Used. The least recently used objects will be removed.
|
||||
#' This uses the filesystem's mtime property. When "lru" is used, each
|
||||
#' \code{get()} is called, it will update the file's mtime.
|
||||
#' }
|
||||
#' \item{\code{"fifo"}}{
|
||||
#' First-in-first-out. The oldest objects will be removed.
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' Both of these policies use files' mtime. Note that some filesystems (notably
|
||||
#' FAT) have poor mtime resolution. (atime is not used because support for
|
||||
#' atime is worse than mtime.)
|
||||
#'
|
||||
#'
|
||||
#' @section Sharing among multiple processes:
|
||||
#'
|
||||
#' The directory for a DiskCache can be shared among multiple R processes. To
|
||||
#' do this, each R process should have a DiskCache object that uses the same
|
||||
#' directory. Each DiskCache will do pruning independently of the others, so if
|
||||
#' they have different pruning parameters, then one DiskCache may remove cached
|
||||
#' objects before another DiskCache would do so.
|
||||
#'
|
||||
#' Even though it is possible for multiple processes to share a DiskCache
|
||||
#' directory, this should not be done on networked file systems, because of
|
||||
#' slow performance of networked file systems can cause problems. If you need
|
||||
#' a high-performance shared cache, you can use one built on a database like
|
||||
#' Redis, SQLite, mySQL, or similar.
|
||||
#'
|
||||
#' When multiple processes share a cache directory, there are some potential
|
||||
#' race conditions. For example, if your code calls \code{exists(key)} to check
|
||||
#' if an object is in the cache, and then call \code{get(key)}, the object may
|
||||
#' be removed from the cache in between those two calls, and \code{get(key)}
|
||||
#' will throw an error. Instead of calling the two functions, it is better to
|
||||
#' simply call \code{get(key)}, and use \code{tryCatch()} to handle the error
|
||||
#' that is thrown if the object is not in the cache. This effectively tests for
|
||||
#' existence and gets the object in one operation.
|
||||
#'
|
||||
#' It is also possible for one processes to prune objects at the same time that
|
||||
#' another processes is trying to prune objects. If this happens, you may see
|
||||
#' a warning from \code{file.remove()} failing to remove a file that has
|
||||
#' already been deleted.
|
||||
#'
|
||||
#'
|
||||
#' @section Methods:
|
||||
#'
|
||||
#' A disk cache object has the following methods:
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{\code{get(key, missing, exec_missing)}}{
|
||||
#' Returns the value associated with \code{key}. If the key is not in the
|
||||
#' cache, then it returns the value specified by \code{missing} or,
|
||||
#' \code{missing} is a function and \code{exec_missing=TRUE}, then
|
||||
#' executes \code{missing}. The function can throw an error or return the
|
||||
#' value. If either of these parameters are specified here, then they
|
||||
#' will override the defaults that were set when the DiskCache object was
|
||||
#' created. See section Missing Keys for more information.
|
||||
#' }
|
||||
#' \item{\code{set(key, value)}}{
|
||||
#' Stores the \code{key}-\code{value} pair in the cache.
|
||||
#' }
|
||||
#' \item{\code{exists(key)}}{
|
||||
#' Returns \code{TRUE} if the cache contains the key, otherwise
|
||||
#' \code{FALSE}.
|
||||
#' }
|
||||
#' \item{\code{size()}}{
|
||||
#' Returns the number of items currently in the cache.
|
||||
#' }
|
||||
#' \item{\code{keys()}}{
|
||||
#' Returns a character vector of all keys currently in the cache.
|
||||
#' }
|
||||
#' \item{\code{reset()}}{
|
||||
#' Clears all objects from the cache.
|
||||
#' }
|
||||
#' \item{\code{destroy()}}{
|
||||
#' Clears all objects in the cache, and removes the cache directory from
|
||||
#' disk.
|
||||
#' }
|
||||
#' \item{\code{prune()}}{
|
||||
#' Prunes the cache, using the parameters specified by \code{max_size},
|
||||
#' \code{max_age}, \code{max_n}, and \code{evict}.
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' @param dir Directory to store files for the cache. If \code{NULL} (the
|
||||
#' default) it will create and use a temporary directory.
|
||||
#' @param max_age Maximum age of files in cache before they are evicted, in
|
||||
#' seconds. Use \code{Inf} for no age limit.
|
||||
#' @param max_size Maximum size of the cache, in bytes. If the cache exceeds
|
||||
#' this size, cached objects will be removed according to the value of the
|
||||
#' \code{evict}. Use \code{Inf} for no size limit.
|
||||
#' @param max_n Maximum number of objects in the cache. If the number of objects
|
||||
#' exceeds this value, then cached objects will be removed according to the
|
||||
#' value of \code{evict}. Use \code{Inf} for no limit of number of items.
|
||||
#' @param evict The eviction policy to use to decide which objects are removed
|
||||
#' when a cache pruning occurs. Currently, \code{"lru"} and \code{"fifo"} are
|
||||
#' supported.
|
||||
#' @param destroy_on_finalize If \code{TRUE}, then when the DiskCache object is
|
||||
#' garbage collected, the cache directory and all objects inside of it will be
|
||||
#' deleted from disk. If \code{FALSE} (the default), it will do nothing when
|
||||
#' finalized.
|
||||
#' @param missing A value to return or a function to execute when
|
||||
#' \code{get(key)} is called but the key is not present in the cache. The
|
||||
#' default is a \code{\link{key_missing}} object. If it is a function to
|
||||
#' execute, the function must take one argument (the key), and you must also
|
||||
#' use \code{exec_missing = TRUE}. If it is a function, it is useful in most
|
||||
#' cases for it to throw an error, although another option is to return a
|
||||
#' value. If a value is returned, that value will in turn be returned by
|
||||
#' \code{get()}. See section Missing keys for more information.
|
||||
#' @param exec_missing If \code{FALSE} (the default), then treat \code{missing}
|
||||
#' as a value to return when \code{get()} results in a cache miss. If
|
||||
#' \code{TRUE}, treat \code{missing} as a function to execute when
|
||||
#' \code{get()} results in a cache miss.
|
||||
#' @param logfile An optional filename or connection object to where logging
|
||||
#' information will be written. To log to the console, use \code{stdout()}.
|
||||
#'
|
||||
#' @export
|
||||
diskCache <- function(
|
||||
dir = NULL,
|
||||
max_size = 10 * 1024 ^ 2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
destroy_on_finalize = FALSE,
|
||||
missing = key_missing(),
|
||||
exec_missing = FALSE,
|
||||
logfile = NULL)
|
||||
{
|
||||
DiskCache$new(dir, max_size, max_age, max_n, evict, destroy_on_finalize,
|
||||
missing, exec_missing, logfile)
|
||||
}
|
||||
|
||||
|
||||
DiskCache <- R6Class("DiskCache",
|
||||
public = list(
|
||||
initialize = function(
|
||||
dir = NULL,
|
||||
max_size = 10 * 1024 ^ 2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
destroy_on_finalize = FALSE,
|
||||
missing = key_missing(),
|
||||
exec_missing = FALSE,
|
||||
logfile = NULL)
|
||||
{
|
||||
if (exec_missing && (!is.function(missing) || length(formals(missing)) == 0)) {
|
||||
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
|
||||
}
|
||||
if (is.null(dir)) {
|
||||
dir <- tempfile("DiskCache-")
|
||||
}
|
||||
if (!is.numeric(max_size)) stop("max_size must be a number. Use `Inf` for no limit.")
|
||||
if (!is.numeric(max_age)) stop("max_age must be a number. Use `Inf` for no limit.")
|
||||
if (!is.numeric(max_n)) stop("max_n must be a number. Use `Inf` for no limit.")
|
||||
|
||||
if (!dirExists(dir)) {
|
||||
private$log(paste0("initialize: Creating ", dir))
|
||||
dir.create(dir, recursive = TRUE)
|
||||
}
|
||||
|
||||
private$dir <- normalizePath(dir)
|
||||
private$max_size <- max_size
|
||||
private$max_age <- max_age
|
||||
private$max_n <- max_n
|
||||
private$evict <- match.arg(evict)
|
||||
private$destroy_on_finalize <- destroy_on_finalize
|
||||
private$missing <- missing
|
||||
private$exec_missing <- exec_missing
|
||||
private$logfile <- logfile
|
||||
|
||||
private$prune_last_time <- as.numeric(Sys.time())
|
||||
},
|
||||
|
||||
get = function(key, missing = private$missing, exec_missing = private$exec_missing) {
|
||||
private$log(paste0('get: key "', key, '"'))
|
||||
self$is_destroyed(throw = TRUE)
|
||||
validate_key(key)
|
||||
|
||||
private$maybe_prune_single(key)
|
||||
|
||||
filename <- private$key_to_filename(key)
|
||||
|
||||
# Instead of calling exists() before fetching the value, just try to
|
||||
# fetch the value. This reduces the risk of a race condition when
|
||||
# multiple processes share a cache.
|
||||
read_error <- FALSE
|
||||
tryCatch(
|
||||
{
|
||||
value <- suppressWarnings(readRDS(filename))
|
||||
if (private$evict == "lru"){
|
||||
Sys.setFileTime(filename, Sys.time())
|
||||
}
|
||||
},
|
||||
error = function(e) {
|
||||
read_error <<- TRUE
|
||||
}
|
||||
)
|
||||
if (read_error) {
|
||||
private$log(paste0('get: key "', key, '" is missing'))
|
||||
|
||||
if (exec_missing) {
|
||||
if (!is.function(missing) || length(formals(missing)) == 0) {
|
||||
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
|
||||
}
|
||||
return(missing(key))
|
||||
} else {
|
||||
return(missing)
|
||||
}
|
||||
}
|
||||
|
||||
private$log(paste0('get: key "', key, '" found'))
|
||||
value
|
||||
},
|
||||
|
||||
set = function(key, value) {
|
||||
private$log(paste0('set: key "', key, '"'))
|
||||
self$is_destroyed(throw = TRUE)
|
||||
validate_key(key)
|
||||
|
||||
file <- private$key_to_filename(key)
|
||||
temp_file <- paste0(file, "-temp-", createUniqueId(8))
|
||||
|
||||
save_error <- FALSE
|
||||
ref_object <- FALSE
|
||||
tryCatch(
|
||||
{
|
||||
saveRDS(value, file = temp_file,
|
||||
refhook = function(x) {
|
||||
ref_object <<- TRUE
|
||||
NULL
|
||||
}
|
||||
)
|
||||
file.rename(temp_file, file)
|
||||
},
|
||||
error = function(e) {
|
||||
save_error <<- TRUE
|
||||
# Unlike file.remove(), unlink() does not raise warning if file does
|
||||
# not exist.
|
||||
unlink(temp_file)
|
||||
}
|
||||
)
|
||||
if (save_error) {
|
||||
private$log(paste0('set: key "', key, '" error'))
|
||||
stop('Error setting value for key "', key, '".')
|
||||
}
|
||||
if (ref_object) {
|
||||
private$log(paste0('set: value is a reference object'))
|
||||
warning("A reference object was cached in a serialized format. The restored object may not work as expected.")
|
||||
}
|
||||
|
||||
private$prune_throttled()
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
exists = function(key) {
|
||||
self$is_destroyed(throw = TRUE)
|
||||
validate_key(key)
|
||||
file.exists(private$key_to_filename(key))
|
||||
},
|
||||
|
||||
# Return all keys in the cache
|
||||
keys = function() {
|
||||
self$is_destroyed(throw = TRUE)
|
||||
files <- dir(private$dir, "\\.rds$")
|
||||
sub("\\.rds$", "", files)
|
||||
},
|
||||
|
||||
remove = function(key) {
|
||||
private$log(paste0('remove: key "', key, '"'))
|
||||
self$is_destroyed(throw = TRUE)
|
||||
validate_key(key)
|
||||
file.remove(private$key_to_filename(key))
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
reset = function() {
|
||||
private$log(paste0('reset'))
|
||||
self$is_destroyed(throw = TRUE)
|
||||
file.remove(dir(private$dir, "\\.rds$", full.names = TRUE))
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
prune = function() {
|
||||
# TODO: It would be good to add parameters `n` and `size`, so that the
|
||||
# cache can be pruned to `max_n - n` and `max_size - size` before adding
|
||||
# an object. Right now we prune after adding the object, so the cache
|
||||
# can temporarily grow past the limits. The reason we don't do this now
|
||||
# is because it is expensive to find the size of the serialized object
|
||||
# before adding it.
|
||||
|
||||
private$log(paste0('prune'))
|
||||
self$is_destroyed(throw = TRUE)
|
||||
|
||||
current_time <- Sys.time()
|
||||
|
||||
filenames <- dir(private$dir, "\\.rds$", full.names = TRUE)
|
||||
info <- file.info(filenames)
|
||||
info <- info[info$isdir == FALSE, ]
|
||||
info$name <- rownames(info)
|
||||
rownames(info) <- NULL
|
||||
# Files could be removed between the dir() and file.info() calls. The
|
||||
# entire row for such files will have NA values. Remove those rows.
|
||||
info <- info[!is.na(info$size), ]
|
||||
|
||||
# 1. Remove any files where the age exceeds max age.
|
||||
if (is.finite(private$max_age)) {
|
||||
timediff <- as.numeric(current_time - info$mtime, units = "secs")
|
||||
rm_idx <- timediff > private$max_age
|
||||
if (any(rm_idx)) {
|
||||
private$log(paste0("prune max_age: Removing ", paste(info$name[rm_idx], collapse = ", ")))
|
||||
file.remove(info$name[rm_idx])
|
||||
info <- info[!rm_idx, ]
|
||||
}
|
||||
}
|
||||
|
||||
# Sort objects by priority. The sorting is done in a function which can be
|
||||
# called multiple times but only does the work the first time.
|
||||
info_is_sorted <- FALSE
|
||||
ensure_info_is_sorted <- function() {
|
||||
if (info_is_sorted) return()
|
||||
|
||||
info <<- info[order(info$mtime, decreasing = TRUE), ]
|
||||
info_is_sorted <<- TRUE
|
||||
}
|
||||
|
||||
# 2. Remove files if there are too many.
|
||||
if (is.finite(private$max_n) && nrow(info) > private$max_n) {
|
||||
ensure_info_is_sorted()
|
||||
rm_idx <- seq_len(nrow(info)) > private$max_n
|
||||
private$log(paste0("prune max_n: Removing ", paste(info$name[rm_idx], collapse = ", ")))
|
||||
rm_success <- file.remove(info$name[rm_idx])
|
||||
info <- info[!rm_success, ]
|
||||
}
|
||||
|
||||
# 3. Remove files if cache is too large.
|
||||
if (is.finite(private$max_size) && sum(info$size) > private$max_size) {
|
||||
ensure_info_is_sorted()
|
||||
cum_size <- cumsum(info$size)
|
||||
rm_idx <- cum_size > private$max_size
|
||||
private$log(paste0("prune max_size: Removing ", paste(info$name[rm_idx], collapse = ", ")))
|
||||
rm_success <- file.remove(info$name[rm_idx])
|
||||
info <- info[!rm_success, ]
|
||||
}
|
||||
|
||||
private$prune_last_time <- as.numeric(current_time)
|
||||
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
size = function() {
|
||||
self$is_destroyed(throw = TRUE)
|
||||
length(dir(private$dir, "\\.rds$"))
|
||||
},
|
||||
|
||||
destroy = function() {
|
||||
if (self$is_destroyed()) {
|
||||
return(invisible(self))
|
||||
}
|
||||
|
||||
private$log(paste0("destroy: Removing ", private$dir))
|
||||
# First create a sentinel file so that other processes sharing this
|
||||
# cache know that the cache is to be destroyed. This is needed because
|
||||
# the recursive unlink is not atomic: another process can add a file to
|
||||
# the directory after unlink starts removing files but before it removes
|
||||
# the directory, and when that happens, the directory removal will fail.
|
||||
file.create(file.path(private$dir, "__destroyed__"))
|
||||
# Remove all the .rds files. This will not remove the setinel file.
|
||||
file.remove(dir(private$dir, "\\.rds$", full.names = TRUE))
|
||||
# Next remove dir recursively, including sentinel file.
|
||||
unlink(private$dir, recursive = TRUE)
|
||||
private$destroyed <- TRUE
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
is_destroyed = function(throw = FALSE) {
|
||||
if (!dirExists(private$dir) ||
|
||||
file.exists(file.path(private$dir, "__destroyed__")))
|
||||
{
|
||||
# It's possible for another process to destroy a shared cache directory
|
||||
private$destroyed <- TRUE
|
||||
}
|
||||
|
||||
if (throw) {
|
||||
if (private$destroyed) {
|
||||
stop("Attempted to use cache which has been destroyed:\n ", private$dir)
|
||||
}
|
||||
|
||||
} else {
|
||||
private$destroyed
|
||||
}
|
||||
},
|
||||
|
||||
finalize = function() {
|
||||
if (private$destroy_on_finalize) {
|
||||
self$destroy()
|
||||
}
|
||||
}
|
||||
),
|
||||
|
||||
private = list(
|
||||
dir = NULL,
|
||||
max_age = NULL,
|
||||
max_size = NULL,
|
||||
max_n = NULL,
|
||||
evict = NULL,
|
||||
destroy_on_finalize = NULL,
|
||||
destroyed = FALSE,
|
||||
missing = NULL,
|
||||
exec_missing = FALSE,
|
||||
logfile = NULL,
|
||||
|
||||
prune_throttle_counter = 0,
|
||||
prune_last_time = NULL,
|
||||
|
||||
key_to_filename = function(key) {
|
||||
validate_key(key)
|
||||
# Additional validation. This 80-char limit is arbitrary, and is
|
||||
# intended to avoid hitting a filename length limit on Windows.
|
||||
if (nchar(key) > 80) {
|
||||
stop("Invalid key: key must have fewer than 80 characters.")
|
||||
}
|
||||
file.path(private$dir, paste0(key, ".rds"))
|
||||
},
|
||||
|
||||
# A wrapper for prune() that throttles it, because prune() can be
|
||||
# expensive due to filesystem operations. This function will prune only
|
||||
# once every 20 times it is called, or if it has been more than 5 seconds
|
||||
# since the last time the cache was actually pruned, whichever is first.
|
||||
# In the future, the behavior may be customizable.
|
||||
prune_throttled = function() {
|
||||
# Count the number of times prune() has been called.
|
||||
private$prune_throttle_counter <- private$prune_throttle_counter + 1
|
||||
|
||||
if (private$prune_throttle_counter > 20 ||
|
||||
private$prune_last_time - as.numeric(Sys.time()) > 5)
|
||||
{
|
||||
self$prune()
|
||||
private$prune_throttle_counter <- 0
|
||||
}
|
||||
},
|
||||
|
||||
# Prunes a single object if it exceeds max_age. If the object does not
|
||||
# exceed max_age, or if the object doesn't exist, do nothing.
|
||||
maybe_prune_single = function(key) {
|
||||
obj <- private$cache[[key]]
|
||||
if (is.null(obj)) return()
|
||||
|
||||
timediff <- as.numeric(Sys.time()) - obj$mtime
|
||||
if (timediff > private$max_age) {
|
||||
private$log(paste0("pruning single object exceeding max_age: Removing ", key))
|
||||
rm(list = key, envir = private$cache)
|
||||
}
|
||||
},
|
||||
|
||||
log = function(text) {
|
||||
if (is.null(private$logfile)) return()
|
||||
|
||||
text <- paste0(format(Sys.time(), "[%Y-%m-%d %H:%M:%OS3] DiskCache "), text)
|
||||
writeLines(text, private$logfile)
|
||||
}
|
||||
)
|
||||
)
|
||||
366
R/cache-memory.R
366
R/cache-memory.R
@@ -1,366 +0,0 @@
|
||||
#' Create a memory cache object
|
||||
#'
|
||||
#' A memory cache object is a key-value store that saves the values in an
|
||||
#' environment. Objects can be stored and retrieved using the \code{get()} and
|
||||
#' \code{set()} methods. Objects are automatically pruned from the cache
|
||||
#' according to the parameters \code{max_size}, \code{max_age}, \code{max_n},
|
||||
#' and \code{evict}.
|
||||
#'
|
||||
#' In a \code{MemoryCache}, R objects are stored directly in the cache; they are
|
||||
#' not \emph{not} serialized before being stored in the cache. This contrasts
|
||||
#' with other cache types, like \code{\link{diskCache}}, where objects are
|
||||
#' serialized, and the serialized object is cached. This can result in some
|
||||
#' differences of behavior. For example, as long as an object is stored in a
|
||||
#' MemoryCache, it will not be garbage collected.
|
||||
#'
|
||||
#'
|
||||
#' @section Missing keys:
|
||||
#' The \code{missing} and \code{exec_missing} parameters controls what happens
|
||||
#' when \code{get()} is called with a key that is not in the cache (a cache
|
||||
#' miss). The default behavior is to return a \code{\link{key_missing}}
|
||||
#' object. This is a \emph{sentinel value} that indicates that the key was not
|
||||
#' present in the cache. You can test if the returned value represents a
|
||||
#' missing key by using the \code{\link{is.key_missing}} function. You can
|
||||
#' also have \code{get()} return a different sentinel value, like \code{NULL}.
|
||||
#' If you want to throw an error on a cache miss, you can do so by providing a
|
||||
#' function for \code{missing} that takes one argument, the key, and also use
|
||||
#' \code{exec_missing=TRUE}.
|
||||
#'
|
||||
#' When the cache is created, you can supply a value for \code{missing}, which
|
||||
#' sets the default value to be returned for missing values. It can also be
|
||||
#' overridden when \code{get()} is called, by supplying a \code{missing}
|
||||
#' argument. For example, if you use \code{cache$get("mykey", missing =
|
||||
#' NULL)}, it will return \code{NULL} if the key is not in the cache.
|
||||
#'
|
||||
#' If your cache is configured so that \code{get()} returns a sentinel value
|
||||
#' to represent a cache miss, then \code{set} will also not allow you to store
|
||||
#' the sentinel value in the cache. It will throw an error if you attempt to
|
||||
#' do so.
|
||||
#'
|
||||
#' Instead of returning the same sentinel value each time there is cache miss,
|
||||
#' the cache can execute a function each time \code{get()} encounters missing
|
||||
#' key. If the function returns a value, then \code{get()} will in turn return
|
||||
#' that value. However, a more common use is for the function to throw an
|
||||
#' error. If an error is thrown, then \code{get()} will not return a value.
|
||||
#'
|
||||
#' To do this, pass a one-argument function to \code{missing}, and use
|
||||
#' \code{exec_missing=TRUE}. For example, if you want to throw an error that
|
||||
#' prints the missing key, you could do this:
|
||||
#'
|
||||
#' \preformatted{
|
||||
#' diskCache(
|
||||
#' missing = function(key) {
|
||||
#' stop("Attempted to get missing key: ", key)
|
||||
#' },
|
||||
#' exec_missing = TRUE
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' If you use this, the code that calls \code{get()} should be wrapped with
|
||||
#' \code{\link{tryCatch}()} to gracefully handle missing keys.
|
||||
#'
|
||||
#' @section Cache pruning:
|
||||
#'
|
||||
#' Cache pruning occurs when \code{set()} is called, or it can be invoked
|
||||
#' manually by calling \code{prune()}.
|
||||
#'
|
||||
#' When a pruning occurs, if there are any objects that are older than
|
||||
#' \code{max_age}, they will be removed.
|
||||
#'
|
||||
#' The \code{max_size} and \code{max_n} parameters are applied to the cache as
|
||||
#' a whole, in contrast to \code{max_age}, which is applied to each object
|
||||
#' individually.
|
||||
#'
|
||||
#' If the number of objects in the cache exceeds \code{max_n}, then objects
|
||||
#' will be removed from the cache according to the eviction policy, which is
|
||||
#' set with the \code{evict} parameter. Objects will be removed so that the
|
||||
#' number of items is \code{max_n}.
|
||||
#'
|
||||
#' If the size of the objects in the cache exceeds \code{max_size}, then
|
||||
#' objects will be removed from the cache. Objects will be removed from the
|
||||
#' cache so that the total size remains under \code{max_size}. Note that the
|
||||
#' size is calculated using the size of the files, not the size of disk space
|
||||
#' used by the files -- these two values can differ because of files are
|
||||
#' stored in blocks on disk. For example, if the block size is 4096 bytes,
|
||||
#' then a file that is one byte in size will take 4096 bytes on disk.
|
||||
#'
|
||||
#' Another time that objects can be removed from the cache is when
|
||||
#' \code{get()} is called. If the target object is older than \code{max_age},
|
||||
#' it will be removed and the cache will report it as a missing value.
|
||||
#'
|
||||
#' @section Eviction policies:
|
||||
#'
|
||||
#' If \code{max_n} or \code{max_size} are used, then objects will be removed
|
||||
#' from the cache according to an eviction policy. The available eviction
|
||||
#' policies are:
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{\code{"lru"}}{
|
||||
#' Least Recently Used. The least recently used objects will be removed.
|
||||
#' This uses the filesystem's atime property. Some filesystems do not
|
||||
#' support atime, or have a very low atime resolution. The DiskCache will
|
||||
#' check for atime support, and if the filesystem does not support atime,
|
||||
#' a warning will be issued and the "fifo" policy will be used instead.
|
||||
#' }
|
||||
#' \item{\code{"fifo"}}{
|
||||
#' First-in-first-out. The oldest objects will be removed.
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' @section Methods:
|
||||
#'
|
||||
#' A disk cache object has the following methods:
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{\code{get(key, missing, exec_missing)}}{
|
||||
#' Returns the value associated with \code{key}. If the key is not in the
|
||||
#' cache, then it returns the value specified by \code{missing} or,
|
||||
#' \code{missing} is a function and \code{exec_missing=TRUE}, then
|
||||
#' executes \code{missing}. The function can throw an error or return the
|
||||
#' value. If either of these parameters are specified here, then they
|
||||
#' will override the defaults that were set when the DiskCache object was
|
||||
#' created. See section Missing Keys for more information.
|
||||
#' }
|
||||
#' \item{\code{set(key, value)}}{
|
||||
#' Stores the \code{key}-\code{value} pair in the cache.
|
||||
#' }
|
||||
#' \item{\code{exists(key)}}{
|
||||
#' Returns \code{TRUE} if the cache contains the key, otherwise
|
||||
#' \code{FALSE}.
|
||||
#' }
|
||||
#' \item{\code{size()}}{
|
||||
#' Returns the number of items currently in the cache.
|
||||
#' }
|
||||
#' \item{\code{keys()}}{
|
||||
#' Returns a character vector of all keys currently in the cache.
|
||||
#' }
|
||||
#' \item{\code{reset()}}{
|
||||
#' Clears all objects from the cache.
|
||||
#' }
|
||||
#' \item{\code{destroy()}}{
|
||||
#' Clears all objects in the cache, and removes the cache directory from
|
||||
#' disk.
|
||||
#' }
|
||||
#' \item{\code{prune()}}{
|
||||
#' Prunes the cache, using the parameters specified by \code{max_size},
|
||||
#' \code{max_age}, \code{max_n}, and \code{evict}.
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' @inheritParams diskCache
|
||||
#'
|
||||
#' @export
|
||||
memoryCache <- function(
|
||||
max_size = 10 * 1024 ^ 2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
missing = key_missing(),
|
||||
exec_missing = FALSE,
|
||||
logfile = NULL)
|
||||
{
|
||||
MemoryCache$new(max_size, max_age, max_n, evict, missing, exec_missing, logfile)
|
||||
}
|
||||
|
||||
MemoryCache <- R6Class("MemoryCache",
|
||||
public = list(
|
||||
initialize = function(
|
||||
max_size = 10 * 1024 ^ 2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
missing = key_missing(),
|
||||
exec_missing = FALSE,
|
||||
logfile = NULL)
|
||||
{
|
||||
if (exec_missing && (!is.function(missing) || length(formals(missing)) == 0)) {
|
||||
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
|
||||
}
|
||||
if (!is.numeric(max_size)) stop("max_size must be a number. Use `Inf` for no limit.")
|
||||
if (!is.numeric(max_age)) stop("max_age must be a number. Use `Inf` for no limit.")
|
||||
if (!is.numeric(max_n)) stop("max_n must be a number. Use `Inf` for no limit.")
|
||||
private$cache <- new.env(parent = emptyenv())
|
||||
private$max_size <- max_size
|
||||
private$max_age <- max_age
|
||||
private$max_n <- max_n
|
||||
private$evict <- match.arg(evict)
|
||||
private$missing <- missing
|
||||
private$exec_missing <- exec_missing
|
||||
private$logfile <- logfile
|
||||
},
|
||||
|
||||
get = function(key, missing = private$missing, exec_missing = private$exec_missing) {
|
||||
private$log(paste0('get: key "', key, '"'))
|
||||
validate_key(key)
|
||||
|
||||
private$maybe_prune_single(key)
|
||||
|
||||
if (!self$exists(key)) {
|
||||
private$log(paste0('get: key "', key, '" is missing'))
|
||||
if (exec_missing) {
|
||||
if (!is.function(missing) || length(formals(missing)) == 0) {
|
||||
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
|
||||
}
|
||||
return(missing(key))
|
||||
} else {
|
||||
return(missing)
|
||||
}
|
||||
}
|
||||
|
||||
private$log(paste0('get: key "', key, '" found'))
|
||||
value <- private$cache[[key]]$value
|
||||
value
|
||||
},
|
||||
|
||||
set = function(key, value) {
|
||||
private$log(paste0('set: key "', key, '"'))
|
||||
validate_key(key)
|
||||
|
||||
time <- as.numeric(Sys.time())
|
||||
|
||||
# Only record size if we're actually using max_size for pruning.
|
||||
if (is.finite(private$max_size)) {
|
||||
# Reported size is rough! See ?object.size.
|
||||
size <- as.numeric(object.size(value))
|
||||
} else {
|
||||
size <- NULL
|
||||
}
|
||||
|
||||
private$cache[[key]] <- list(
|
||||
key = key,
|
||||
value = value,
|
||||
size = size,
|
||||
mtime = time,
|
||||
atime = time
|
||||
)
|
||||
self$prune()
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
exists = function(key) {
|
||||
validate_key(key)
|
||||
# Faster than `exists(key, envir = private$cache, inherits = FALSE)
|
||||
!is.null(private$cache[[key]])
|
||||
},
|
||||
|
||||
keys = function() {
|
||||
ls(private$cache, sorted = FALSE) # Faster with sorted=FALSE
|
||||
},
|
||||
|
||||
remove = function(key) {
|
||||
private$log(paste0('remove: key "', key, '"'))
|
||||
validate_key(key)
|
||||
rm(list = key, envir = private$cache)
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
reset = function() {
|
||||
private$log(paste0('reset'))
|
||||
rm(list = self$keys(), envir = private$cache)
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
prune = function() {
|
||||
private$log(paste0('prune'))
|
||||
info <- private$object_info()
|
||||
|
||||
# 1. Remove any objects where the age exceeds max age.
|
||||
if (is.finite(private$max_age)) {
|
||||
time <- as.numeric(Sys.time())
|
||||
timediff <- time - info$mtime
|
||||
rm_idx <- timediff > private$max_age
|
||||
if (any(rm_idx)) {
|
||||
private$log(paste0("prune max_age: Removing ", paste(info$key[rm_idx], collapse = ", ")))
|
||||
rm(list = info$key[rm_idx], envir = private$cache)
|
||||
info <- info[!rm_idx, ]
|
||||
}
|
||||
}
|
||||
|
||||
# Sort objects by priority, according to eviction policy. The sorting is
|
||||
# done in a function which can be called multiple times but only does
|
||||
# the work the first time.
|
||||
info_is_sorted <- FALSE
|
||||
ensure_info_is_sorted <- function() {
|
||||
if (info_is_sorted) return()
|
||||
|
||||
if (private$evict == "lru") {
|
||||
info <<- info[order(info$atime, decreasing = TRUE), ]
|
||||
} else if (private$evict == "fifo") {
|
||||
info <<- info[order(info$mtime, decreasing = TRUE), ]
|
||||
} else {
|
||||
stop('Unknown eviction policy "', private$evict, '"')
|
||||
}
|
||||
info_is_sorted <<- TRUE
|
||||
}
|
||||
|
||||
# 2. Remove objects if there are too many.
|
||||
if (is.finite(private$max_n) && nrow(info) > private$max_n) {
|
||||
ensure_info_is_sorted()
|
||||
rm_idx <- seq_len(nrow(info)) > private$max_n
|
||||
private$log(paste0("prune max_n: Removing ", paste(info$key[rm_idx], collapse = ", ")))
|
||||
rm(list = info$key[rm_idx], envir = private$cache)
|
||||
info <- info[!rm_idx, ]
|
||||
}
|
||||
|
||||
# 3. Remove objects if cache is too large.
|
||||
if (is.finite(private$max_size) && sum(info$size) > private$max_size) {
|
||||
ensure_info_is_sorted()
|
||||
cum_size <- cumsum(info$size)
|
||||
rm_idx <- cum_size > private$max_size
|
||||
private$log(paste0("prune max_size: Removing ", paste(info$key[rm_idx], collapse = ", ")))
|
||||
rm(list = info$key[rm_idx], envir = private$cache)
|
||||
info <- info[!rm_idx, ]
|
||||
}
|
||||
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
size = function() {
|
||||
length(self$keys())
|
||||
}
|
||||
),
|
||||
|
||||
private = list(
|
||||
cache = NULL,
|
||||
max_age = NULL,
|
||||
max_size = NULL,
|
||||
max_n = NULL,
|
||||
evict = NULL,
|
||||
missing = NULL,
|
||||
exec_missing = NULL,
|
||||
logfile = NULL,
|
||||
|
||||
# Prunes a single object if it exceeds max_age. If the object does not
|
||||
# exceed max_age, or if the object doesn't exist, do nothing.
|
||||
maybe_prune_single = function(key) {
|
||||
if (!is.finite(private$max_age)) return()
|
||||
|
||||
obj <- private$cache[[key]]
|
||||
if (is.null(obj)) return()
|
||||
|
||||
timediff <- as.numeric(Sys.time()) - obj$mtime
|
||||
if (timediff > private$max_age) {
|
||||
private$log(paste0("pruning single object exceeding max_age: Removing ", key))
|
||||
rm(list = key, envir = private$cache)
|
||||
}
|
||||
},
|
||||
|
||||
object_info = function() {
|
||||
keys <- ls(private$cache, sorted = FALSE)
|
||||
data.frame(
|
||||
key = keys,
|
||||
size = vapply(keys, function(key) private$cache[[key]]$size, 0),
|
||||
mtime = vapply(keys, function(key) private$cache[[key]]$mtime, 0),
|
||||
atime = vapply(keys, function(key) private$cache[[key]]$atime, 0),
|
||||
stringsAsFactors = FALSE
|
||||
)
|
||||
},
|
||||
|
||||
log = function(text) {
|
||||
if (is.null(private$logfile)) return()
|
||||
|
||||
text <- paste0(format(Sys.time(), "[%Y-%m-%d %H:%M:%OS3] MemoryCache "), text)
|
||||
writeLines(text, private$logfile)
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -1,33 +0,0 @@
|
||||
#' A Key Missing object
|
||||
#'
|
||||
#' A \code{key_missing} object represents a cache miss.
|
||||
#'
|
||||
#' @param x An object to test.
|
||||
#'
|
||||
#' @seealso \code{\link{diskCache}}, \code{\link{memoryCache}}.
|
||||
#'
|
||||
#' @export
|
||||
key_missing <- function() {
|
||||
structure(list(), class = "key_missing")
|
||||
}
|
||||
|
||||
#' @rdname key_missing
|
||||
#' @export
|
||||
is.key_missing <- function(x) {
|
||||
inherits(x, "key_missing")
|
||||
}
|
||||
|
||||
#' @export
|
||||
print.key_missing <- function(x, ...) {
|
||||
cat("<Key Missing>\n")
|
||||
}
|
||||
|
||||
|
||||
validate_key <- function(key) {
|
||||
if (!is.character(key) || length(key) != 1 || nchar(key) == 0) {
|
||||
stop("Invalid key: key must be single non-empty string.")
|
||||
}
|
||||
if (grepl("[^a-z0-9]", key)) {
|
||||
stop("Invalid key: ", key, ". Only lowercase letters and numbers are allowed.")
|
||||
}
|
||||
}
|
||||
@@ -249,20 +249,14 @@ nearPoints <- function(df, coordinfo, xvar = NULL, yvar = NULL,
|
||||
x <- asNumber(df[[xvar]])
|
||||
y <- asNumber(df[[yvar]])
|
||||
|
||||
# Get the coordinates of the point (in img pixel coordinates)
|
||||
point_img <- scaleCoords(coordinfo$x, coordinfo$y, coordinfo)
|
||||
# Get the pixel coordinates of the point
|
||||
coordPx <- scaleCoords(coordinfo$x, coordinfo$y, coordinfo)
|
||||
|
||||
# Get coordinates of data points (in img pixel coordinates)
|
||||
data_img <- scaleCoords(x, y, coordinfo)
|
||||
# Get pixel coordinates of data points
|
||||
dataPx <- scaleCoords(x, y, coordinfo)
|
||||
|
||||
# Get x/y distances (in css coordinates)
|
||||
dist_css <- list(
|
||||
x = (data_img$x - point_img$x) / coordinfo$pixelratio$x,
|
||||
y = (data_img$y - point_img$y) / coordinfo$pixelratio$y
|
||||
)
|
||||
|
||||
# Distances of data points to the target point, in css pixels.
|
||||
dists <- sqrt(dist_css$x^2 + dist_css$y^2)
|
||||
# Distances of data points to coordPx
|
||||
dists <- sqrt((dataPx$x - coordPx$x) ^ 2 + (dataPx$y - coordPx$y) ^ 2)
|
||||
|
||||
if (addDist)
|
||||
df$dist_ <- dists
|
||||
@@ -304,56 +298,50 @@ nearPoints <- function(df, coordinfo, xvar = NULL, yvar = NULL,
|
||||
# The coordinfo data structure will look something like the examples below.
|
||||
# For base graphics, `mapping` is empty, and there are no panelvars:
|
||||
# List of 7
|
||||
# $ x : num 4.37
|
||||
# $ y : num 12
|
||||
# $ pixelratio:List of 2
|
||||
# ..$ x: num 2
|
||||
# ..$ y: num 2
|
||||
# $ mapping : Named list()
|
||||
# $ domain :List of 4
|
||||
# $ x : num 4.37
|
||||
# $ y : num 12
|
||||
# $ mapping: Named list()
|
||||
# $ domain :List of 4
|
||||
# ..$ left : num 1.36
|
||||
# ..$ right : num 5.58
|
||||
# ..$ bottom: num 9.46
|
||||
# ..$ top : num 34.8
|
||||
# $ range :List of 4
|
||||
# $ range :List of 4
|
||||
# ..$ left : num 58
|
||||
# ..$ right : num 429
|
||||
# ..$ bottom: num 226
|
||||
# ..$ top : num 58
|
||||
# $ log :List of 2
|
||||
# $ log :List of 2
|
||||
# ..$ x: NULL
|
||||
# ..$ y: NULL
|
||||
# $ .nonce : num 0.343
|
||||
# $ .nonce : num 0.343
|
||||
#
|
||||
# For ggplot2, the mapping vars usually will be included, and if faceting is
|
||||
# used, they will be listed as panelvars:
|
||||
# List of 9
|
||||
# $ x : num 3.78
|
||||
# $ y : num 17.1
|
||||
# $ pixelratio:List of 2
|
||||
# ..$ x: num 2
|
||||
# ..$ y: num 2
|
||||
# $ panelvar1 : int 6
|
||||
# $ panelvar2 : int 0
|
||||
# $ mapping :List of 4
|
||||
# $ x : num 3.78
|
||||
# $ y : num 17.1
|
||||
# $ panelvar1: int 6
|
||||
# $ panelvar2: int 0
|
||||
# $ mapping :List of 4
|
||||
# ..$ x : chr "wt"
|
||||
# ..$ y : chr "mpg"
|
||||
# ..$ panelvar1: chr "cyl"
|
||||
# ..$ panelvar2: chr "am"
|
||||
# $ domain :List of 4
|
||||
# $ domain :List of 4
|
||||
# ..$ left : num 1.32
|
||||
# ..$ right : num 5.62
|
||||
# ..$ bottom: num 9.22
|
||||
# ..$ top : num 35.1
|
||||
# $ range :List of 4
|
||||
# $ range :List of 4
|
||||
# ..$ left : num 172
|
||||
# ..$ right : num 300
|
||||
# ..$ bottom: num 144
|
||||
# ..$ top : num 28.5
|
||||
# $ log :List of 2
|
||||
# $ log :List of 2
|
||||
# ..$ x: NULL
|
||||
# ..$ y: NULL
|
||||
# $ .nonce : num 0.603
|
||||
# $ .nonce : num 0.603
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,588 +0,0 @@
|
||||
#' Plot output with cached images
|
||||
#'
|
||||
#' Renders a reactive plot, with plot images cached to disk.
|
||||
#'
|
||||
#' \code{expr} is an expression that generates a plot, similar to that in
|
||||
#' \code{renderPlot}. Unlike with \code{renderPlot}, this expression does not
|
||||
#' take reactive dependencies. It is re-executed only when the cache key
|
||||
#' changes.
|
||||
#'
|
||||
#' \code{cacheKeyExpr} is an expression which, when evaluated, returns an object
|
||||
#' which will be serialized and hashed using the \code{\link[digest]{digest}}
|
||||
#' function to generate a string that will be used as a cache key. This key is
|
||||
#' used to identify the contents of the plot: if the cache key is the same as a
|
||||
#' previous time, it assumes that the plot is the same and can be retrieved from
|
||||
#' the cache.
|
||||
#'
|
||||
#' This \code{cacheKeyExpr} is reactive, and so it will be re-evaluated when any
|
||||
#' upstream reactives are invalidated. This will also trigger re-execution of
|
||||
#' the plotting expression, \code{expr}.
|
||||
#'
|
||||
#' The key should consist of "normal" R objects, like vectors and lists. Lists
|
||||
#' should in turn contain other normal R objects. If the key contains
|
||||
#' environments, external pointers, or reference objects -- or even if it has
|
||||
#' such objects attached as attributes -- then it is possible that it will
|
||||
#' change unpredictably even when you do not expect it to. Additionally, because
|
||||
#' the entire key is serialized and hashed, if it contains a very large object
|
||||
#' -- a large data set, for example -- there may be a noticeable performance
|
||||
#' penalty.
|
||||
#'
|
||||
#' If you face these issues with the cache key, you can work around them by
|
||||
#' extracting out the important parts of the objects, and/or by converting them
|
||||
#' to normal R objects before returning them. Your expression could even
|
||||
#' serialize and hash that information in an efficient way and return a string,
|
||||
#' which will in turn be hashed (very quickly) by the
|
||||
#' \code{\link[digest]{digest}} function.
|
||||
#'
|
||||
#' Internally, the result from \code{cacheKeyExpr} is combined with the name of
|
||||
#' the output (if you assign it to \code{output$plot1}, it will be combined
|
||||
#' with \code{"plot1"}) to form the actual key that is used. As a result, even
|
||||
#' if there are multiple plots that have the same \code{cacheKeyExpr}, they
|
||||
#' will not have cache key collisions.
|
||||
#'
|
||||
#' @section Cache scoping:
|
||||
#'
|
||||
#' There are a number of different ways you may want to scope the cache. For
|
||||
#' example, you may want each user session to have their own plot cache, or
|
||||
#' you may want each run of the application to have a cache (shared among
|
||||
#' possibly multiple simultaneous user sessions), or you may want to have a
|
||||
#' cache that persists even after the application is shut down and started
|
||||
#' again.
|
||||
#'
|
||||
#' To control the scope of the cache, use the \code{cache} parameter. There
|
||||
#' are two ways of having Shiny automatically create and clean up the disk
|
||||
#' cache.
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{1}{To scope the cache to one run of a Shiny application (shared
|
||||
#' among possibly multiple user sessions), use \code{cache="app"}. This
|
||||
#' is the default. The cache will be shared across multiple sessions, so
|
||||
#' there is potentially a large performance benefit if there are many users
|
||||
#' of the application. When the application stops running, the cache will
|
||||
#' be deleted. If plots cannot be safely shared across users, this should
|
||||
#' not be used.}
|
||||
#' \item{2}{To scope the cache to one session, use \code{cache="session"}.
|
||||
#' When a new user session starts -- in other words, when a web browser
|
||||
#' visits the Shiny application -- a new cache will be created on disk
|
||||
#' for that session. When the session ends, the cache will be deleted.
|
||||
#' The cache will not be shared across multiple sessions.}
|
||||
#' }
|
||||
#'
|
||||
#' If either \code{"app"} or \code{"session"} is used, the cache will be 10 MB
|
||||
#' in size, and will be stored stored in memory, using a
|
||||
#' \code{\link{memoryCache}} object. Note that the cache space will be shared
|
||||
#' among all cached plots within a single application or session.
|
||||
#'
|
||||
#' In some cases, you may want more control over the caching behavior. For
|
||||
#' example, you may want to use a larger or smaller cache, share a cache
|
||||
#' among multiple R processes, or you may want the cache to persist across
|
||||
#' multiple runs of an application, or even across multiple R processes.
|
||||
#'
|
||||
#' To use different settings for an application-scoped cache, you can call
|
||||
#' \code{\link{shinyOptions}()} at the top of your app.R, server.R, or
|
||||
#' global.R. For example, this will create a cache with 20 MB of space
|
||||
#' instead of the default 10 MB:
|
||||
#' \preformatted{
|
||||
#' shinyOptions(cache = memoryCache(size = 20e6))
|
||||
#' }
|
||||
#'
|
||||
#' To use different settings for a session-scoped cache, you can call
|
||||
#' \code{\link{shinyOptions}()} at the top of your server function. To use
|
||||
#' the session-scoped cache, you must also call \code{renderCachedPlot} with
|
||||
#' \code{cache="session"}. This will create a 20 MB cache for the session:
|
||||
#' \preformatted{
|
||||
#' function(input, output, session) {
|
||||
#' shinyOptions(cache = memoryCache(size = 20e6))
|
||||
#'
|
||||
#' output$plot <- renderCachedPlot(
|
||||
#' ...,
|
||||
#' cache = "session"
|
||||
#' )
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' If you want to create a cache that is shared across multiple concurrent
|
||||
#' R processes, you can use a \code{\link{diskCache}}. You can create an
|
||||
#' application-level shared cache by putting this at the top of your app.R,
|
||||
#' server.R, or global.R:
|
||||
#' \preformatted{
|
||||
#' shinyOptions(cache = diskCache(file.path(dirname(tempdir()), "myapp-cache"))
|
||||
#' }
|
||||
#'
|
||||
#' This will create a subdirectory in your system temp directory named
|
||||
#' \code{myapp-cache} (replace \code{myapp-cache} with a unique name of
|
||||
#' your choosing). On most platforms, this directory will be removed when
|
||||
#' your system reboots. This cache will persist across multiple starts and
|
||||
#' stops of the R process, as long as you do not reboot.
|
||||
#'
|
||||
#' To have the cache persist even across multiple reboots, you can create the
|
||||
#' cache in a location outside of the temp directory. For example, it could
|
||||
#' be a subdirectory of the application:
|
||||
#' \preformatted{
|
||||
#' shinyOptions(cache = diskCache("./myapp-cache"))
|
||||
#' }
|
||||
#'
|
||||
#' In this case, resetting the cache will have to be done manually, by deleting
|
||||
#' the directory.
|
||||
#'
|
||||
#' You can also scope a cache to just one plot, or selected plots. To do that,
|
||||
#' create a \code{\link{memoryCache}} or \code{\link{diskCache}}, and pass it
|
||||
#' as the \code{cache} argument of \code{renderCachedPlot}.
|
||||
#'
|
||||
#' @section Interactive plots:
|
||||
#'
|
||||
#' \code{renderCachedPlot} can be used to create interactive plots. See
|
||||
#' \code{\link{plotOutput}} for more information and examples.
|
||||
#'
|
||||
#'
|
||||
#' @inheritParams renderPlot
|
||||
#' @param cacheKeyExpr An expression that returns a cache key. This key should
|
||||
#' be a unique identifier for a plot: the assumption is that if the cache key
|
||||
#' is the same, then the plot will be the same.
|
||||
#' @param sizePolicy A function that takes two arguments, \code{width} and
|
||||
#' \code{height}, and returns a list with \code{width} and \code{height}. The
|
||||
#' purpose is to round the actual pixel dimensions from the browser to some
|
||||
#' other dimensions, so that this will not generate and cache images of every
|
||||
#' possible pixel dimension. See \code{\link{sizeGrowthRatio}} for more
|
||||
#' information on the default sizing policy.
|
||||
#' @param res The resolution of the PNG, in pixels per inch.
|
||||
#' @param cache The scope of the cache, or a cache object. This can be
|
||||
#' \code{"app"} (the default), \code{"session"}, or a cache object like
|
||||
#' a \code{\link{diskCache}}. See the Cache Scoping section for more
|
||||
#' information.
|
||||
#'
|
||||
#' @seealso See \code{\link{renderPlot}} for the regular, non-cached version of
|
||||
#' this function. For more about configuring caches, see
|
||||
#' \code{\link{memoryCache}} and \code{\link{diskCache}}.
|
||||
#'
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' # A basic example that uses the default app-scoped memory cache.
|
||||
#' # The cache will be shared among all simultaneous users of the application.
|
||||
#' shinyApp(
|
||||
#' fluidPage(
|
||||
#' sidebarLayout(
|
||||
#' sidebarPanel(
|
||||
#' sliderInput("n", "Number of points", 4, 32, value = 8, step = 4)
|
||||
#' ),
|
||||
#' mainPanel(plotOutput("plot"))
|
||||
#' )
|
||||
#' ),
|
||||
#' function(input, output, session) {
|
||||
#' output$plot <- renderCachedPlot({
|
||||
#' Sys.sleep(2) # Add an artificial delay
|
||||
#' seqn <- seq_len(input$n)
|
||||
#' plot(mtcars$wt[seqn], mtcars$mpg[seqn],
|
||||
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
#' },
|
||||
#' cacheKeyExpr = { list(input$n) }
|
||||
#' )
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#'
|
||||
#'
|
||||
#' # An example uses a data object shared across sessions. mydata() is part of
|
||||
#' # the cache key, so when its value changes, plots that were previously
|
||||
#' # stored in the cache will no longer be used (unless mydata() changes back
|
||||
#' # to its previous value).
|
||||
#' mydata <- reactiveVal(data.frame(x = rnorm(400), y = rnorm(400)))
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' sidebarLayout(
|
||||
#' sidebarPanel(
|
||||
#' sliderInput("n", "Number of points", 50, 400, 100, step = 50),
|
||||
#' actionButton("newdata", "New data")
|
||||
#' ),
|
||||
#' mainPanel(
|
||||
#' plotOutput("plot")
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' observeEvent(input$newdata, {
|
||||
#' mydata(data.frame(x = rnorm(400), y = rnorm(400)))
|
||||
#' })
|
||||
#'
|
||||
#' output$plot <- renderCachedPlot(
|
||||
#' {
|
||||
#' Sys.sleep(2)
|
||||
#' d <- mydata()
|
||||
#' seqn <- seq_len(input$n)
|
||||
#' plot(d$x[seqn], d$y[seqn], xlim = range(d$x), ylim = range(d$y))
|
||||
#' },
|
||||
#' cacheKeyExpr = { list(input$n, mydata()) },
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#'
|
||||
#' # A basic application with two plots, where each plot in each session has
|
||||
#' # a separate cache.
|
||||
#' shinyApp(
|
||||
#' fluidPage(
|
||||
#' sidebarLayout(
|
||||
#' sidebarPanel(
|
||||
#' sliderInput("n", "Number of points", 4, 32, value = 8, step = 4)
|
||||
#' ),
|
||||
#' mainPanel(
|
||||
#' plotOutput("plot1"),
|
||||
#' plotOutput("plot2")
|
||||
#' )
|
||||
#' )
|
||||
#' ),
|
||||
#' function(input, output, session) {
|
||||
#' output$plot1 <- renderCachedPlot({
|
||||
#' Sys.sleep(2) # Add an artificial delay
|
||||
#' seqn <- seq_len(input$n)
|
||||
#' plot(mtcars$wt[seqn], mtcars$mpg[seqn],
|
||||
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
#' },
|
||||
#' cacheKeyExpr = { list(input$n) },
|
||||
#' cache = memoryCache()
|
||||
#' )
|
||||
#' output$plot2 <- renderCachedPlot({
|
||||
#' Sys.sleep(2) # Add an artificial delay
|
||||
#' seqn <- seq_len(input$n)
|
||||
#' plot(mtcars$wt[seqn], mtcars$mpg[seqn],
|
||||
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
#' },
|
||||
#' cacheKeyExpr = { list(input$n) },
|
||||
#' cache = memoryCache()
|
||||
#' )
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#' }
|
||||
#'
|
||||
#' \dontrun{
|
||||
#' # At the top of app.R, this set the application-scoped cache to be a memory
|
||||
#' # cache that is 20 MB in size, and where cached objects expire after one
|
||||
#' # hour.
|
||||
#' shinyOptions(cache = memoryCache(max_size = 20e6, max_age = 3600))
|
||||
#'
|
||||
#' # At the top of app.R, this set the application-scoped cache to be a disk
|
||||
#' # cache that can be shared among multiple concurrent R processes, and is
|
||||
#' # deleted when the system reboots.
|
||||
#' shinyOptions(cache = diskCache(file.path(dirname(tempdir()), "myapp-cache"))
|
||||
#'
|
||||
#' # At the top of app.R, this set the application-scoped cache to be a disk
|
||||
#' # cache that can be shared among multiple concurrent R processes, and
|
||||
#' # persists on disk across reboots.
|
||||
#' shinyOptions(cache = diskCache("./myapp-cache"))
|
||||
#'
|
||||
#' # At the top of the server function, this set the session-scoped cache to be
|
||||
#' # a memory cache that is 5 MB in size.
|
||||
#' server <- function(input, output, session) {
|
||||
#' shinyOptions(cache = memoryCache(max_size = 5e6))
|
||||
#'
|
||||
#' output$plot <- renderCachedPlot(
|
||||
#' ...,
|
||||
#' cache = "session"
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' }
|
||||
#' @export
|
||||
renderCachedPlot <- function(expr,
|
||||
cacheKeyExpr,
|
||||
sizePolicy = sizeGrowthRatio(width = 400, height = 400, growthRate = 1.2),
|
||||
res = 72,
|
||||
cache = "app",
|
||||
...,
|
||||
outputArgs = list()
|
||||
) {
|
||||
|
||||
# This ..stacktraceon is matched by a ..stacktraceoff.. when plotFunc
|
||||
# is called
|
||||
installExprFunction(expr, "func", parent.frame(), quoted = FALSE, ..stacktraceon = TRUE)
|
||||
# This is so that the expr doesn't re-execute by itself; it needs to be
|
||||
# triggered by the cache key (or width/height) changing.
|
||||
isolatedFunc <- function() isolate(func())
|
||||
|
||||
args <- list(...)
|
||||
|
||||
cacheKeyExpr <- substitute(cacheKeyExpr)
|
||||
# The real cache key we'll use also includes width, height, res, pixelratio.
|
||||
# This is just the part supplied by the user.
|
||||
userCacheKey <- reactive(cacheKeyExpr, env = parent.frame(), quoted = TRUE, label = "userCacheKey")
|
||||
|
||||
ensureCacheSetup <- function() {
|
||||
# For our purposes, cache objects must support these methods.
|
||||
isCacheObject <- function(x) {
|
||||
# Use tryCatch in case the object does not support `$`.
|
||||
tryCatch(
|
||||
is.function(x$get) && is.function(x$set),
|
||||
error = function(e) FALSE
|
||||
)
|
||||
}
|
||||
|
||||
if (isCacheObject(cache)) {
|
||||
# If `cache` is already a cache object, do nothing
|
||||
return()
|
||||
|
||||
} else if (identical(cache, "app")) {
|
||||
cache <<- getShinyOption("cache")
|
||||
|
||||
} else if (identical(cache, "session")) {
|
||||
cache <<- session$cache
|
||||
|
||||
} else {
|
||||
stop('`cache` must either be "app", "session", or a cache object with methods, `$get`, and `$set`.')
|
||||
}
|
||||
}
|
||||
|
||||
# The width and height of the plot to draw, given from sizePolicy. These
|
||||
# values get filled by an observer below.
|
||||
fitDims <- reactiveValues(width = NULL, height = NULL)
|
||||
|
||||
resizeObserver <- NULL
|
||||
ensureResizeObserver <- function() {
|
||||
if (!is.null(resizeObserver))
|
||||
return()
|
||||
|
||||
# Given the actual width/height of the image in the browser, this gets the
|
||||
# width/height from sizePolicy() and pushes those values into `fitDims`.
|
||||
# It's done this way so that the `fitDims` only change (and cause
|
||||
# invalidations) when the rendered image size changes, and not every time
|
||||
# the browser's <img> tag changes size.
|
||||
doResizeCheck <- function() {
|
||||
width <- session$clientData[[paste0('output_', outputName, '_width')]]
|
||||
height <- session$clientData[[paste0('output_', outputName, '_height')]]
|
||||
|
||||
if (is.null(width)) width <- 0
|
||||
if (is.null(height)) height <- 0
|
||||
|
||||
rect <- sizePolicy(c(width, height))
|
||||
fitDims$width <- rect[1]
|
||||
fitDims$height <- rect[2]
|
||||
}
|
||||
|
||||
# Run it once immediately, then set up the observer
|
||||
isolate(doResizeCheck())
|
||||
|
||||
resizeObserver <<- observe(doResizeCheck())
|
||||
}
|
||||
|
||||
# Vars to store session and output, so that they can be accessed from
|
||||
# the plotObj() reactive.
|
||||
session <- NULL
|
||||
outputName <- NULL
|
||||
|
||||
|
||||
drawReactive <- reactive(label = "plotObj", {
|
||||
hybrid_chain(
|
||||
# Depend on the user cache key, even though we don't use the value. When
|
||||
# it changes, it can cause the drawReactive to re-execute. (Though
|
||||
# drawReactive will not necessarily re-execute -- it must be called from
|
||||
# renderFunc, which happens only if there's a cache miss.)
|
||||
userCacheKey(),
|
||||
function(userCacheKeyValue) {
|
||||
# Get width/height, but don't depend on them.
|
||||
isolate({
|
||||
width <- fitDims$width
|
||||
height <- fitDims$height
|
||||
})
|
||||
|
||||
pixelratio <- session$clientData$pixelratio %OR% 1
|
||||
|
||||
do.call("drawPlot", c(
|
||||
list(
|
||||
name = outputName,
|
||||
session = session,
|
||||
func = isolatedFunc,
|
||||
width = width,
|
||||
height = height,
|
||||
pixelratio = pixelratio,
|
||||
res = res
|
||||
),
|
||||
args
|
||||
))
|
||||
},
|
||||
catch = function(reason) {
|
||||
# Non-isolating read. A common reason for errors in plotting is because
|
||||
# the dimensions are too small. By taking a dependency on width/height,
|
||||
# we can try again if the plot output element changes size.
|
||||
fitDims$width
|
||||
fitDims$height
|
||||
|
||||
# Propagate the error
|
||||
stop(reason)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
# This function is the one that's returned from renderPlot(), and gets
|
||||
# wrapped in an observer when the output value is assigned.
|
||||
renderFunc <- function(shinysession, name, ...) {
|
||||
outputName <<- name
|
||||
session <<- shinysession
|
||||
ensureCacheSetup()
|
||||
ensureResizeObserver()
|
||||
|
||||
hybrid_chain(
|
||||
# This use of the userCacheKey() sets up the reactive dependency that
|
||||
# causes plot re-draw events. These may involve pulling from the cache,
|
||||
# replaying a display list, or re-executing user code.
|
||||
userCacheKey(),
|
||||
function(userCacheKeyResult) {
|
||||
width <- fitDims$width
|
||||
height <- fitDims$height
|
||||
pixelratio <- session$clientData$pixelratio %OR% 1
|
||||
|
||||
key <- digest::digest(list(outputName, userCacheKeyResult, width, height, res, pixelratio), "xxhash64")
|
||||
|
||||
plotObj <- cache$get(key)
|
||||
|
||||
# First look in cache.
|
||||
# Case 1. cache hit.
|
||||
if (!is.key_missing(plotObj)) {
|
||||
return(list(
|
||||
cacheHit = TRUE,
|
||||
key = key,
|
||||
plotObj = plotObj,
|
||||
width = width,
|
||||
height = height,
|
||||
pixelratio = pixelratio
|
||||
))
|
||||
}
|
||||
|
||||
# If not in cache, hybrid_chain call to drawReactive
|
||||
#
|
||||
# Two more possible cases:
|
||||
# 2. drawReactive will re-execute and return a plot that's the
|
||||
# correct size.
|
||||
# 3. It will not re-execute, but it will return the previous value,
|
||||
# which is the wrong size. It will include a valid display list
|
||||
# which can be used by resizeSavedPlot.
|
||||
hybrid_chain(
|
||||
drawReactive(),
|
||||
function(drawReactiveResult) {
|
||||
# Pass along the key for caching in the next stage
|
||||
list(
|
||||
cacheHit = FALSE,
|
||||
key = key,
|
||||
plotObj = drawReactiveResult,
|
||||
width = width,
|
||||
height = height,
|
||||
pixelratio = pixelratio
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
function(result) {
|
||||
width <- result$width
|
||||
height <- result$height
|
||||
pixelratio <- result$pixelratio
|
||||
|
||||
# Three possibilities when we get here:
|
||||
# 1. There was a cache hit. No need to set a value in the cache.
|
||||
# 2. There was a cache miss, and the plotObj is already the correct
|
||||
# size (because drawReactive re-executed). In this case, we need
|
||||
# to cache it.
|
||||
# 3. There was a cache miss, and the plotObj was not the corect size.
|
||||
# In this case, we need to replay the display list, and then cache
|
||||
# the result.
|
||||
if (!result$cacheHit) {
|
||||
# If the image is already the correct size, this just returns the
|
||||
# object unchanged.
|
||||
result$plotObj <- do.call("resizeSavedPlot", c(
|
||||
list(
|
||||
name,
|
||||
shinysession,
|
||||
result$plotObj,
|
||||
width,
|
||||
height,
|
||||
pixelratio,
|
||||
res
|
||||
),
|
||||
args
|
||||
))
|
||||
|
||||
# Save a cached copy of the plotObj. The recorded displaylist for
|
||||
# the plot can't be serialized and restored properly within the same
|
||||
# R session, so we NULL it out before saving. (The image data and
|
||||
# other metadata be saved and restored just fine.) Displaylists can
|
||||
# also be very large (~1.5MB for a basic ggplot), and they would not
|
||||
# be commonly used. Note that displaylist serialization was fixed in
|
||||
# revision 74506 (2e6c669), and should be in R 3.6. A MemoryCache
|
||||
# doesn't need to serialize objects, so it could actually save a
|
||||
# display list, but for the reasons listed previously, it's
|
||||
# generally not worth it.
|
||||
# The plotResult is not the same as the recordedPlot (it is used to
|
||||
# retrieve coordmap information for ggplot2 objects) but it is only
|
||||
# used in conjunction with the recordedPlot, and we'll remove it
|
||||
# because it can be quite large.
|
||||
result$plotObj$plotResult <- NULL
|
||||
result$plotObj$recordedPlot <- NULL
|
||||
cache$set(result$key, result$plotObj)
|
||||
}
|
||||
|
||||
img <- result$plotObj$img
|
||||
# Replace exact pixel dimensions; instead, the max-height and
|
||||
# max-width will be set to 100% from CSS.
|
||||
img$class <- "shiny-scalable"
|
||||
img$width <- NULL
|
||||
img$height <- NULL
|
||||
|
||||
img
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
# If renderPlot isn't going to adapt to the height of the div, then the
|
||||
# div needs to adapt to the height of renderPlot. By default, plotOutput
|
||||
# sets the height to 400px, so to make it adapt we need to override it
|
||||
# with NULL.
|
||||
outputFunc <- plotOutput
|
||||
formals(outputFunc)['height'] <- list(NULL)
|
||||
|
||||
markRenderFunction(outputFunc, renderFunc, outputArgs = outputArgs)
|
||||
}
|
||||
|
||||
|
||||
#' Create a sizing function that grows at a given ratio
|
||||
#'
|
||||
#' Returns a function which takes a two-element vector representing an input
|
||||
#' width and height, and returns a two-element vector of width and height. The
|
||||
#' possible widths are the base width times the growthRate to any integer power.
|
||||
#' For example, with a base width of 500 and growth rate of 1.25, the possible
|
||||
#' widths include 320, 400, 500, 625, 782, and so on, both smaller and larger.
|
||||
#' Sizes are rounded up to the next pixel. Heights are computed the same way as
|
||||
#' widths.
|
||||
#'
|
||||
#' @param width,height Base width and height.
|
||||
#' @param growthRate Growth rate multiplier.
|
||||
#'
|
||||
#' @seealso This is to be used with \code{\link{renderCachedPlot}}.
|
||||
#'
|
||||
#' @examples
|
||||
#' f <- sizeGrowthRatio(500, 500, 1.25)
|
||||
#' f(c(400, 400))
|
||||
#' f(c(500, 500))
|
||||
#' f(c(530, 550))
|
||||
#' f(c(625, 700))
|
||||
#'
|
||||
#' @export
|
||||
sizeGrowthRatio <- function(width = 400, height = 400, growthRate = 1.2) {
|
||||
round_dim_up <- function(x, base, rate) {
|
||||
power <- ceiling(log(x / base, rate))
|
||||
ceiling(base * rate^power)
|
||||
}
|
||||
|
||||
function(dims) {
|
||||
if (length(dims) != 2) {
|
||||
stop("dims must be a vector with two numbers, for width and height.")
|
||||
}
|
||||
c(
|
||||
round_dim_up(dims[1], width, growthRate),
|
||||
round_dim_up(dims[2], height, growthRate)
|
||||
)
|
||||
}
|
||||
}
|
||||
262
R/render-plot.R
262
R/render-plot.R
@@ -133,12 +133,10 @@ renderPlot <- function(expr, width='auto', height='auto', res=72, ...,
|
||||
function(result) {
|
||||
dims <- getDims()
|
||||
pixelratio <- session$clientData$pixelratio %OR% 1
|
||||
result <- do.call("resizeSavedPlot", c(
|
||||
do.call("resizeSavedPlot", c(
|
||||
list(name, shinysession, result, dims$width, dims$height, pixelratio, res),
|
||||
args
|
||||
))
|
||||
|
||||
result$img
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -156,25 +154,23 @@ renderPlot <- function(expr, width='auto', height='auto', res=72, ...,
|
||||
resizeSavedPlot <- function(name, session, result, width, height, pixelratio, res, ...) {
|
||||
if (result$img$width == width && result$img$height == height &&
|
||||
result$pixelratio == pixelratio && result$res == res) {
|
||||
return(result)
|
||||
return(result$img)
|
||||
}
|
||||
|
||||
coordmap <- NULL
|
||||
outfile <- plotPNG(function() {
|
||||
grDevices::replayPlot(result$recordedPlot)
|
||||
coordmap <<- getCoordmap(result$plotResult, width*pixelratio, height*pixelratio, res*pixelratio)
|
||||
coordmap <<- getCoordmap(result$plotResult, width, height, pixelratio, res)
|
||||
}, width = width*pixelratio, height = height*pixelratio, res = res*pixelratio, ...)
|
||||
on.exit(unlink(outfile), add = TRUE)
|
||||
|
||||
result$img <- list(
|
||||
img <- list(
|
||||
src = session$fileUrl(name, outfile, contentType = "image/png"),
|
||||
width = width,
|
||||
height = height,
|
||||
coordmap = coordmap,
|
||||
error = attr(coordmap, "error", exact = TRUE)
|
||||
)
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
drawPlot <- function(name, session, func, width, height, pixelratio, res, ...) {
|
||||
@@ -231,7 +227,7 @@ drawPlot <- function(name, session, func, width, height, pixelratio, res, ...) {
|
||||
list(
|
||||
plotResult = value,
|
||||
recordedPlot = grDevices::recordPlot(),
|
||||
coordmap = getCoordmap(value, width*pixelratio, height*pixelratio, res*pixelratio),
|
||||
coordmap = getCoordmap(value, width, height, pixelratio, res),
|
||||
pixelratio = pixelratio,
|
||||
res = res
|
||||
)
|
||||
@@ -251,7 +247,6 @@ drawPlot <- function(name, session, func, width, height, pixelratio, res, ...) {
|
||||
# Get coordmap error message if present
|
||||
error = attr(result$coordmap, "error", exact = TRUE)
|
||||
))
|
||||
|
||||
result
|
||||
},
|
||||
finally = function() {
|
||||
@@ -284,26 +279,22 @@ custom_print.ggplot <- function(x) {
|
||||
# below. For base graphics:
|
||||
# plot(mtcars$wt, mtcars$mpg)
|
||||
# str(getPrevPlotCoordmap(400, 300))
|
||||
# List of 2
|
||||
# $ panels:List of 1
|
||||
# ..$ :List of 4
|
||||
# .. ..$ domain :List of 4
|
||||
# .. .. ..$ left : num 1.36
|
||||
# .. .. ..$ right : num 5.58
|
||||
# .. .. ..$ bottom: num 9.46
|
||||
# .. .. ..$ top : num 34.8
|
||||
# .. ..$ range :List of 4
|
||||
# .. .. ..$ left : num 65.6
|
||||
# .. .. ..$ right : num 366
|
||||
# .. .. ..$ bottom: num 238
|
||||
# .. .. ..$ top : num 48.2
|
||||
# .. ..$ log :List of 2
|
||||
# .. .. ..$ x: NULL
|
||||
# .. .. ..$ y: NULL
|
||||
# .. ..$ mapping: Named list()
|
||||
# $ dims :List of 2
|
||||
# ..$ width : num 400
|
||||
# ..$ height: num 300
|
||||
# List of 1
|
||||
# $ :List of 4
|
||||
# ..$ domain :List of 4
|
||||
# .. ..$ left : num 1.36
|
||||
# .. ..$ right : num 5.58
|
||||
# .. ..$ bottom: num 9.46
|
||||
# .. ..$ top : num 34.8
|
||||
# ..$ range :List of 4
|
||||
# .. ..$ left : num 50.4
|
||||
# .. ..$ right : num 373
|
||||
# .. ..$ bottom: num 199
|
||||
# .. ..$ top : num 79.6
|
||||
# ..$ log :List of 2
|
||||
# .. ..$ x: NULL
|
||||
# .. ..$ y: NULL
|
||||
# ..$ mapping: Named list()
|
||||
#
|
||||
# For ggplot2, first you need to define the print.ggplot function from inside
|
||||
# renderPlot, then use it to print the plot:
|
||||
@@ -322,33 +313,29 @@ custom_print.ggplot <- function(x) {
|
||||
# }
|
||||
#
|
||||
# p <- print(ggplot(mtcars, aes(wt, mpg)) + geom_point())
|
||||
# str(getGgplotCoordmap(p, 400, 300, 72))
|
||||
# List of 2
|
||||
# $ panels:List of 1
|
||||
# ..$ :List of 8
|
||||
# .. ..$ panel : num 1
|
||||
# .. ..$ row : num 1
|
||||
# .. ..$ col : num 1
|
||||
# .. ..$ panel_vars: Named list()
|
||||
# .. ..$ log :List of 2
|
||||
# .. .. ..$ x: NULL
|
||||
# .. .. ..$ y: NULL
|
||||
# .. ..$ domain :List of 4
|
||||
# .. .. ..$ left : num 1.32
|
||||
# .. .. ..$ right : num 5.62
|
||||
# .. .. ..$ bottom: num 9.22
|
||||
# .. .. ..$ top : num 35.1
|
||||
# .. ..$ mapping :List of 2
|
||||
# .. .. ..$ x: chr "wt"
|
||||
# .. .. ..$ y: chr "mpg"
|
||||
# .. ..$ range :List of 4
|
||||
# .. .. ..$ left : num 33.3
|
||||
# .. .. ..$ right : num 355
|
||||
# .. .. ..$ bottom: num 328
|
||||
# .. .. ..$ top : num 5.48
|
||||
# $ dims :List of 2
|
||||
# ..$ width : num 400
|
||||
# ..$ height: num 300
|
||||
# str(getGgplotCoordmap(p, 1, 72))
|
||||
# List of 1
|
||||
# $ :List of 10
|
||||
# ..$ panel : int 1
|
||||
# ..$ row : int 1
|
||||
# ..$ col : int 1
|
||||
# ..$ panel_vars: Named list()
|
||||
# ..$ log :List of 2
|
||||
# .. ..$ x: NULL
|
||||
# .. ..$ y: NULL
|
||||
# ..$ domain :List of 4
|
||||
# .. ..$ left : num 1.32
|
||||
# .. ..$ right : num 5.62
|
||||
# .. ..$ bottom: num 9.22
|
||||
# .. ..$ top : num 35.1
|
||||
# ..$ mapping :List of 2
|
||||
# .. ..$ x: chr "wt"
|
||||
# .. ..$ y: chr "mpg"
|
||||
# ..$ range :List of 4
|
||||
# .. ..$ left : num 40.8
|
||||
# .. ..$ right : num 446
|
||||
# .. ..$ bottom: num 263
|
||||
# .. ..$ top : num 14.4
|
||||
#
|
||||
# With a faceted ggplot2 plot, the outer list contains two objects, each of
|
||||
# which represents one panel. In this example, there is one panelvar, but there
|
||||
@@ -356,63 +343,59 @@ custom_print.ggplot <- function(x) {
|
||||
# mtc <- mtcars
|
||||
# mtc$am <- factor(mtc$am)
|
||||
# p <- print(ggplot(mtc, aes(wt, mpg)) + geom_point() + facet_wrap(~ am))
|
||||
# str(getGgplotCoordmap(p, 400, 300, 72))
|
||||
# str(getGgplotCoordmap(p, 1, 72))
|
||||
# List of 2
|
||||
# $ panels:List of 2
|
||||
# ..$ :List of 8
|
||||
# .. ..$ panel : num 1
|
||||
# .. ..$ row : int 1
|
||||
# .. ..$ col : int 1
|
||||
# .. ..$ panel_vars:List of 1
|
||||
# .. .. ..$ panelvar1: Factor w/ 2 levels "0","1": 1
|
||||
# .. ..$ log :List of 2
|
||||
# .. .. ..$ x: NULL
|
||||
# .. .. ..$ y: NULL
|
||||
# .. ..$ domain :List of 4
|
||||
# .. .. ..$ left : num 1.32
|
||||
# .. .. ..$ right : num 5.62
|
||||
# .. .. ..$ bottom: num 9.22
|
||||
# .. .. ..$ top : num 35.1
|
||||
# .. ..$ mapping :List of 3
|
||||
# .. .. ..$ x : chr "wt"
|
||||
# .. .. ..$ y : chr "mpg"
|
||||
# .. .. ..$ panelvar1: chr "am"
|
||||
# .. ..$ range :List of 4
|
||||
# .. .. ..$ left : num 33.3
|
||||
# .. .. ..$ right : num 191
|
||||
# .. .. ..$ bottom: num 328
|
||||
# .. .. ..$ top : num 23.1
|
||||
# ..$ :List of 8
|
||||
# .. ..$ panel : num 2
|
||||
# .. ..$ row : int 1
|
||||
# .. ..$ col : int 2
|
||||
# .. ..$ panel_vars:List of 1
|
||||
# .. .. ..$ panelvar1: Factor w/ 2 levels "0","1": 2
|
||||
# .. ..$ log :List of 2
|
||||
# .. .. ..$ x: NULL
|
||||
# .. .. ..$ y: NULL
|
||||
# .. ..$ domain :List of 4
|
||||
# .. .. ..$ left : num 1.32
|
||||
# .. .. ..$ right : num 5.62
|
||||
# .. .. ..$ bottom: num 9.22
|
||||
# .. .. ..$ top : num 35.1
|
||||
# .. ..$ mapping :List of 3
|
||||
# .. .. ..$ x : chr "wt"
|
||||
# .. .. ..$ y : chr "mpg"
|
||||
# .. .. ..$ panelvar1: chr "am"
|
||||
# .. ..$ range :List of 4
|
||||
# .. .. ..$ left : num 197
|
||||
# .. .. ..$ right : num 355
|
||||
# .. .. ..$ bottom: num 328
|
||||
# .. .. ..$ top : num 23.1
|
||||
# $ dims :List of 2
|
||||
# ..$ width : num 400
|
||||
# ..$ height: num 300
|
||||
# $ :List of 10
|
||||
# ..$ panel : int 1
|
||||
# ..$ row : int 1
|
||||
# ..$ col : int 1
|
||||
# ..$ panel_vars:List of 1
|
||||
# .. ..$ panelvar1: Factor w/ 2 levels "0","1": 1
|
||||
# ..$ log :List of 2
|
||||
# .. ..$ x: NULL
|
||||
# .. ..$ y: NULL
|
||||
# ..$ domain :List of 4
|
||||
# .. ..$ left : num 1.32
|
||||
# .. ..$ right : num 5.62
|
||||
# .. ..$ bottom: num 9.22
|
||||
# .. ..$ top : num 35.1
|
||||
# ..$ mapping :List of 3
|
||||
# .. ..$ x : chr "wt"
|
||||
# .. ..$ y : chr "mpg"
|
||||
# .. ..$ panelvar1: chr "am"
|
||||
# ..$ range :List of 4
|
||||
# .. ..$ left : num 45.6
|
||||
# .. ..$ right : num 317
|
||||
# .. ..$ bottom: num 251
|
||||
# .. ..$ top : num 35.7
|
||||
# $ :List of 10
|
||||
# ..$ panel : int 2
|
||||
# ..$ row : int 1
|
||||
# ..$ col : int 2
|
||||
# ..$ panel_vars:List of 1
|
||||
# .. ..$ panelvar1: Factor w/ 2 levels "0","1": 2
|
||||
# ..$ log :List of 2
|
||||
# .. ..$ x: NULL
|
||||
# .. ..$ y: NULL
|
||||
# ..$ domain :List of 4
|
||||
# .. ..$ left : num 1.32
|
||||
# .. ..$ right : num 5.62
|
||||
# .. ..$ bottom: num 9.22
|
||||
# .. ..$ top : num 35.1
|
||||
# ..$ mapping :List of 3
|
||||
# .. ..$ x : chr "wt"
|
||||
# .. ..$ y : chr "mpg"
|
||||
# .. ..$ panelvar1: chr "am"
|
||||
# ..$ range :List of 4
|
||||
# .. ..$ left : num 322
|
||||
# .. ..$ right : num 594
|
||||
# .. ..$ bottom: num 251
|
||||
# .. ..$ top : num 35.7
|
||||
|
||||
|
||||
getCoordmap <- function(x, width, height, res) {
|
||||
getCoordmap <- function(x, width, height, pixelratio, res) {
|
||||
if (inherits(x, "ggplot_build_gtable")) {
|
||||
getGgplotCoordmap(x, width, height, res)
|
||||
getGgplotCoordmap(x, pixelratio, res)
|
||||
} else {
|
||||
getPrevPlotCoordmap(width, height)
|
||||
}
|
||||
@@ -432,7 +415,7 @@ getPrevPlotCoordmap <- function(width, height) {
|
||||
}
|
||||
|
||||
# Wrapped in double list because other types of plots can have multiple panels.
|
||||
panel_info <- list(list(
|
||||
list(list(
|
||||
# Bounds of the plot area, in data space
|
||||
domain = list(
|
||||
left = usrCoords[1],
|
||||
@@ -456,43 +439,27 @@ getPrevPlotCoordmap <- function(width, height) {
|
||||
# (not an array) in JSON.
|
||||
mapping = list(x = NULL)[0]
|
||||
))
|
||||
|
||||
list(
|
||||
panels = panel_info,
|
||||
dims = list(
|
||||
width = width,
|
||||
height =height
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
# Given a ggplot_build_gtable object, return a coordmap for it.
|
||||
getGgplotCoordmap <- function(p, width, height, res) {
|
||||
getGgplotCoordmap <- function(p, pixelratio, res) {
|
||||
if (!inherits(p, "ggplot_build_gtable"))
|
||||
return(NULL)
|
||||
|
||||
tryCatch({
|
||||
# Get info from built ggplot object
|
||||
panel_info <- find_panel_info(p$build)
|
||||
info <- find_panel_info(p$build)
|
||||
|
||||
# Get ranges from gtable - it's possible for this to return more elements than
|
||||
# info, because it calculates positions even for panels that aren't present.
|
||||
# This can happen with facet_wrap.
|
||||
ranges <- find_panel_ranges(p$gtable, res)
|
||||
ranges <- find_panel_ranges(p$gtable, pixelratio, res)
|
||||
|
||||
for (i in seq_along(panel_info)) {
|
||||
panel_info[[i]]$range <- ranges[[i]]
|
||||
for (i in seq_along(info)) {
|
||||
info[[i]]$range <- ranges[[i]]
|
||||
}
|
||||
|
||||
return(
|
||||
list(
|
||||
panels = panel_info,
|
||||
dims = list(
|
||||
width = width,
|
||||
height = height
|
||||
)
|
||||
)
|
||||
)
|
||||
return(info)
|
||||
|
||||
}, error = function(e) {
|
||||
# If there was an error extracting info from the ggplot object, just return
|
||||
@@ -519,11 +486,13 @@ find_panel_info <- function(b) {
|
||||
# This is for ggplot2>2.2.1, after an API was introduced for extracting
|
||||
# information about the plot object.
|
||||
find_panel_info_api <- function(b) {
|
||||
# Workaround for check NOTE, until ggplot2 >2.2.1 is released
|
||||
colon_colon <- `::`
|
||||
# Given a built ggplot object, return x and y domains (data space coords) for
|
||||
# each panel.
|
||||
layout <- ggplot2::summarise_layout(b)
|
||||
coord <- ggplot2::summarise_coord(b)
|
||||
layers <- ggplot2::summarise_layers(b)
|
||||
layout <- colon_colon("ggplot2", "summarise_layout")(b)
|
||||
coord <- colon_colon("ggplot2", "summarise_coord")(b)
|
||||
layers <- colon_colon("ggplot2", "summarise_layers")(b)
|
||||
|
||||
# Given x and y scale objects and a coord object, return a list that has
|
||||
# the bases of log transformations for x and y, or NULL if it's not a
|
||||
@@ -853,7 +822,7 @@ find_panel_info_non_api <- function(b, ggplot_format) {
|
||||
|
||||
|
||||
# Given a gtable object, return the x and y ranges (in pixel dimensions)
|
||||
find_panel_ranges <- function(g, res) {
|
||||
find_panel_ranges <- function(g, pixelratio, res) {
|
||||
# Given a vector of unit objects, return logical vector indicating which ones
|
||||
# are "null" units. These units use the remaining available width/height --
|
||||
# that is, the space not occupied by elements that have an absolute size.
|
||||
@@ -983,15 +952,26 @@ find_panel_ranges <- function(g, res) {
|
||||
layout <- layout[order(layout$t, layout$l), ]
|
||||
layout$panel <- seq_len(nrow(layout))
|
||||
|
||||
# When using a HiDPI client on a Linux server, the pixel
|
||||
# dimensions are doubled, so we have to divide the dimensions by
|
||||
# `pixelratio`. When a HiDPI client is used on a Mac server (with
|
||||
# the quartz device), the pixel dimensions _aren't_ doubled, even though
|
||||
# the image has double size. In the latter case we don't have to scale the
|
||||
# numbers down.
|
||||
pix_ratio <- 1
|
||||
if (!grepl("^quartz", names(grDevices::dev.cur()))) {
|
||||
pix_ratio <- pixelratio
|
||||
}
|
||||
|
||||
# Return list of lists, where each inner list has left, right, top, bottom
|
||||
# values for a panel
|
||||
lapply(seq_len(nrow(layout)), function(i) {
|
||||
p <- layout[i, , drop = FALSE]
|
||||
list(
|
||||
left = x_pos[p$l - 1],
|
||||
right = x_pos[p$r],
|
||||
bottom = y_pos[p$b],
|
||||
top = y_pos[p$t - 1]
|
||||
left = x_pos[p$l - 1] / pix_ratio,
|
||||
right = x_pos[p$r] / pix_ratio,
|
||||
bottom = y_pos[p$b] / pix_ratio,
|
||||
top = y_pos[p$t - 1] / pix_ratio
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
11
R/server.R
11
R/server.R
@@ -579,16 +579,12 @@ runApp <- function(appDir=getwd(),
|
||||
.globals$running <- FALSE
|
||||
}, add = TRUE)
|
||||
|
||||
# Enable per-app Shiny options, for shinyOptions() and getShinyOption().
|
||||
# Enable per-app Shiny options
|
||||
oldOptionSet <- .globals$options
|
||||
on.exit({
|
||||
.globals$options <- oldOptionSet
|
||||
},add = TRUE)
|
||||
|
||||
# A unique identifier associated with this run of this application. It is
|
||||
# shared across sessions.
|
||||
shinyOptions(appToken = createUniqueId(8))
|
||||
|
||||
# Make warnings print immediately
|
||||
# Set pool.scheduler to support pool package
|
||||
ops <- options(
|
||||
@@ -598,11 +594,6 @@ runApp <- function(appDir=getwd(),
|
||||
)
|
||||
on.exit(options(ops), add = TRUE)
|
||||
|
||||
# Set up default cache for app.
|
||||
if (is.null(getShinyOption("cache"))) {
|
||||
shinyOptions(cache = MemoryCache$new())
|
||||
}
|
||||
|
||||
appParts <- as.shiny.appobj(appDir)
|
||||
|
||||
# The lines below set some of the app's running options, which
|
||||
|
||||
86
R/shiny.R
86
R/shiny.R
@@ -445,8 +445,6 @@ ShinySession <- R6Class(
|
||||
testMode = FALSE, # Are we running in test mode?
|
||||
testExportExprs = list(),
|
||||
outputValues = list(), # Saved output values (for testing mode)
|
||||
currentOutputName = NULL, # Name of the currently-running output
|
||||
outputInfo = list(), # List of information for each output
|
||||
testSnapshotUrl = character(0),
|
||||
|
||||
sendResponse = function(requestMsg, value) {
|
||||
@@ -493,16 +491,6 @@ ShinySession <- R6Class(
|
||||
return(defaultValue)
|
||||
return(result)
|
||||
},
|
||||
withCurrentOutput = function(name, expr) {
|
||||
if (!is.null(private$currentOutputName)) {
|
||||
stop("Nested calls to withCurrentOutput() are not allowed.")
|
||||
}
|
||||
|
||||
promises::with_promise_domain(
|
||||
createVarPromiseDomain(private, "currentOutputName", name),
|
||||
expr
|
||||
)
|
||||
},
|
||||
shouldSuspend = function(name) {
|
||||
# Find corresponding hidden state clientData variable, with the format
|
||||
# "output_foo_hidden". (It comes from .clientdata_output_foo_hidden
|
||||
@@ -703,7 +691,6 @@ ShinySession <- R6Class(
|
||||
request = 'ANY', # Websocket request object
|
||||
singletons = character(0), # Tracks singleton HTML fragments sent to the page
|
||||
userData = 'environment',
|
||||
cache = NULL, # A cache object used in the session
|
||||
user = NULL,
|
||||
groups = NULL,
|
||||
|
||||
@@ -738,8 +725,6 @@ ShinySession <- R6Class(
|
||||
private$.outputs <- list()
|
||||
private$.outputOptions <- list()
|
||||
|
||||
self$cache <- MemoryCache$new()
|
||||
|
||||
private$bookmarkCallbacks <- Callbacks$new()
|
||||
private$bookmarkedCallbacks <- Callbacks$new()
|
||||
private$restoreCallbacks <- Callbacks$new()
|
||||
@@ -916,11 +901,9 @@ ShinySession <- R6Class(
|
||||
# Create subdir for this scope
|
||||
if (!is.null(state$dir)) {
|
||||
scopeState$dir <- file.path(state$dir, namespace)
|
||||
if (!dirExists(scopeState$dir)) {
|
||||
res <- dir.create(scopeState$dir)
|
||||
if (res == FALSE) {
|
||||
stop("Error creating subdirectory for scope ", namespace)
|
||||
}
|
||||
res <- dir.create(scopeState$dir)
|
||||
if (res == FALSE) {
|
||||
stop("Error creating subdirectory for scope ", namespace)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1088,11 +1071,7 @@ ShinySession <- R6Class(
|
||||
# to include the $then/$catch calls below?
|
||||
hybrid_chain(
|
||||
hybrid_chain(
|
||||
{
|
||||
private$withCurrentOutput(name, {
|
||||
shinyCallingHandlers(func())
|
||||
})
|
||||
},
|
||||
shinyCallingHandlers(func()),
|
||||
catch = function(cond) {
|
||||
if (inherits(cond, "shiny.custom.error")) {
|
||||
if (isTRUE(getOption("show.error.messages"))) printError(cond)
|
||||
@@ -1335,47 +1314,6 @@ ShinySession <- R6Class(
|
||||
}
|
||||
},
|
||||
|
||||
getCurrentOutputInfo = function() {
|
||||
name <- private$currentOutputName
|
||||
|
||||
tmp_info <- private$outputInfo[[name]] %OR% list(name = name)
|
||||
|
||||
# cd_names() returns names of all items in clientData, without taking a
|
||||
# reactive dependency. It is a function and it's memoized, so that we do
|
||||
# the (relatively) expensive isolate(names(...)) call only when needed,
|
||||
# and at most one time in this function.
|
||||
.cd_names <- NULL
|
||||
cd_names <- function() {
|
||||
if (is.null(.cd_names)) {
|
||||
.cd_names <<- isolate(names(self$clientData))
|
||||
}
|
||||
.cd_names
|
||||
}
|
||||
|
||||
# If we don't already have width for this output info, see if it's
|
||||
# present, and if so, add it.
|
||||
if (! ("width" %in% names(tmp_info)) ) {
|
||||
width_name <- paste0("output_", name, "_width")
|
||||
if (width_name %in% cd_names()) {
|
||||
tmp_info$width <- reactive({
|
||||
self$clientData[[width_name]]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (! ("height" %in% names(tmp_info)) ) {
|
||||
height_name <- paste0("output_", name, "_height")
|
||||
if (height_name %in% cd_names()) {
|
||||
tmp_info$height <- reactive({
|
||||
self$clientData[[height_name]]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private$outputInfo[[name]] <- tmp_info
|
||||
private$outputInfo[[name]]
|
||||
},
|
||||
|
||||
createBookmarkObservers = function() {
|
||||
# This registers observers for bookmarking to work.
|
||||
|
||||
@@ -2119,16 +2057,6 @@ outputOptions <- function(x, name, ...) {
|
||||
.subset2(x, 'impl')$outputOptions(name, ...)
|
||||
}
|
||||
|
||||
|
||||
#' Get information about the output that is currently being executed.
|
||||
#'
|
||||
#' @param session The current Shiny session.
|
||||
#'
|
||||
#' @export
|
||||
getCurrentOutputInfo <- function(session = getDefaultReactiveDomain()) {
|
||||
session$getCurrentOutputInfo()
|
||||
}
|
||||
|
||||
#' Add callbacks for Shiny session events
|
||||
#'
|
||||
#' These functions are for registering callbacks on Shiny session events.
|
||||
@@ -2197,9 +2125,7 @@ flushPendingSessions <- function() {
|
||||
#' called from within the server function, this will default to the current
|
||||
#' session, and the callback will be invoked when the current session ends. If
|
||||
#' \code{onStop} is called outside a server function, then the callback will
|
||||
#' be invoked with the application exits. If \code{NULL}, it is the same as
|
||||
#' calling \code{onStop} outside of the server function, and the callback will
|
||||
#' be invoked when the application exits.
|
||||
#' be invoked with the application exits.
|
||||
#'
|
||||
#'
|
||||
#' @seealso \code{\link{onSessionEnded}()} for the same functionality, but at
|
||||
@@ -2259,7 +2185,7 @@ flushPendingSessions <- function() {
|
||||
#' }
|
||||
#' @export
|
||||
onStop <- function(fun, session = getDefaultReactiveDomain()) {
|
||||
if (is.null(session)) {
|
||||
if (is.null(getDefaultReactiveDomain())) {
|
||||
return(.globals$onStopCallbacks$register(fun))
|
||||
} else {
|
||||
# Note: In the future if we allow scoping the onStop() callback to modules
|
||||
|
||||
19
R/utils.R
19
R/utils.R
@@ -269,25 +269,6 @@ dirExists <- function(paths) {
|
||||
file.exists(paths) & file.info(paths)$isdir
|
||||
}
|
||||
|
||||
# Removes empty directory (vectorized). This is needed because file.remove()
|
||||
# on Unix will remove empty directories, but on Windows, it will not. On
|
||||
# Windows, you would need to use unlink(recursive=TRUE), which is not very
|
||||
# safe. This function does it safely on Unix and Windows.
|
||||
dirRemove <- function(path) {
|
||||
for (p in path) {
|
||||
if (!dirExists(p)) {
|
||||
stop("Cannot remove non-existent directory ", p, ".")
|
||||
}
|
||||
if (length(dir(p, all.files = TRUE, no.. = TRUE)) != 0) {
|
||||
stop("Cannot remove non-empty directory ", p, ".")
|
||||
}
|
||||
result <- unlink(p, recursive = TRUE)
|
||||
if (result == 1) {
|
||||
stop("Error removing directory ", p, ".")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Attempt to join a path and relative path, and turn the result into a
|
||||
# (normalized) absolute path. The result will only be returned if it is an
|
||||
# existing file/directory and is a descendant of dir.
|
||||
|
||||
@@ -105,7 +105,6 @@ sd_section("Rendering functions",
|
||||
"Functions that you use in your application's server side code, assigning them to outputs that appear in your user interface.",
|
||||
c(
|
||||
"renderPlot",
|
||||
"renderCachedPlot",
|
||||
"renderText",
|
||||
"renderPrint",
|
||||
"renderDataTable",
|
||||
@@ -197,9 +196,7 @@ sd_section("Utility functions",
|
||||
"exprToFunction",
|
||||
"installExprFunction",
|
||||
"parseQueryString",
|
||||
"getCurrentOutputInfo",
|
||||
"plotPNG",
|
||||
"sizeGrowthRatio",
|
||||
"exportTestValues",
|
||||
"setSerializer",
|
||||
"snapshotExclude",
|
||||
@@ -210,10 +207,7 @@ sd_section("Utility functions",
|
||||
"shinyDeprecated",
|
||||
"serverInfo",
|
||||
"shiny-options",
|
||||
"onStop",
|
||||
"diskCache",
|
||||
"memoryCache",
|
||||
"key_missing"
|
||||
"onStop"
|
||||
)
|
||||
)
|
||||
sd_section("Plot interaction",
|
||||
|
||||
@@ -12,11 +12,6 @@ pre.shiny-text-output.noplaceholder:empty {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.shiny-image-output img.shiny-scalable, .shiny-plot-output img.shiny-scalable {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
#shiny-disconnected-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
8
inst/www/shared/shiny.min.js
vendored
8
inst/www/shared/shiny.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,13 +5,13 @@
|
||||
\alias{fixedPanel}
|
||||
\title{Panel with absolute positioning}
|
||||
\usage{
|
||||
absolutePanel(..., top = NULL, left = NULL, right = NULL,
|
||||
bottom = NULL, width = NULL, height = NULL, draggable = FALSE,
|
||||
fixed = FALSE, cursor = c("auto", "move", "default", "inherit"))
|
||||
|
||||
fixedPanel(..., top = NULL, left = NULL, right = NULL,
|
||||
bottom = NULL, width = NULL, height = NULL, draggable = FALSE,
|
||||
absolutePanel(..., top = NULL, left = NULL, right = NULL, bottom = NULL,
|
||||
width = NULL, height = NULL, draggable = FALSE, fixed = FALSE,
|
||||
cursor = c("auto", "move", "default", "inherit"))
|
||||
|
||||
fixedPanel(..., top = NULL, left = NULL, right = NULL, bottom = NULL,
|
||||
width = NULL, height = NULL, draggable = FALSE, cursor = c("auto",
|
||||
"move", "default", "inherit"))
|
||||
}
|
||||
\arguments{
|
||||
\item{...}{Attributes (named arguments) or children (unnamed arguments) that
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
\usage{
|
||||
bookmarkButton(label = "Bookmark...", icon = shiny::icon("link", lib =
|
||||
"glyphicon"),
|
||||
title = "Bookmark this application's state and get a URL for sharing.",
|
||||
..., id = "._bookmark_")
|
||||
title = "Bookmark this application's state and get a URL for sharing.", ...,
|
||||
id = "._bookmark_")
|
||||
}
|
||||
\arguments{
|
||||
\item{label}{The contents of the button or link--usually a text label, but
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
\alias{brushOpts}
|
||||
\title{Create an object representing brushing options}
|
||||
\usage{
|
||||
brushOpts(id = NULL, fill = "#9cf", stroke = "#036",
|
||||
opacity = 0.25, delay = 300, delayType = c("debounce", "throttle"),
|
||||
clip = TRUE, direction = c("xy", "x", "y"), resetOnNew = FALSE)
|
||||
brushOpts(id = NULL, fill = "#9cf", stroke = "#036", opacity = 0.25,
|
||||
delay = 300, delayType = c("debounce", "throttle"), clip = TRUE,
|
||||
direction = c("xy", "x", "y"), resetOnNew = FALSE)
|
||||
}
|
||||
\arguments{
|
||||
\item{id}{Input value name. For example, if the value is \code{"plot_brush"},
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
\title{Checkbox Group Input Control}
|
||||
\usage{
|
||||
checkboxGroupInput(inputId, label, choices = NULL, selected = NULL,
|
||||
inline = FALSE, width = NULL, choiceNames = NULL,
|
||||
choiceValues = NULL)
|
||||
inline = FALSE, width = NULL, choiceNames = NULL, choiceValues = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{inputId}{The \code{input} slot that will be used to access the value.}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
\alias{createRenderFunction}
|
||||
\title{Implement render functions}
|
||||
\usage{
|
||||
createRenderFunction(func, transform = function(value, session, name,
|
||||
...) value, outputFunc = NULL, outputArgs = NULL)
|
||||
createRenderFunction(func, transform = function(value, session, name, ...)
|
||||
value, outputFunc = NULL, outputArgs = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{func}{A function without parameters, that returns user data. If the
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
\title{Create date range input}
|
||||
\usage{
|
||||
dateRangeInput(inputId, label, start = NULL, end = NULL, min = NULL,
|
||||
max = NULL, format = "yyyy-mm-dd", startview = "month",
|
||||
weekstart = 0, language = "en", separator = " to ", width = NULL,
|
||||
autoclose = TRUE)
|
||||
max = NULL, format = "yyyy-mm-dd", startview = "month", weekstart = 0,
|
||||
language = "en", separator = " to ", width = NULL, autoclose = TRUE)
|
||||
}
|
||||
\arguments{
|
||||
\item{inputId}{The \code{input} slot that will be used to access the value.}
|
||||
|
||||
@@ -5,11 +5,9 @@
|
||||
\alias{throttle}
|
||||
\title{Slow down a reactive expression with debounce/throttle}
|
||||
\usage{
|
||||
debounce(r, millis, priority = 100,
|
||||
domain = getDefaultReactiveDomain())
|
||||
debounce(r, millis, priority = 100, domain = getDefaultReactiveDomain())
|
||||
|
||||
throttle(r, millis, priority = 100,
|
||||
domain = getDefaultReactiveDomain())
|
||||
throttle(r, millis, priority = 100, domain = getDefaultReactiveDomain())
|
||||
}
|
||||
\arguments{
|
||||
\item{r}{A reactive expression (that invalidates too often).}
|
||||
|
||||
239
man/diskCache.Rd
239
man/diskCache.Rd
@@ -1,239 +0,0 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/cache-disk.R
|
||||
\name{diskCache}
|
||||
\alias{diskCache}
|
||||
\title{Create a disk cache object}
|
||||
\usage{
|
||||
diskCache(dir = NULL, max_size = 10 * 1024^2, max_age = Inf,
|
||||
max_n = Inf, evict = c("lru", "fifo"), destroy_on_finalize = FALSE,
|
||||
missing = key_missing(), exec_missing = FALSE, logfile = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{dir}{Directory to store files for the cache. If \code{NULL} (the
|
||||
default) it will create and use a temporary directory.}
|
||||
|
||||
\item{max_size}{Maximum size of the cache, in bytes. If the cache exceeds
|
||||
this size, cached objects will be removed according to the value of the
|
||||
\code{evict}. Use \code{Inf} for no size limit.}
|
||||
|
||||
\item{max_age}{Maximum age of files in cache before they are evicted, in
|
||||
seconds. Use \code{Inf} for no age limit.}
|
||||
|
||||
\item{max_n}{Maximum number of objects in the cache. If the number of objects
|
||||
exceeds this value, then cached objects will be removed according to the
|
||||
value of \code{evict}. Use \code{Inf} for no limit of number of items.}
|
||||
|
||||
\item{evict}{The eviction policy to use to decide which objects are removed
|
||||
when a cache pruning occurs. Currently, \code{"lru"} and \code{"fifo"} are
|
||||
supported.}
|
||||
|
||||
\item{destroy_on_finalize}{If \code{TRUE}, then when the DiskCache object is
|
||||
garbage collected, the cache directory and all objects inside of it will be
|
||||
deleted from disk. If \code{FALSE} (the default), it will do nothing when
|
||||
finalized.}
|
||||
|
||||
\item{missing}{A value to return or a function to execute when
|
||||
\code{get(key)} is called but the key is not present in the cache. The
|
||||
default is a \code{\link{key_missing}} object. If it is a function to
|
||||
execute, the function must take one argument (the key), and you must also
|
||||
use \code{exec_missing = TRUE}. If it is a function, it is useful in most
|
||||
cases for it to throw an error, although another option is to return a
|
||||
value. If a value is returned, that value will in turn be returned by
|
||||
\code{get()}. See section Missing keys for more information.}
|
||||
|
||||
\item{exec_missing}{If \code{FALSE} (the default), then treat \code{missing}
|
||||
as a value to return when \code{get()} results in a cache miss. If
|
||||
\code{TRUE}, treat \code{missing} as a function to execute when
|
||||
\code{get()} results in a cache miss.}
|
||||
|
||||
\item{logfile}{An optional filename or connection object to where logging
|
||||
information will be written. To log to the console, use \code{stdout()}.}
|
||||
}
|
||||
\description{
|
||||
A disk cache object is a key-value store that saves the values as files in a
|
||||
directory on disk. Objects can be stored and retrieved using the \code{get()}
|
||||
and \code{set()} methods. Objects are automatically pruned from the cache
|
||||
according to the parameters \code{max_size}, \code{max_age}, \code{max_n},
|
||||
and \code{evict}.
|
||||
}
|
||||
\section{Missing Keys}{
|
||||
|
||||
|
||||
The \code{missing} and \code{exec_missing} parameters controls what happens
|
||||
when \code{get()} is called with a key that is not in the cache (a cache
|
||||
miss). The default behavior is to return a \code{\link{key_missing}}
|
||||
object. This is a \emph{sentinel value} that indicates that the key was not
|
||||
present in the cache. You can test if the returned value represents a
|
||||
missing key by using the \code{\link{is.key_missing}} function. You can
|
||||
also have \code{get()} return a different sentinel value, like \code{NULL}.
|
||||
If you want to throw an error on a cache miss, you can do so by providing a
|
||||
function for \code{missing} that takes one argument, the key, and also use
|
||||
\code{exec_missing=TRUE}.
|
||||
|
||||
When the cache is created, you can supply a value for \code{missing}, which
|
||||
sets the default value to be returned for missing values. It can also be
|
||||
overridden when \code{get()} is called, by supplying a \code{missing}
|
||||
argument. For example, if you use \code{cache$get("mykey", missing =
|
||||
NULL)}, it will return \code{NULL} if the key is not in the cache.
|
||||
|
||||
If your cache is configured so that \code{get()} returns a sentinel value
|
||||
to represent a cache miss, then \code{set} will also not allow you to store
|
||||
the sentinel value in the cache. It will throw an error if you attempt to
|
||||
do so.
|
||||
|
||||
Instead of returning the same sentinel value each time there is cache miss,
|
||||
the cache can execute a function each time \code{get()} encounters missing
|
||||
key. If the function returns a value, then \code{get()} will in turn return
|
||||
that value. However, a more common use is for the function to throw an
|
||||
error. If an error is thrown, then \code{get()} will not return a value.
|
||||
|
||||
To do this, pass a one-argument function to \code{missing}, and use
|
||||
\code{exec_missing=TRUE}. For example, if you want to throw an error that
|
||||
prints the missing key, you could do this:
|
||||
|
||||
\preformatted{
|
||||
diskCache(
|
||||
missing = function(key) {
|
||||
stop("Attempted to get missing key: ", key)
|
||||
},
|
||||
exec_missing = TRUE
|
||||
)
|
||||
}
|
||||
|
||||
If you use this, the code that calls \code{get()} should be wrapped with
|
||||
\code{\link{tryCatch}()} to gracefully handle missing keys.
|
||||
}
|
||||
|
||||
\section{Cache pruning}{
|
||||
|
||||
|
||||
Cache pruning occurs when \code{set()} is called, or it can be invoked
|
||||
manually by calling \code{prune()}.
|
||||
|
||||
The disk cache will throttle the pruning so that it does not happen on
|
||||
every call to \code{set()}, because the filesystem operations for checking
|
||||
the status of files can be slow. Instead, it will prune once in every 20
|
||||
calls to \code{set()}, or if at least 5 seconds have elapsed since the last
|
||||
prune occurred, whichever is first. These parameters are currently not
|
||||
customizable, but may be in the future.
|
||||
|
||||
When a pruning occurs, if there are any objects that are older than
|
||||
\code{max_age}, they will be removed.
|
||||
|
||||
The \code{max_size} and \code{max_n} parameters are applied to the cache as
|
||||
a whole, in contrast to \code{max_age}, which is applied to each object
|
||||
individually.
|
||||
|
||||
If the number of objects in the cache exceeds \code{max_n}, then objects
|
||||
will be removed from the cache according to the eviction policy, which is
|
||||
set with the \code{evict} parameter. Objects will be removed so that the
|
||||
number of items is \code{max_n}.
|
||||
|
||||
If the size of the objects in the cache exceeds \code{max_size}, then
|
||||
objects will be removed from the cache. Objects will be removed from the
|
||||
cache so that the total size remains under \code{max_size}. Note that the
|
||||
size is calculated using the size of the files, not the size of disk space
|
||||
used by the files -- these two values can differ because of files are
|
||||
stored in blocks on disk. For example, if the block size is 4096 bytes,
|
||||
then a file that is one byte in size will take 4096 bytes on disk.
|
||||
|
||||
Another time that objects can be removed from the cache is when
|
||||
\code{get()} is called. If the target object is older than \code{max_age},
|
||||
it will be removed and the cache will report it as a missing value.
|
||||
}
|
||||
|
||||
\section{Eviction policies}{
|
||||
|
||||
|
||||
If \code{max_n} or \code{max_size} are used, then objects will be removed
|
||||
from the cache according to an eviction policy. The available eviction
|
||||
policies are:
|
||||
|
||||
\describe{
|
||||
\item{\code{"lru"}}{
|
||||
Least Recently Used. The least recently used objects will be removed.
|
||||
This uses the filesystem's mtime property. When "lru" is used, each
|
||||
\code{get()} is called, it will update the file's mtime.
|
||||
}
|
||||
\item{\code{"fifo"}}{
|
||||
First-in-first-out. The oldest objects will be removed.
|
||||
}
|
||||
}
|
||||
|
||||
Both of these policies use files' mtime. Note that some filesystems (notably
|
||||
FAT) have poor mtime resolution. (atime is not used because support for
|
||||
atime is worse than mtime.)
|
||||
}
|
||||
|
||||
\section{Sharing among multiple processes}{
|
||||
|
||||
|
||||
The directory for a DiskCache can be shared among multiple R processes. To
|
||||
do this, each R process should have a DiskCache object that uses the same
|
||||
directory. Each DiskCache will do pruning independently of the others, so if
|
||||
they have different pruning parameters, then one DiskCache may remove cached
|
||||
objects before another DiskCache would do so.
|
||||
|
||||
Even though it is possible for multiple processes to share a DiskCache
|
||||
directory, this should not be done on networked file systems, because of
|
||||
slow performance of networked file systems can cause problems. If you need
|
||||
a high-performance shared cache, you can use one built on a database like
|
||||
Redis, SQLite, mySQL, or similar.
|
||||
|
||||
When multiple processes share a cache directory, there are some potential
|
||||
race conditions. For example, if your code calls \code{exists(key)} to check
|
||||
if an object is in the cache, and then call \code{get(key)}, the object may
|
||||
be removed from the cache in between those two calls, and \code{get(key)}
|
||||
will throw an error. Instead of calling the two functions, it is better to
|
||||
simply call \code{get(key)}, and use \code{tryCatch()} to handle the error
|
||||
that is thrown if the object is not in the cache. This effectively tests for
|
||||
existence and gets the object in one operation.
|
||||
|
||||
It is also possible for one processes to prune objects at the same time that
|
||||
another processes is trying to prune objects. If this happens, you may see
|
||||
a warning from \code{file.remove()} failing to remove a file that has
|
||||
already been deleted.
|
||||
}
|
||||
|
||||
\section{Methods}{
|
||||
|
||||
|
||||
A disk cache object has the following methods:
|
||||
|
||||
\describe{
|
||||
\item{\code{get(key, missing, exec_missing)}}{
|
||||
Returns the value associated with \code{key}. If the key is not in the
|
||||
cache, then it returns the value specified by \code{missing} or,
|
||||
\code{missing} is a function and \code{exec_missing=TRUE}, then
|
||||
executes \code{missing}. The function can throw an error or return the
|
||||
value. If either of these parameters are specified here, then they
|
||||
will override the defaults that were set when the DiskCache object was
|
||||
created. See section Missing Keys for more information.
|
||||
}
|
||||
\item{\code{set(key, value)}}{
|
||||
Stores the \code{key}-\code{value} pair in the cache.
|
||||
}
|
||||
\item{\code{exists(key)}}{
|
||||
Returns \code{TRUE} if the cache contains the key, otherwise
|
||||
\code{FALSE}.
|
||||
}
|
||||
\item{\code{size()}}{
|
||||
Returns the number of items currently in the cache.
|
||||
}
|
||||
\item{\code{keys()}}{
|
||||
Returns a character vector of all keys currently in the cache.
|
||||
}
|
||||
\item{\code{reset()}}{
|
||||
Clears all objects from the cache.
|
||||
}
|
||||
\item{\code{destroy()}}{
|
||||
Clears all objects in the cache, and removes the cache directory from
|
||||
disk.
|
||||
}
|
||||
\item{\code{prune()}}{
|
||||
Prunes the cache, using the parameters specified by \code{max_size},
|
||||
\code{max_age}, \code{max_n}, and \code{evict}.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
\alias{downloadHandler}
|
||||
\title{File Downloads}
|
||||
\usage{
|
||||
downloadHandler(filename, content, contentType = NA,
|
||||
outputArgs = list())
|
||||
downloadHandler(filename, content, contentType = NA, outputArgs = list())
|
||||
}
|
||||
\arguments{
|
||||
\item{filename}{A string of the filename, including extension, that the
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
\alias{fileInput}
|
||||
\title{File Upload Control}
|
||||
\usage{
|
||||
fileInput(inputId, label, multiple = FALSE, accept = NULL,
|
||||
width = NULL, buttonLabel = "Browse...",
|
||||
placeholder = "No file selected")
|
||||
fileInput(inputId, label, multiple = FALSE, accept = NULL, width = NULL,
|
||||
buttonLabel = "Browse...", placeholder = "No file selected")
|
||||
}
|
||||
\arguments{
|
||||
\item{inputId}{The \code{input} slot that will be used to access the value.}
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
\alias{fillPage}
|
||||
\title{Create a page that fills the window}
|
||||
\usage{
|
||||
fillPage(..., padding = 0, title = NULL, bootstrap = TRUE,
|
||||
theme = NULL)
|
||||
fillPage(..., padding = 0, title = NULL, bootstrap = TRUE, theme = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{...}{Elements to include within the page.}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/shiny.R
|
||||
\name{getCurrentOutputInfo}
|
||||
\alias{getCurrentOutputInfo}
|
||||
\title{Get information about the output that is currently being executed.}
|
||||
\usage{
|
||||
getCurrentOutputInfo(session = getDefaultReactiveDomain())
|
||||
}
|
||||
\arguments{
|
||||
\item{session}{The current Shiny session.}
|
||||
}
|
||||
\description{
|
||||
Get information about the output that is currently being executed.
|
||||
}
|
||||
@@ -4,8 +4,8 @@
|
||||
\alias{hoverOpts}
|
||||
\title{Create an object representing hover options}
|
||||
\usage{
|
||||
hoverOpts(id = NULL, delay = 300, delayType = c("debounce",
|
||||
"throttle"), clip = TRUE, nullOutside = TRUE)
|
||||
hoverOpts(id = NULL, delay = 300, delayType = c("debounce", "throttle"),
|
||||
clip = TRUE, nullOutside = TRUE)
|
||||
}
|
||||
\arguments{
|
||||
\item{id}{Input value name. For example, if the value is \code{"plot_hover"},
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
\alias{uiOutput}
|
||||
\title{Create an HTML output element}
|
||||
\usage{
|
||||
htmlOutput(outputId, inline = FALSE, container = if (inline) span else
|
||||
div, ...)
|
||||
htmlOutput(outputId, inline = FALSE, container = if (inline) span else div,
|
||||
...)
|
||||
|
||||
uiOutput(outputId, inline = FALSE, container = if (inline) span else
|
||||
div, ...)
|
||||
uiOutput(outputId, inline = FALSE, container = if (inline) span else div,
|
||||
...)
|
||||
}
|
||||
\arguments{
|
||||
\item{outputId}{output variable to read the value from}
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
\alias{installExprFunction}
|
||||
\title{Install an expression as a function}
|
||||
\usage{
|
||||
installExprFunction(expr, name, eval.env = parent.frame(2),
|
||||
quoted = FALSE, assign.env = parent.frame(1),
|
||||
label = deparse(sys.call(-1)[[1]]), wrappedWithLabel = TRUE,
|
||||
..stacktraceon = FALSE)
|
||||
installExprFunction(expr, name, eval.env = parent.frame(2), quoted = FALSE,
|
||||
assign.env = parent.frame(1), label = deparse(sys.call(-1)[[1]]),
|
||||
wrappedWithLabel = TRUE, ..stacktraceon = FALSE)
|
||||
}
|
||||
\arguments{
|
||||
\item{expr}{A quoted or unquoted expression}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/cache-utils.R
|
||||
\name{key_missing}
|
||||
\alias{key_missing}
|
||||
\alias{is.key_missing}
|
||||
\title{A Key Missing object}
|
||||
\usage{
|
||||
key_missing()
|
||||
|
||||
is.key_missing(x)
|
||||
}
|
||||
\arguments{
|
||||
\item{x}{An object to test.}
|
||||
}
|
||||
\description{
|
||||
A \code{key_missing} object represents a cache miss.
|
||||
}
|
||||
\seealso{
|
||||
\code{\link{diskCache}}, \code{\link{memoryCache}}.
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/cache-memory.R
|
||||
\name{memoryCache}
|
||||
\alias{memoryCache}
|
||||
\title{Create a memory cache object}
|
||||
\usage{
|
||||
memoryCache(max_size = 10 * 1024^2, max_age = Inf, max_n = Inf,
|
||||
evict = c("lru", "fifo"), missing = key_missing(),
|
||||
exec_missing = FALSE, logfile = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{max_size}{Maximum size of the cache, in bytes. If the cache exceeds
|
||||
this size, cached objects will be removed according to the value of the
|
||||
\code{evict}. Use \code{Inf} for no size limit.}
|
||||
|
||||
\item{max_age}{Maximum age of files in cache before they are evicted, in
|
||||
seconds. Use \code{Inf} for no age limit.}
|
||||
|
||||
\item{max_n}{Maximum number of objects in the cache. If the number of objects
|
||||
exceeds this value, then cached objects will be removed according to the
|
||||
value of \code{evict}. Use \code{Inf} for no limit of number of items.}
|
||||
|
||||
\item{evict}{The eviction policy to use to decide which objects are removed
|
||||
when a cache pruning occurs. Currently, \code{"lru"} and \code{"fifo"} are
|
||||
supported.}
|
||||
|
||||
\item{missing}{A value to return or a function to execute when
|
||||
\code{get(key)} is called but the key is not present in the cache. The
|
||||
default is a \code{\link{key_missing}} object. If it is a function to
|
||||
execute, the function must take one argument (the key), and you must also
|
||||
use \code{exec_missing = TRUE}. If it is a function, it is useful in most
|
||||
cases for it to throw an error, although another option is to return a
|
||||
value. If a value is returned, that value will in turn be returned by
|
||||
\code{get()}. See section Missing keys for more information.}
|
||||
|
||||
\item{exec_missing}{If \code{FALSE} (the default), then treat \code{missing}
|
||||
as a value to return when \code{get()} results in a cache miss. If
|
||||
\code{TRUE}, treat \code{missing} as a function to execute when
|
||||
\code{get()} results in a cache miss.}
|
||||
|
||||
\item{logfile}{An optional filename or connection object to where logging
|
||||
information will be written. To log to the console, use \code{stdout()}.}
|
||||
}
|
||||
\description{
|
||||
A memory cache object is a key-value store that saves the values in an
|
||||
environment. Objects can be stored and retrieved using the \code{get()} and
|
||||
\code{set()} methods. Objects are automatically pruned from the cache
|
||||
according to the parameters \code{max_size}, \code{max_age}, \code{max_n},
|
||||
and \code{evict}.
|
||||
}
|
||||
\details{
|
||||
In a \code{MemoryCache}, R objects are stored directly in the cache; they are
|
||||
not \emph{not} serialized before being stored in the cache. This contrasts
|
||||
with other cache types, like \code{\link{diskCache}}, where objects are
|
||||
serialized, and the serialized object is cached. This can result in some
|
||||
differences of behavior. For example, as long as an object is stored in a
|
||||
MemoryCache, it will not be garbage collected.
|
||||
}
|
||||
\section{Missing keys}{
|
||||
|
||||
The \code{missing} and \code{exec_missing} parameters controls what happens
|
||||
when \code{get()} is called with a key that is not in the cache (a cache
|
||||
miss). The default behavior is to return a \code{\link{key_missing}}
|
||||
object. This is a \emph{sentinel value} that indicates that the key was not
|
||||
present in the cache. You can test if the returned value represents a
|
||||
missing key by using the \code{\link{is.key_missing}} function. You can
|
||||
also have \code{get()} return a different sentinel value, like \code{NULL}.
|
||||
If you want to throw an error on a cache miss, you can do so by providing a
|
||||
function for \code{missing} that takes one argument, the key, and also use
|
||||
\code{exec_missing=TRUE}.
|
||||
|
||||
When the cache is created, you can supply a value for \code{missing}, which
|
||||
sets the default value to be returned for missing values. It can also be
|
||||
overridden when \code{get()} is called, by supplying a \code{missing}
|
||||
argument. For example, if you use \code{cache$get("mykey", missing =
|
||||
NULL)}, it will return \code{NULL} if the key is not in the cache.
|
||||
|
||||
If your cache is configured so that \code{get()} returns a sentinel value
|
||||
to represent a cache miss, then \code{set} will also not allow you to store
|
||||
the sentinel value in the cache. It will throw an error if you attempt to
|
||||
do so.
|
||||
|
||||
Instead of returning the same sentinel value each time there is cache miss,
|
||||
the cache can execute a function each time \code{get()} encounters missing
|
||||
key. If the function returns a value, then \code{get()} will in turn return
|
||||
that value. However, a more common use is for the function to throw an
|
||||
error. If an error is thrown, then \code{get()} will not return a value.
|
||||
|
||||
To do this, pass a one-argument function to \code{missing}, and use
|
||||
\code{exec_missing=TRUE}. For example, if you want to throw an error that
|
||||
prints the missing key, you could do this:
|
||||
|
||||
\preformatted{
|
||||
diskCache(
|
||||
missing = function(key) {
|
||||
stop("Attempted to get missing key: ", key)
|
||||
},
|
||||
exec_missing = TRUE
|
||||
)
|
||||
}
|
||||
|
||||
If you use this, the code that calls \code{get()} should be wrapped with
|
||||
\code{\link{tryCatch}()} to gracefully handle missing keys.
|
||||
}
|
||||
|
||||
\section{Cache pruning}{
|
||||
|
||||
|
||||
Cache pruning occurs when \code{set()} is called, or it can be invoked
|
||||
manually by calling \code{prune()}.
|
||||
|
||||
When a pruning occurs, if there are any objects that are older than
|
||||
\code{max_age}, they will be removed.
|
||||
|
||||
The \code{max_size} and \code{max_n} parameters are applied to the cache as
|
||||
a whole, in contrast to \code{max_age}, which is applied to each object
|
||||
individually.
|
||||
|
||||
If the number of objects in the cache exceeds \code{max_n}, then objects
|
||||
will be removed from the cache according to the eviction policy, which is
|
||||
set with the \code{evict} parameter. Objects will be removed so that the
|
||||
number of items is \code{max_n}.
|
||||
|
||||
If the size of the objects in the cache exceeds \code{max_size}, then
|
||||
objects will be removed from the cache. Objects will be removed from the
|
||||
cache so that the total size remains under \code{max_size}. Note that the
|
||||
size is calculated using the size of the files, not the size of disk space
|
||||
used by the files -- these two values can differ because of files are
|
||||
stored in blocks on disk. For example, if the block size is 4096 bytes,
|
||||
then a file that is one byte in size will take 4096 bytes on disk.
|
||||
|
||||
Another time that objects can be removed from the cache is when
|
||||
\code{get()} is called. If the target object is older than \code{max_age},
|
||||
it will be removed and the cache will report it as a missing value.
|
||||
}
|
||||
|
||||
\section{Eviction policies}{
|
||||
|
||||
|
||||
If \code{max_n} or \code{max_size} are used, then objects will be removed
|
||||
from the cache according to an eviction policy. The available eviction
|
||||
policies are:
|
||||
|
||||
\describe{
|
||||
\item{\code{"lru"}}{
|
||||
Least Recently Used. The least recently used objects will be removed.
|
||||
This uses the filesystem's atime property. Some filesystems do not
|
||||
support atime, or have a very low atime resolution. The DiskCache will
|
||||
check for atime support, and if the filesystem does not support atime,
|
||||
a warning will be issued and the "fifo" policy will be used instead.
|
||||
}
|
||||
\item{\code{"fifo"}}{
|
||||
First-in-first-out. The oldest objects will be removed.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
\section{Methods}{
|
||||
|
||||
|
||||
A disk cache object has the following methods:
|
||||
|
||||
\describe{
|
||||
\item{\code{get(key, missing, exec_missing)}}{
|
||||
Returns the value associated with \code{key}. If the key is not in the
|
||||
cache, then it returns the value specified by \code{missing} or,
|
||||
\code{missing} is a function and \code{exec_missing=TRUE}, then
|
||||
executes \code{missing}. The function can throw an error or return the
|
||||
value. If either of these parameters are specified here, then they
|
||||
will override the defaults that were set when the DiskCache object was
|
||||
created. See section Missing Keys for more information.
|
||||
}
|
||||
\item{\code{set(key, value)}}{
|
||||
Stores the \code{key}-\code{value} pair in the cache.
|
||||
}
|
||||
\item{\code{exists(key)}}{
|
||||
Returns \code{TRUE} if the cache contains the key, otherwise
|
||||
\code{FALSE}.
|
||||
}
|
||||
\item{\code{size()}}{
|
||||
Returns the number of items currently in the cache.
|
||||
}
|
||||
\item{\code{keys()}}{
|
||||
Returns a character vector of all keys currently in the cache.
|
||||
}
|
||||
\item{\code{reset()}}{
|
||||
Clears all objects from the cache.
|
||||
}
|
||||
\item{\code{destroy()}}{
|
||||
Clears all objects in the cache, and removes the cache directory from
|
||||
disk.
|
||||
}
|
||||
\item{\code{prune()}}{
|
||||
Prunes the cache, using the parameters specified by \code{max_size},
|
||||
\code{max_age}, \code{max_n}, and \code{evict}.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,9 @@
|
||||
\title{Create a page with a top level navigation bar}
|
||||
\usage{
|
||||
navbarPage(title, ..., id = NULL, selected = NULL,
|
||||
position = c("static-top", "fixed-top", "fixed-bottom"),
|
||||
header = NULL, footer = NULL, inverse = FALSE,
|
||||
collapsible = FALSE, collapsable, fluid = TRUE, responsive = NULL,
|
||||
theme = NULL, windowTitle = title)
|
||||
position = c("static-top", "fixed-top", "fixed-bottom"), header = NULL,
|
||||
footer = NULL, inverse = FALSE, collapsible = FALSE, collapsable,
|
||||
fluid = TRUE, responsive = NULL, theme = NULL, windowTitle = title)
|
||||
|
||||
navbarMenu(title, ..., menuName = title, icon = NULL)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
\alias{navlistPanel}
|
||||
\title{Create a navigation list panel}
|
||||
\usage{
|
||||
navlistPanel(..., id = NULL, selected = NULL, well = TRUE,
|
||||
fluid = TRUE, widths = c(4, 8))
|
||||
navlistPanel(..., id = NULL, selected = NULL, well = TRUE, fluid = TRUE,
|
||||
widths = c(4, 8))
|
||||
}
|
||||
\arguments{
|
||||
\item{...}{\code{\link{tabPanel}} elements to include in the navlist}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
\title{Find rows of data that are near a click/hover/double-click}
|
||||
\usage{
|
||||
nearPoints(df, coordinfo, xvar = NULL, yvar = NULL, panelvar1 = NULL,
|
||||
panelvar2 = NULL, threshold = 5, maxpoints = NULL,
|
||||
addDist = FALSE, allRows = FALSE)
|
||||
panelvar2 = NULL, threshold = 5, maxpoints = NULL, addDist = FALSE,
|
||||
allRows = FALSE)
|
||||
}
|
||||
\arguments{
|
||||
\item{df}{A data frame from which to select rows.}
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
\title{Create a reactive observer}
|
||||
\usage{
|
||||
observe(x, env = parent.frame(), quoted = FALSE, label = NULL,
|
||||
suspended = FALSE, priority = 0,
|
||||
domain = getDefaultReactiveDomain(), autoDestroy = TRUE,
|
||||
..stacktraceon = TRUE)
|
||||
suspended = FALSE, priority = 0, domain = getDefaultReactiveDomain(),
|
||||
autoDestroy = TRUE, ..stacktraceon = TRUE)
|
||||
}
|
||||
\arguments{
|
||||
\item{x}{An expression (quoted or unquoted). Any return value will be
|
||||
|
||||
@@ -7,15 +7,13 @@
|
||||
\usage{
|
||||
observeEvent(eventExpr, handlerExpr, event.env = parent.frame(),
|
||||
event.quoted = FALSE, handler.env = parent.frame(),
|
||||
handler.quoted = FALSE, label = NULL, suspended = FALSE,
|
||||
priority = 0, domain = getDefaultReactiveDomain(),
|
||||
autoDestroy = TRUE, ignoreNULL = TRUE, ignoreInit = FALSE,
|
||||
once = FALSE)
|
||||
handler.quoted = FALSE, label = NULL, suspended = FALSE, priority = 0,
|
||||
domain = getDefaultReactiveDomain(), autoDestroy = TRUE,
|
||||
ignoreNULL = TRUE, ignoreInit = FALSE, once = FALSE)
|
||||
|
||||
eventReactive(eventExpr, valueExpr, event.env = parent.frame(),
|
||||
event.quoted = FALSE, value.env = parent.frame(),
|
||||
value.quoted = FALSE, label = NULL,
|
||||
domain = getDefaultReactiveDomain(), ignoreNULL = TRUE,
|
||||
event.quoted = FALSE, value.env = parent.frame(), value.quoted = FALSE,
|
||||
label = NULL, domain = getDefaultReactiveDomain(), ignoreNULL = TRUE,
|
||||
ignoreInit = FALSE)
|
||||
}
|
||||
\arguments{
|
||||
|
||||
@@ -13,9 +13,7 @@ onStop(fun, session = getDefaultReactiveDomain())
|
||||
called from within the server function, this will default to the current
|
||||
session, and the callback will be invoked when the current session ends. If
|
||||
\code{onStop} is called outside a server function, then the callback will
|
||||
be invoked with the application exits. If \code{NULL}, it is the same as
|
||||
calling \code{onStop} outside of the server function, and the callback will
|
||||
be invoked when the application exits.}
|
||||
be invoked with the application exits.}
|
||||
}
|
||||
\value{
|
||||
A function which, if invoked, will cancel the callback.
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
\alias{imageOutput}
|
||||
\title{Create an plot or image output element}
|
||||
\usage{
|
||||
imageOutput(outputId, width = "100\%", height = "400px",
|
||||
click = NULL, dblclick = NULL, hover = NULL, hoverDelay = NULL,
|
||||
hoverDelayType = NULL, brush = NULL, clickId = NULL,
|
||||
hoverId = NULL, inline = FALSE)
|
||||
imageOutput(outputId, width = "100\%", height = "400px", click = NULL,
|
||||
dblclick = NULL, hover = NULL, hoverDelay = NULL,
|
||||
hoverDelayType = NULL, brush = NULL, clickId = NULL, hoverId = NULL,
|
||||
inline = FALSE)
|
||||
|
||||
plotOutput(outputId, width = "100\%", height = "400px", click = NULL,
|
||||
dblclick = NULL, hover = NULL, hoverDelay = NULL,
|
||||
hoverDelayType = NULL, brush = NULL, clickId = NULL,
|
||||
hoverId = NULL, inline = FALSE)
|
||||
hoverDelayType = NULL, brush = NULL, clickId = NULL, hoverId = NULL,
|
||||
inline = FALSE)
|
||||
}
|
||||
\arguments{
|
||||
\item{outputId}{output variable to read the plot/image from.}
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
\title{Create radio buttons}
|
||||
\usage{
|
||||
radioButtons(inputId, label, choices = NULL, selected = NULL,
|
||||
inline = FALSE, width = NULL, choiceNames = NULL,
|
||||
choiceValues = NULL)
|
||||
inline = FALSE, width = NULL, choiceNames = NULL, choiceValues = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{inputId}{The \code{input} slot that will be used to access the value.}
|
||||
|
||||
@@ -1,316 +0,0 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/render-cached-plot.R
|
||||
\name{renderCachedPlot}
|
||||
\alias{renderCachedPlot}
|
||||
\title{Plot output with cached images}
|
||||
\usage{
|
||||
renderCachedPlot(expr, cacheKeyExpr, sizePolicy = sizeGrowthRatio(width =
|
||||
400, height = 400, growthRate = 1.2), res = 72, cache = "app", ...,
|
||||
outputArgs = list())
|
||||
}
|
||||
\arguments{
|
||||
\item{expr}{An expression that generates a plot.}
|
||||
|
||||
\item{cacheKeyExpr}{An expression that returns a cache key. This key should
|
||||
be a unique identifier for a plot: the assumption is that if the cache key
|
||||
is the same, then the plot will be the same.}
|
||||
|
||||
\item{sizePolicy}{A function that takes two arguments, \code{width} and
|
||||
\code{height}, and returns a list with \code{width} and \code{height}. The
|
||||
purpose is to round the actual pixel dimensions from the browser to some
|
||||
other dimensions, so that this will not generate and cache images of every
|
||||
possible pixel dimension. See \code{\link{sizeGrowthRatio}} for more
|
||||
information on the default sizing policy.}
|
||||
|
||||
\item{res}{The resolution of the PNG, in pixels per inch.}
|
||||
|
||||
\item{cache}{The scope of the cache, or a cache object. This can be
|
||||
\code{"app"} (the default), \code{"session"}, or a cache object like
|
||||
a \code{\link{diskCache}}. See the Cache Scoping section for more
|
||||
information.}
|
||||
|
||||
\item{...}{Arguments to be passed through to \code{\link[grDevices]{png}}.
|
||||
These can be used to set the width, height, background color, etc.}
|
||||
|
||||
\item{outputArgs}{A list of arguments to be passed through to the implicit
|
||||
call to \code{\link{plotOutput}} when \code{renderPlot} is used in an
|
||||
interactive R Markdown document.}
|
||||
}
|
||||
\description{
|
||||
Renders a reactive plot, with plot images cached to disk.
|
||||
}
|
||||
\details{
|
||||
\code{expr} is an expression that generates a plot, similar to that in
|
||||
\code{renderPlot}. Unlike with \code{renderPlot}, this expression does not
|
||||
take reactive dependencies. It is re-executed only when the cache key
|
||||
changes.
|
||||
|
||||
\code{cacheKeyExpr} is an expression which, when evaluated, returns an object
|
||||
which will be serialized and hashed using the \code{\link[digest]{digest}}
|
||||
function to generate a string that will be used as a cache key. This key is
|
||||
used to identify the contents of the plot: if the cache key is the same as a
|
||||
previous time, it assumes that the plot is the same and can be retrieved from
|
||||
the cache.
|
||||
|
||||
This \code{cacheKeyExpr} is reactive, and so it will be re-evaluated when any
|
||||
upstream reactives are invalidated. This will also trigger re-execution of
|
||||
the plotting expression, \code{expr}.
|
||||
|
||||
The key should consist of "normal" R objects, like vectors and lists. Lists
|
||||
should in turn contain other normal R objects. If the key contains
|
||||
environments, external pointers, or reference objects -- or even if it has
|
||||
such objects attached as attributes -- then it is possible that it will
|
||||
change unpredictably even when you do not expect it to. Additionally, because
|
||||
the entire key is serialized and hashed, if it contains a very large object
|
||||
-- a large data set, for example -- there may be a noticeable performance
|
||||
penalty.
|
||||
|
||||
If you face these issues with the cache key, you can work around them by
|
||||
extracting out the important parts of the objects, and/or by converting them
|
||||
to normal R objects before returning them. Your expression could even
|
||||
serialize and hash that information in an efficient way and return a string,
|
||||
which will in turn be hashed (very quickly) by the
|
||||
\code{\link[digest]{digest}} function.
|
||||
|
||||
Internally, the result from \code{cacheKeyExpr} is combined with the name of
|
||||
the output (if you assign it to \code{output$plot1}, it will be combined
|
||||
with \code{"plot1"}) to form the actual key that is used. As a result, even
|
||||
if there are multiple plots that have the same \code{cacheKeyExpr}, they
|
||||
will not have cache key collisions.
|
||||
}
|
||||
\section{Cache scoping}{
|
||||
|
||||
|
||||
There are a number of different ways you may want to scope the cache. For
|
||||
example, you may want each user session to have their own plot cache, or
|
||||
you may want each run of the application to have a cache (shared among
|
||||
possibly multiple simultaneous user sessions), or you may want to have a
|
||||
cache that persists even after the application is shut down and started
|
||||
again.
|
||||
|
||||
To control the scope of the cache, use the \code{cache} parameter. There
|
||||
are two ways of having Shiny automatically create and clean up the disk
|
||||
cache.
|
||||
|
||||
\describe{
|
||||
\item{1}{To scope the cache to one run of a Shiny application (shared
|
||||
among possibly multiple user sessions), use \code{cache="app"}. This
|
||||
is the default. The cache will be shared across multiple sessions, so
|
||||
there is potentially a large performance benefit if there are many users
|
||||
of the application. When the application stops running, the cache will
|
||||
be deleted. If plots cannot be safely shared across users, this should
|
||||
not be used.}
|
||||
\item{2}{To scope the cache to one session, use \code{cache="session"}.
|
||||
When a new user session starts -- in other words, when a web browser
|
||||
visits the Shiny application -- a new cache will be created on disk
|
||||
for that session. When the session ends, the cache will be deleted.
|
||||
The cache will not be shared across multiple sessions.}
|
||||
}
|
||||
|
||||
If either \code{"app"} or \code{"session"} is used, the cache will be 10 MB
|
||||
in size, and will be stored stored in memory, using a
|
||||
\code{\link{memoryCache}} object. Note that the cache space will be shared
|
||||
among all cached plots within a single application or session.
|
||||
|
||||
In some cases, you may want more control over the caching behavior. For
|
||||
example, you may want to use a larger or smaller cache, share a cache
|
||||
among multiple R processes, or you may want the cache to persist across
|
||||
multiple runs of an application, or even across multiple R processes.
|
||||
|
||||
To use different settings for an application-scoped cache, you can call
|
||||
\code{\link{shinyOptions}()} at the top of your app.R, server.R, or
|
||||
global.R. For example, this will create a cache with 20 MB of space
|
||||
instead of the default 10 MB:
|
||||
\preformatted{
|
||||
shinyOptions(cache = memoryCache(size = 20e6))
|
||||
}
|
||||
|
||||
To use different settings for a session-scoped cache, you can call
|
||||
\code{\link{shinyOptions}()} at the top of your server function. To use
|
||||
the session-scoped cache, you must also call \code{renderCachedPlot} with
|
||||
\code{cache="session"}. This will create a 20 MB cache for the session:
|
||||
\preformatted{
|
||||
function(input, output, session) {
|
||||
shinyOptions(cache = memoryCache(size = 20e6))
|
||||
|
||||
output$plot <- renderCachedPlot(
|
||||
...,
|
||||
cache = "session"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
If you want to create a cache that is shared across multiple concurrent
|
||||
R processes, you can use a \code{\link{diskCache}}. You can create an
|
||||
application-level shared cache by putting this at the top of your app.R,
|
||||
server.R, or global.R:
|
||||
\preformatted{
|
||||
shinyOptions(cache = diskCache(file.path(dirname(tempdir()), "myapp-cache"))
|
||||
}
|
||||
|
||||
This will create a subdirectory in your system temp directory named
|
||||
\code{myapp-cache} (replace \code{myapp-cache} with a unique name of
|
||||
your choosing). On most platforms, this directory will be removed when
|
||||
your system reboots. This cache will persist across multiple starts and
|
||||
stops of the R process, as long as you do not reboot.
|
||||
|
||||
To have the cache persist even across multiple reboots, you can create the
|
||||
cache in a location outside of the temp directory. For example, it could
|
||||
be a subdirectory of the application:
|
||||
\preformatted{
|
||||
shinyOptions(cache = diskCache("./myapp-cache"))
|
||||
}
|
||||
|
||||
In this case, resetting the cache will have to be done manually, by deleting
|
||||
the directory.
|
||||
|
||||
You can also scope a cache to just one plot, or selected plots. To do that,
|
||||
create a \code{\link{memoryCache}} or \code{\link{diskCache}}, and pass it
|
||||
as the \code{cache} argument of \code{renderCachedPlot}.
|
||||
}
|
||||
|
||||
\section{Interactive plots}{
|
||||
|
||||
|
||||
\code{renderCachedPlot} can be used to create interactive plots. See
|
||||
\code{\link{plotOutput}} for more information and examples.
|
||||
}
|
||||
|
||||
\examples{
|
||||
## Only run examples in interactive R sessions
|
||||
if (interactive()) {
|
||||
|
||||
# A basic example that uses the default app-scoped memory cache.
|
||||
# The cache will be shared among all simultaneous users of the application.
|
||||
shinyApp(
|
||||
fluidPage(
|
||||
sidebarLayout(
|
||||
sidebarPanel(
|
||||
sliderInput("n", "Number of points", 4, 32, value = 8, step = 4)
|
||||
),
|
||||
mainPanel(plotOutput("plot"))
|
||||
)
|
||||
),
|
||||
function(input, output, session) {
|
||||
output$plot <- renderCachedPlot({
|
||||
Sys.sleep(2) # Add an artificial delay
|
||||
seqn <- seq_len(input$n)
|
||||
plot(mtcars$wt[seqn], mtcars$mpg[seqn],
|
||||
xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
},
|
||||
cacheKeyExpr = { list(input$n) }
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
# An example uses a data object shared across sessions. mydata() is part of
|
||||
# the cache key, so when its value changes, plots that were previously
|
||||
# stored in the cache will no longer be used (unless mydata() changes back
|
||||
# to its previous value).
|
||||
mydata <- reactiveVal(data.frame(x = rnorm(400), y = rnorm(400)))
|
||||
|
||||
ui <- fluidPage(
|
||||
sidebarLayout(
|
||||
sidebarPanel(
|
||||
sliderInput("n", "Number of points", 50, 400, 100, step = 50),
|
||||
actionButton("newdata", "New data")
|
||||
),
|
||||
mainPanel(
|
||||
plotOutput("plot")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
server <- function(input, output, session) {
|
||||
observeEvent(input$newdata, {
|
||||
mydata(data.frame(x = rnorm(400), y = rnorm(400)))
|
||||
})
|
||||
|
||||
output$plot <- renderCachedPlot(
|
||||
{
|
||||
Sys.sleep(2)
|
||||
d <- mydata()
|
||||
seqn <- seq_len(input$n)
|
||||
plot(d$x[seqn], d$y[seqn], xlim = range(d$x), ylim = range(d$y))
|
||||
},
|
||||
cacheKeyExpr = { list(input$n, mydata()) },
|
||||
)
|
||||
}
|
||||
|
||||
shinyApp(ui, server)
|
||||
|
||||
|
||||
# A basic application with two plots, where each plot in each session has
|
||||
# a separate cache.
|
||||
shinyApp(
|
||||
fluidPage(
|
||||
sidebarLayout(
|
||||
sidebarPanel(
|
||||
sliderInput("n", "Number of points", 4, 32, value = 8, step = 4)
|
||||
),
|
||||
mainPanel(
|
||||
plotOutput("plot1"),
|
||||
plotOutput("plot2")
|
||||
)
|
||||
)
|
||||
),
|
||||
function(input, output, session) {
|
||||
output$plot1 <- renderCachedPlot({
|
||||
Sys.sleep(2) # Add an artificial delay
|
||||
seqn <- seq_len(input$n)
|
||||
plot(mtcars$wt[seqn], mtcars$mpg[seqn],
|
||||
xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
},
|
||||
cacheKeyExpr = { list(input$n) },
|
||||
cache = memoryCache()
|
||||
)
|
||||
output$plot2 <- renderCachedPlot({
|
||||
Sys.sleep(2) # Add an artificial delay
|
||||
seqn <- seq_len(input$n)
|
||||
plot(mtcars$wt[seqn], mtcars$mpg[seqn],
|
||||
xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
},
|
||||
cacheKeyExpr = { list(input$n) },
|
||||
cache = memoryCache()
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
\dontrun{
|
||||
# At the top of app.R, this set the application-scoped cache to be a memory
|
||||
# cache that is 20 MB in size, and where cached objects expire after one
|
||||
# hour.
|
||||
shinyOptions(cache = memoryCache(max_size = 20e6, max_age = 3600))
|
||||
|
||||
# At the top of app.R, this set the application-scoped cache to be a disk
|
||||
# cache that can be shared among multiple concurrent R processes, and is
|
||||
# deleted when the system reboots.
|
||||
shinyOptions(cache = diskCache(file.path(dirname(tempdir()), "myapp-cache"))
|
||||
|
||||
# At the top of app.R, this set the application-scoped cache to be a disk
|
||||
# cache that can be shared among multiple concurrent R processes, and
|
||||
# persists on disk across reboots.
|
||||
shinyOptions(cache = diskCache("./myapp-cache"))
|
||||
|
||||
# At the top of the server function, this set the session-scoped cache to be
|
||||
# a memory cache that is 5 MB in size.
|
||||
server <- function(input, output, session) {
|
||||
shinyOptions(cache = memoryCache(max_size = 5e6))
|
||||
|
||||
output$plot <- renderCachedPlot(
|
||||
...,
|
||||
cache = "session"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
\seealso{
|
||||
See \code{\link{renderPlot}} for the regular, non-cached version of
|
||||
this function. For more about configuring caches, see
|
||||
\code{\link{memoryCache}} and \code{\link{diskCache}}.
|
||||
}
|
||||
@@ -5,8 +5,8 @@
|
||||
\title{Table output with the JavaScript library DataTables}
|
||||
\usage{
|
||||
renderDataTable(expr, options = NULL, searchDelay = 500,
|
||||
callback = "function(oTable) {}", escape = TRUE,
|
||||
env = parent.frame(), quoted = FALSE, outputArgs = list())
|
||||
callback = "function(oTable) {}", escape = TRUE, env = parent.frame(),
|
||||
quoted = FALSE, outputArgs = list())
|
||||
}
|
||||
\arguments{
|
||||
\item{expr}{An expression that returns a data frame or a matrix.}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
\alias{renderImage}
|
||||
\title{Image file output}
|
||||
\usage{
|
||||
renderImage(expr, env = parent.frame(), quoted = FALSE,
|
||||
deleteFile = TRUE, outputArgs = list())
|
||||
renderImage(expr, env = parent.frame(), quoted = FALSE, deleteFile = TRUE,
|
||||
outputArgs = list())
|
||||
}
|
||||
\arguments{
|
||||
\item{expr}{An expression that returns a list.}
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
\alias{renderUI}
|
||||
\title{UI Output}
|
||||
\usage{
|
||||
renderUI(expr, env = parent.frame(), quoted = FALSE,
|
||||
outputArgs = list())
|
||||
renderUI(expr, env = parent.frame(), quoted = FALSE, outputArgs = list())
|
||||
}
|
||||
\arguments{
|
||||
\item{expr}{An expression that returns a Shiny tag object, \code{\link{HTML}},
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
\alias{as.tags.shiny.appobj}
|
||||
\title{Create a Shiny app object}
|
||||
\usage{
|
||||
shinyApp(ui = NULL, server = NULL, onStart = NULL,
|
||||
options = list(), uiPattern = "/", enableBookmarking = NULL)
|
||||
shinyApp(ui = NULL, server = NULL, onStart = NULL, options = list(),
|
||||
uiPattern = "/", enableBookmarking = NULL)
|
||||
|
||||
shinyAppDir(appDir, options = list())
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/render-cached-plot.R
|
||||
\name{sizeGrowthRatio}
|
||||
\alias{sizeGrowthRatio}
|
||||
\title{Create a sizing function that grows at a given ratio}
|
||||
\usage{
|
||||
sizeGrowthRatio(width = 400, height = 400, growthRate = 1.2)
|
||||
}
|
||||
\arguments{
|
||||
\item{width, height}{Base width and height.}
|
||||
|
||||
\item{growthRate}{Growth rate multiplier.}
|
||||
}
|
||||
\description{
|
||||
Returns a function which takes a two-element vector representing an input
|
||||
width and height, and returns a two-element vector of width and height. The
|
||||
possible widths are the base width times the growthRate to any integer power.
|
||||
For example, with a base width of 500 and growth rate of 1.25, the possible
|
||||
widths include 320, 400, 500, 625, 782, and so on, both smaller and larger.
|
||||
Sizes are rounded up to the next pixel. Heights are computed the same way as
|
||||
widths.
|
||||
}
|
||||
\examples{
|
||||
f <- sizeGrowthRatio(500, 500, 1.25)
|
||||
f(c(400, 400))
|
||||
f(c(500, 500))
|
||||
f(c(530, 550))
|
||||
f(c(625, 700))
|
||||
|
||||
}
|
||||
\seealso{
|
||||
This is to be used with \code{\link{renderCachedPlot}}.
|
||||
}
|
||||
@@ -5,11 +5,10 @@
|
||||
\alias{animationOptions}
|
||||
\title{Slider Input Widget}
|
||||
\usage{
|
||||
sliderInput(inputId, label, min, max, value, step = NULL,
|
||||
round = FALSE, format = NULL, locale = NULL, ticks = TRUE,
|
||||
animate = FALSE, width = NULL, sep = ",", pre = NULL,
|
||||
post = NULL, timeFormat = NULL, timezone = NULL,
|
||||
dragRange = TRUE)
|
||||
sliderInput(inputId, label, min, max, value, step = NULL, round = FALSE,
|
||||
format = NULL, locale = NULL, ticks = TRUE, animate = FALSE,
|
||||
width = NULL, sep = ",", pre = NULL, post = NULL, timeFormat = NULL,
|
||||
timezone = NULL, dragRange = TRUE)
|
||||
|
||||
animationOptions(interval = 1000, loop = FALSE, playButton = NULL,
|
||||
pauseButton = NULL)
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
\alias{snapshotPreprocessInput}
|
||||
\title{Add a function for preprocessing an input before taking a test snapshot}
|
||||
\usage{
|
||||
snapshotPreprocessInput(inputId, fun,
|
||||
session = getDefaultReactiveDomain())
|
||||
snapshotPreprocessInput(inputId, fun, session = getDefaultReactiveDomain())
|
||||
}
|
||||
\arguments{
|
||||
\item{inputId}{Name of the input value.}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
\alias{tabsetPanel}
|
||||
\title{Create a tabset panel}
|
||||
\usage{
|
||||
tabsetPanel(..., id = NULL, selected = NULL, type = c("tabs",
|
||||
"pills"), position = NULL)
|
||||
tabsetPanel(..., id = NULL, selected = NULL, type = c("tabs", "pills"),
|
||||
position = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{...}{\code{\link{tabPanel}} elements to include in the tabset}
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
\alias{textAreaInput}
|
||||
\title{Create a textarea input control}
|
||||
\usage{
|
||||
textAreaInput(inputId, label, value = "", width = NULL,
|
||||
height = NULL, cols = NULL, rows = NULL, placeholder = NULL,
|
||||
resize = NULL)
|
||||
textAreaInput(inputId, label, value = "", width = NULL, height = NULL,
|
||||
cols = NULL, rows = NULL, placeholder = NULL, resize = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{inputId}{The \code{input} slot that will be used to access the value.}
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
\alias{textInput}
|
||||
\title{Create a text input control}
|
||||
\usage{
|
||||
textInput(inputId, label, value = "", width = NULL,
|
||||
placeholder = NULL)
|
||||
textInput(inputId, label, value = "", width = NULL, placeholder = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{inputId}{The \code{input} slot that will be used to access the value.}
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
\alias{textOutput}
|
||||
\title{Create a text output element}
|
||||
\usage{
|
||||
textOutput(outputId, container = if (inline) span else div,
|
||||
inline = FALSE)
|
||||
textOutput(outputId, container = if (inline) span else div, inline = FALSE)
|
||||
}
|
||||
\arguments{
|
||||
\item{outputId}{output variable to read the value from}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
\alias{updateCheckboxGroupInput}
|
||||
\title{Change the value of a checkbox group input on the client}
|
||||
\usage{
|
||||
updateCheckboxGroupInput(session, inputId, label = NULL,
|
||||
choices = NULL, selected = NULL, inline = FALSE,
|
||||
choiceNames = NULL, choiceValues = NULL)
|
||||
updateCheckboxGroupInput(session, inputId, label = NULL, choices = NULL,
|
||||
selected = NULL, inline = FALSE, choiceNames = NULL,
|
||||
choiceValues = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{session}{The \code{session} object passed to function given to
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
\alias{updateDateInput}
|
||||
\title{Change the value of a date input on the client}
|
||||
\usage{
|
||||
updateDateInput(session, inputId, label = NULL, value = NULL,
|
||||
min = NULL, max = NULL)
|
||||
updateDateInput(session, inputId, label = NULL, value = NULL, min = NULL,
|
||||
max = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{session}{The \code{session} object passed to function given to
|
||||
|
||||
@@ -7,10 +7,9 @@
|
||||
\title{Reporting progress (functional API)}
|
||||
\usage{
|
||||
withProgress(expr, min = 0, max = 1, value = min + (max - min) * 0.1,
|
||||
message = NULL, detail = NULL,
|
||||
style = getShinyOption("progress.style", default = "notification"),
|
||||
session = getDefaultReactiveDomain(), env = parent.frame(),
|
||||
quoted = FALSE)
|
||||
message = NULL, detail = NULL, style = getShinyOption("progress.style",
|
||||
default = "notification"), session = getDefaultReactiveDomain(),
|
||||
env = parent.frame(), quoted = FALSE)
|
||||
|
||||
setProgress(value = NULL, message = NULL, detail = NULL,
|
||||
session = getDefaultReactiveDomain())
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -249,17 +249,11 @@ function mapValues(obj, f) {
|
||||
const newObj = {};
|
||||
for (let key in obj) {
|
||||
if (obj.hasOwnProperty(key))
|
||||
newObj[key] = f(obj[key], key, obj);
|
||||
newObj[key] = f(obj[key]);
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
|
||||
// This is does the same as Number.isNaN, but that function unfortunately does
|
||||
// not exist in any version of IE.
|
||||
function isnan(x) {
|
||||
return typeof(x) === 'number' && isNaN(x);
|
||||
}
|
||||
|
||||
// Binary equality function used by the equal function.
|
||||
function _equal(x, y) {
|
||||
if ($.type(x) === "object" && $.type(y) === "object") {
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
context("Cache")
|
||||
|
||||
test_that("DiskCache: handling missing values", {
|
||||
d <- diskCache()
|
||||
expect_true(is.key_missing(d$get("abcd")))
|
||||
d$set("a", 100)
|
||||
expect_identical(d$get("a"), 100)
|
||||
expect_identical(d$get("y", missing = NULL), NULL)
|
||||
expect_error(
|
||||
d$get("y", missing = function(key) stop("Missing key: ", key), exec_missing = TRUE),
|
||||
"^Missing key: y$",
|
||||
)
|
||||
|
||||
d <- diskCache(missing = NULL)
|
||||
expect_true(is.null(d$get("abcd")))
|
||||
d$set("a", 100)
|
||||
expect_identical(d$get("a"), 100)
|
||||
expect_identical(d$get("y", missing = -1), -1)
|
||||
expect_error(
|
||||
d$get("y", missing = function(key) stop("Missing key: ", key), exec_missing = TRUE),
|
||||
"^Missing key: y$",
|
||||
)
|
||||
|
||||
|
||||
d <- diskCache(missing = function(key) stop("Missing key: ", key), exec_missing = TRUE)
|
||||
expect_error(d$get("abcd"), "^Missing key: abcd$")
|
||||
# When exec_missing=TRUE, should be able to set a value that's identical to
|
||||
# missing. Need to suppress warnings, because it will warn about reference
|
||||
# object (the environment captured by the function)
|
||||
d$set("x", NULL)
|
||||
suppressWarnings(d$set("x", function(key) stop("Missing key: ", key)))
|
||||
d$set("a", 100)
|
||||
expect_identical(d$get("a"), 100)
|
||||
expect_identical(d$get("y", missing = NULL, exec_missing = FALSE), NULL)
|
||||
expect_true(is.key_missing(d$get("y", missing = key_missing(), exec_missing = FALSE)))
|
||||
expect_identical(d$get("y", exec_missing = FALSE), function(key) stop("Missing key: ", key))
|
||||
expect_error(
|
||||
d$get("y", missing = function(key) stop("Missing key 2: ", key), exec_missing = TRUE),
|
||||
"^Missing key 2: y$",
|
||||
)
|
||||
|
||||
# Can't use exec_missing when missing is not a function
|
||||
expect_error(diskCache(missing = 1, exec_missing = TRUE))
|
||||
})
|
||||
|
||||
test_that("MemoryCache: handling missing values", {
|
||||
d <- memoryCache()
|
||||
expect_true(is.key_missing(d$get("abcd")))
|
||||
d$set("a", 100)
|
||||
expect_identical(d$get("a"), 100)
|
||||
expect_identical(d$get("y", missing = NULL), NULL)
|
||||
expect_error(
|
||||
d$get("y", missing = function(key) stop("Missing key: ", key), exec_missing = TRUE),
|
||||
"^Missing key: y$",
|
||||
)
|
||||
|
||||
d <- memoryCache(missing = NULL)
|
||||
expect_true(is.null(d$get("abcd")))
|
||||
d$set("a", 100)
|
||||
expect_identical(d$get("a"), 100)
|
||||
expect_identical(d$get("y", missing = -1), -1)
|
||||
expect_error(
|
||||
d$get("y", missing = function(key) stop("Missing key: ", key), exec_missing = TRUE),
|
||||
"^Missing key: y$",
|
||||
)
|
||||
|
||||
d <- memoryCache(missing = function(key) stop("Missing key: ", key), exec_missing = TRUE)
|
||||
expect_error(d$get("abcd"), "^Missing key: abcd$")
|
||||
# When exec_missing==TRUE, should be able to set a value that's identical to
|
||||
# missing.
|
||||
d$set("x", NULL)
|
||||
d$set("x", function(key) stop("Missing key: ", key))
|
||||
d$set("a", 100)
|
||||
expect_identical(d$get("a"), 100)
|
||||
expect_identical(d$get("y", missing = NULL, exec_missing = FALSE), NULL)
|
||||
expect_true(is.key_missing(d$get("y", missing = key_missing(), exec_missing = FALSE)))
|
||||
expect_error(
|
||||
d$get("y", missing = function(key) stop("Missing key 2: ", key), exec_missing = TRUE),
|
||||
"^Missing key 2: y$",
|
||||
)
|
||||
|
||||
# Can't create a cache with both missing and missing_f
|
||||
expect_error(memoryCache(missing = 1, exec_missing = TRUE))
|
||||
})
|
||||
@@ -21,52 +21,50 @@ test_that("ggplot coordmap", {
|
||||
scale_x_continuous(expand = c(0, 0)) +
|
||||
scale_y_continuous(expand = c(0, 0))
|
||||
png(tmpfile, width = 500, height = 500)
|
||||
m <- getGgplotCoordmap(print(p), 500, 500, 72)
|
||||
m <- getGgplotCoordmap(print(p), 1, 72)
|
||||
dev.off()
|
||||
|
||||
expect_equal(m$dims, list(width = 500, height = 500))
|
||||
|
||||
# Check mapping vars
|
||||
expect_equal(m$panels[[1]]$mapping, list(x = "xvar", y = "yvar"))
|
||||
expect_equal(m[[1]]$mapping, list(x = "xvar", y = "yvar"))
|
||||
# Check domain
|
||||
expect_equal(
|
||||
sortList(m$panels[[1]]$domain),
|
||||
sortList(m[[1]]$domain),
|
||||
sortList(list(left=0, right=5, bottom=10, top=20))
|
||||
)
|
||||
# Check for no log bases
|
||||
expect_equal(
|
||||
sortList(m$panels[[1]]$log),
|
||||
sortList(m[[1]]$log),
|
||||
sortList(list(x=NULL, y=NULL))
|
||||
)
|
||||
# panel_vars should be an empty named list
|
||||
expect_identical(m$panels[[1]]$panel_vars, list(a=1)[0])
|
||||
expect_identical(m[[1]]$panel_vars, list(a=1)[0])
|
||||
# Sanity check for ranges. Checking exact range values isn't feasible due to
|
||||
# variations in graphics devices, and possible changes to positioning in
|
||||
# ggplot2.
|
||||
expect_true(m$panels[[1]]$range$left > 20 && m$panels[[1]]$range$left < 70)
|
||||
expect_true(m$panels[[1]]$range$right > 480 && m$panels[[1]]$range$right < 499)
|
||||
expect_true(m$panels[[1]]$range$bottom > 450 && m$panels[[1]]$range$bottom < 490)
|
||||
expect_true(m$panels[[1]]$range$top > 1 && m$panels[[1]]$range$top < 20)
|
||||
expect_true(m[[1]]$range$left > 20 && m[[1]]$range$left < 70)
|
||||
expect_true(m[[1]]$range$right > 480 && m[[1]]$range$right < 499)
|
||||
expect_true(m[[1]]$range$bottom > 450 && m[[1]]$range$bottom < 490)
|
||||
expect_true(m[[1]]$range$top > 1 && m[[1]]$range$top < 20)
|
||||
|
||||
|
||||
# Scatterplot where aes() is declared in geom
|
||||
p <- ggplot(dat, aes(xvar)) + geom_point(aes(y=yvar))
|
||||
png(tmpfile)
|
||||
m <- getGgplotCoordmap(print(p), 500, 500, 72)
|
||||
m <- getGgplotCoordmap(print(p), 1, 72)
|
||||
dev.off()
|
||||
|
||||
# Check mapping vars
|
||||
expect_equal(sortList(m$panels[[1]]$mapping), list(x = "xvar", y = "yvar"))
|
||||
expect_equal(sortList(m[[1]]$mapping), list(x = "xvar", y = "yvar"))
|
||||
|
||||
|
||||
# Plot with an expression in aes, and a computed variable (histogram)
|
||||
p <- ggplot(dat, aes(xvar/2)) + geom_histogram(binwidth=1)
|
||||
png(tmpfile)
|
||||
m <- getGgplotCoordmap(print(p), 500, 500, 72)
|
||||
m <- getGgplotCoordmap(print(p), 1, 72)
|
||||
dev.off()
|
||||
|
||||
# Check mapping vars - no value for y
|
||||
expect_equal(sortList(m$panels[[1]]$mapping), list(x = "xvar/2", y = NULL))
|
||||
expect_equal(sortList(m[[1]]$mapping), list(x = "xvar/2", y = NULL))
|
||||
})
|
||||
|
||||
|
||||
@@ -83,38 +81,38 @@ test_that("ggplot coordmap with facet_wrap", {
|
||||
scale_y_continuous(expand = c(0, 0)) +
|
||||
facet_wrap(~ g, ncol = 2)
|
||||
png(tmpfile)
|
||||
m <- getGgplotCoordmap(print(p), 500, 400, 72)
|
||||
m <- getGgplotCoordmap(print(p), 1, 72)
|
||||
dev.off()
|
||||
|
||||
# Should have 3 panels
|
||||
expect_equal(length(m$panels), 3)
|
||||
expect_equal(m$panels[[1]]$panel, 1)
|
||||
expect_equal(m$panels[[1]]$row, 1)
|
||||
expect_equal(m$panels[[1]]$col, 1)
|
||||
expect_equal(m$panels[[2]]$panel, 2)
|
||||
expect_equal(m$panels[[2]]$row, 1)
|
||||
expect_equal(m$panels[[2]]$col, 2)
|
||||
expect_equal(m$panels[[3]]$panel, 3)
|
||||
expect_equal(m$panels[[3]]$row, 2)
|
||||
expect_equal(m$panels[[3]]$col, 1)
|
||||
expect_equal(length(m), 3)
|
||||
expect_equal(m[[1]]$panel, 1)
|
||||
expect_equal(m[[1]]$row, 1)
|
||||
expect_equal(m[[1]]$col, 1)
|
||||
expect_equal(m[[2]]$panel, 2)
|
||||
expect_equal(m[[2]]$row, 1)
|
||||
expect_equal(m[[2]]$col, 2)
|
||||
expect_equal(m[[3]]$panel, 3)
|
||||
expect_equal(m[[3]]$row, 2)
|
||||
expect_equal(m[[3]]$col, 1)
|
||||
|
||||
# Check mapping vars
|
||||
expect_equal(m$panels[[1]]$mapping, list(x = "xvar", y = "yvar", panelvar1 = "g"))
|
||||
expect_equal(m$panels[[1]]$mapping, m$panels[[2]]$mapping)
|
||||
expect_equal(m$panels[[2]]$mapping, m$panels[[3]]$mapping)
|
||||
expect_equal(m[[1]]$mapping, list(x = "xvar", y = "yvar", panelvar1 = "g"))
|
||||
expect_equal(m[[1]]$mapping, m[[2]]$mapping)
|
||||
expect_equal(m[[2]]$mapping, m[[3]]$mapping)
|
||||
# Check domain
|
||||
expect_equal(
|
||||
sortList(m$panels[[1]]$domain),
|
||||
sortList(m[[1]]$domain),
|
||||
sortList(list(left=0, right=10, bottom=10, top=30))
|
||||
)
|
||||
expect_equal(sortList(m$panels[[1]]$domain), sortList(m$panels[[2]]$domain))
|
||||
expect_equal(sortList(m$panels[[2]]$domain), sortList(m$panels[[3]]$domain))
|
||||
expect_equal(sortList(m[[1]]$domain), sortList(m[[2]]$domain))
|
||||
expect_equal(sortList(m[[2]]$domain), sortList(m[[3]]$domain))
|
||||
|
||||
# Check panel vars
|
||||
factor_vals <- dat$g
|
||||
expect_equal(m$panels[[1]]$panel_vars, list(panelvar1 = factor_vals[1]))
|
||||
expect_equal(m$panels[[2]]$panel_vars, list(panelvar1 = factor_vals[2]))
|
||||
expect_equal(m$panels[[3]]$panel_vars, list(panelvar1 = factor_vals[3]))
|
||||
expect_equal(m[[1]]$panel_vars, list(panelvar1 = factor_vals[1]))
|
||||
expect_equal(m[[2]]$panel_vars, list(panelvar1 = factor_vals[2]))
|
||||
expect_equal(m[[3]]$panel_vars, list(panelvar1 = factor_vals[3]))
|
||||
})
|
||||
|
||||
|
||||
@@ -132,75 +130,75 @@ test_that("ggplot coordmap with facet_grid", {
|
||||
# facet_grid horizontal
|
||||
p1 <- p + facet_grid(. ~ g)
|
||||
png(tmpfile)
|
||||
m <- getGgplotCoordmap(print(p1), 500, 400, 72)
|
||||
m <- getGgplotCoordmap(print(p1), 1, 72)
|
||||
dev.off()
|
||||
|
||||
# Should have 3 panels
|
||||
expect_equal(length(m$panels), 3)
|
||||
expect_equal(m$panels[[1]]$panel, 1)
|
||||
expect_equal(m$panels[[1]]$row, 1)
|
||||
expect_equal(m$panels[[1]]$col, 1)
|
||||
expect_equal(m$panels[[2]]$panel, 2)
|
||||
expect_equal(m$panels[[2]]$row, 1)
|
||||
expect_equal(m$panels[[2]]$col, 2)
|
||||
expect_equal(m$panels[[3]]$panel, 3)
|
||||
expect_equal(m$panels[[3]]$row, 1)
|
||||
expect_equal(m$panels[[3]]$col, 3)
|
||||
expect_equal(length(m), 3)
|
||||
expect_equal(m[[1]]$panel, 1)
|
||||
expect_equal(m[[1]]$row, 1)
|
||||
expect_equal(m[[1]]$col, 1)
|
||||
expect_equal(m[[2]]$panel, 2)
|
||||
expect_equal(m[[2]]$row, 1)
|
||||
expect_equal(m[[2]]$col, 2)
|
||||
expect_equal(m[[3]]$panel, 3)
|
||||
expect_equal(m[[3]]$row, 1)
|
||||
expect_equal(m[[3]]$col, 3)
|
||||
|
||||
# Check mapping vars
|
||||
expect_equal(m$panels[[1]]$mapping, list(x = "xvar", y = "yvar", panelvar1 = "g"))
|
||||
expect_equal(m$panels[[1]]$mapping, m$panels[[2]]$mapping)
|
||||
expect_equal(m$panels[[2]]$mapping, m$panels[[3]]$mapping)
|
||||
expect_equal(m[[1]]$mapping, list(x = "xvar", y = "yvar", panelvar1 = "g"))
|
||||
expect_equal(m[[1]]$mapping, m[[2]]$mapping)
|
||||
expect_equal(m[[2]]$mapping, m[[3]]$mapping)
|
||||
# Check domain
|
||||
expect_equal(
|
||||
sortList(m$panels[[1]]$domain),
|
||||
sortList(m[[1]]$domain),
|
||||
sortList(list(left=0, right=10, bottom=10, top=30))
|
||||
)
|
||||
expect_equal(sortList(m$panels[[1]]$domain), sortList(m$panels[[2]]$domain))
|
||||
expect_equal(sortList(m$panels[[2]]$domain), sortList(m$panels[[3]]$domain))
|
||||
expect_equal(sortList(m[[1]]$domain), sortList(m[[2]]$domain))
|
||||
expect_equal(sortList(m[[2]]$domain), sortList(m[[3]]$domain))
|
||||
|
||||
# Check panel vars
|
||||
factor_vals <- dat$g
|
||||
expect_equal(m$panels[[1]]$panel_vars, list(panelvar1 = factor_vals[1]))
|
||||
expect_equal(m$panels[[2]]$panel_vars, list(panelvar1 = factor_vals[2]))
|
||||
expect_equal(m$panels[[3]]$panel_vars, list(panelvar1 = factor_vals[3]))
|
||||
expect_equal(m[[1]]$panel_vars, list(panelvar1 = factor_vals[1]))
|
||||
expect_equal(m[[2]]$panel_vars, list(panelvar1 = factor_vals[2]))
|
||||
expect_equal(m[[3]]$panel_vars, list(panelvar1 = factor_vals[3]))
|
||||
|
||||
|
||||
# facet_grid vertical
|
||||
p1 <- p + facet_grid(g ~ .)
|
||||
png(tmpfile)
|
||||
m <- getGgplotCoordmap(print(p1), 500, 400, 72)
|
||||
m <- getGgplotCoordmap(print(p1), 1, 72)
|
||||
dev.off()
|
||||
|
||||
# Should have 3 panels
|
||||
expect_equal(length(m$panels), 3)
|
||||
expect_equal(m$panels[[1]]$panel, 1)
|
||||
expect_equal(m$panels[[1]]$row, 1)
|
||||
expect_equal(m$panels[[1]]$col, 1)
|
||||
expect_equal(m$panels[[2]]$panel, 2)
|
||||
expect_equal(m$panels[[2]]$row, 2)
|
||||
expect_equal(m$panels[[2]]$col, 1)
|
||||
expect_equal(m$panels[[3]]$panel, 3)
|
||||
expect_equal(m$panels[[3]]$row, 3)
|
||||
expect_equal(m$panels[[3]]$col, 1)
|
||||
expect_equal(length(m), 3)
|
||||
expect_equal(m[[1]]$panel, 1)
|
||||
expect_equal(m[[1]]$row, 1)
|
||||
expect_equal(m[[1]]$col, 1)
|
||||
expect_equal(m[[2]]$panel, 2)
|
||||
expect_equal(m[[2]]$row, 2)
|
||||
expect_equal(m[[2]]$col, 1)
|
||||
expect_equal(m[[3]]$panel, 3)
|
||||
expect_equal(m[[3]]$row, 3)
|
||||
expect_equal(m[[3]]$col, 1)
|
||||
|
||||
# Check mapping vars
|
||||
expect_equal(m$panels[[1]]$mapping, list(x = "xvar", y = "yvar", panelvar1 = "g"))
|
||||
expect_equal(m$panels[[1]]$mapping, m$panels[[2]]$mapping)
|
||||
expect_equal(m$panels[[2]]$mapping, m$panels[[3]]$mapping)
|
||||
expect_equal(m[[1]]$mapping, list(x = "xvar", y = "yvar", panelvar1 = "g"))
|
||||
expect_equal(m[[1]]$mapping, m[[2]]$mapping)
|
||||
expect_equal(m[[2]]$mapping, m[[3]]$mapping)
|
||||
# Check domain
|
||||
expect_equal(
|
||||
sortList(m$panels[[1]]$domain),
|
||||
sortList(m[[1]]$domain),
|
||||
sortList(list(left=0, right=10, bottom=10, top=30))
|
||||
)
|
||||
expect_equal(sortList(m$panels[[1]]$domain), sortList(m$panels[[2]]$domain))
|
||||
expect_equal(sortList(m$panels[[2]]$domain), sortList(m$panels[[3]]$domain))
|
||||
expect_equal(sortList(m[[1]]$domain), sortList(m[[2]]$domain))
|
||||
expect_equal(sortList(m[[2]]$domain), sortList(m[[3]]$domain))
|
||||
|
||||
# Check panel vars
|
||||
factor_vals <- dat$g
|
||||
expect_equal(m$panels[[1]]$panel_vars, list(panelvar1 = factor_vals[1]))
|
||||
expect_equal(m$panels[[2]]$panel_vars, list(panelvar1 = factor_vals[2]))
|
||||
expect_equal(m$panels[[3]]$panel_vars, list(panelvar1 = factor_vals[3]))
|
||||
expect_equal(m[[1]]$panel_vars, list(panelvar1 = factor_vals[1]))
|
||||
expect_equal(m[[2]]$panel_vars, list(panelvar1 = factor_vals[2]))
|
||||
expect_equal(m[[3]]$panel_vars, list(panelvar1 = factor_vals[3]))
|
||||
})
|
||||
|
||||
|
||||
@@ -217,43 +215,43 @@ test_that("ggplot coordmap with 2D facet_grid", {
|
||||
|
||||
p1 <- p + facet_grid(g ~ h)
|
||||
png(tmpfile)
|
||||
m <- getGgplotCoordmap(print(p1), 500, 400, 72)
|
||||
m <- getGgplotCoordmap(print(p1), 1, 72)
|
||||
dev.off()
|
||||
|
||||
# Should have 4 panels
|
||||
expect_equal(length(m$panels), 4)
|
||||
expect_equal(m$panels[[1]]$panel, 1)
|
||||
expect_equal(m$panels[[1]]$row, 1)
|
||||
expect_equal(m$panels[[1]]$col, 1)
|
||||
expect_equal(m$panels[[2]]$panel, 2)
|
||||
expect_equal(m$panels[[2]]$row, 1)
|
||||
expect_equal(m$panels[[2]]$col, 2)
|
||||
expect_equal(m$panels[[3]]$panel, 3)
|
||||
expect_equal(m$panels[[3]]$row, 2)
|
||||
expect_equal(m$panels[[3]]$col, 1)
|
||||
expect_equal(m$panels[[4]]$panel, 4)
|
||||
expect_equal(m$panels[[4]]$row, 2)
|
||||
expect_equal(m$panels[[4]]$col, 2)
|
||||
expect_equal(length(m), 4)
|
||||
expect_equal(m[[1]]$panel, 1)
|
||||
expect_equal(m[[1]]$row, 1)
|
||||
expect_equal(m[[1]]$col, 1)
|
||||
expect_equal(m[[2]]$panel, 2)
|
||||
expect_equal(m[[2]]$row, 1)
|
||||
expect_equal(m[[2]]$col, 2)
|
||||
expect_equal(m[[3]]$panel, 3)
|
||||
expect_equal(m[[3]]$row, 2)
|
||||
expect_equal(m[[3]]$col, 1)
|
||||
expect_equal(m[[4]]$panel, 4)
|
||||
expect_equal(m[[4]]$row, 2)
|
||||
expect_equal(m[[4]]$col, 2)
|
||||
|
||||
# Check mapping vars
|
||||
expect_equal(m$panels[[1]]$mapping, list(x = "xvar", y = "yvar", panelvar1 = "h", panelvar2 = "g"))
|
||||
expect_equal(m$panels[[1]]$mapping, m$panels[[2]]$mapping)
|
||||
expect_equal(m$panels[[2]]$mapping, m$panels[[3]]$mapping)
|
||||
expect_equal(m$panels[[4]]$mapping, m$panels[[4]]$mapping)
|
||||
expect_equal(m[[1]]$mapping, list(x = "xvar", y = "yvar", panelvar1 = "h", panelvar2 = "g"))
|
||||
expect_equal(m[[1]]$mapping, m[[2]]$mapping)
|
||||
expect_equal(m[[2]]$mapping, m[[3]]$mapping)
|
||||
expect_equal(m[[4]]$mapping, m[[4]]$mapping)
|
||||
# Check domain
|
||||
expect_equal(
|
||||
sortList(m$panels[[1]]$domain),
|
||||
sortList(m[[1]]$domain),
|
||||
sortList(list(left=0, right=15, bottom=10, top=40))
|
||||
)
|
||||
expect_equal(sortList(m$panels[[1]]$domain), sortList(m$panels[[2]]$domain))
|
||||
expect_equal(sortList(m$panels[[2]]$domain), sortList(m$panels[[3]]$domain))
|
||||
expect_equal(sortList(m$panels[[3]]$domain), sortList(m$panels[[4]]$domain))
|
||||
expect_equal(sortList(m[[1]]$domain), sortList(m[[2]]$domain))
|
||||
expect_equal(sortList(m[[2]]$domain), sortList(m[[3]]$domain))
|
||||
expect_equal(sortList(m[[3]]$domain), sortList(m[[4]]$domain))
|
||||
|
||||
# Check panel vars
|
||||
expect_equal(m$panels[[1]]$panel_vars, list(panelvar1 = dat$h[1], panelvar2 = dat$g[1]))
|
||||
expect_equal(m$panels[[2]]$panel_vars, list(panelvar1 = dat$h[2], panelvar2 = dat$g[1]))
|
||||
expect_equal(m$panels[[3]]$panel_vars, list(panelvar1 = dat$h[1], panelvar2 = dat$g[2]))
|
||||
expect_equal(m$panels[[4]]$panel_vars, list(panelvar1 = dat$h[2], panelvar2 = dat$g[2]))
|
||||
expect_equal(m[[1]]$panel_vars, list(panelvar1 = dat$h[1], panelvar2 = dat$g[1]))
|
||||
expect_equal(m[[2]]$panel_vars, list(panelvar1 = dat$h[2], panelvar2 = dat$g[1]))
|
||||
expect_equal(m[[3]]$panel_vars, list(panelvar1 = dat$h[1], panelvar2 = dat$g[2]))
|
||||
expect_equal(m[[4]]$panel_vars, list(panelvar1 = dat$h[2], panelvar2 = dat$g[2]))
|
||||
})
|
||||
|
||||
|
||||
@@ -267,12 +265,12 @@ test_that("ggplot coordmap with various data types", {
|
||||
scale_x_discrete(expand = c(0 ,0)) +
|
||||
scale_y_discrete(expand = c(0, 0))
|
||||
png(tmpfile)
|
||||
m <- getGgplotCoordmap(print(p), 500, 400, 72)
|
||||
m <- getGgplotCoordmap(print(p), 1, 72)
|
||||
dev.off()
|
||||
|
||||
# Check domain
|
||||
expect_equal(
|
||||
sortList(m$panels[[1]]$domain),
|
||||
sortList(m[[1]]$domain),
|
||||
sortList(list(left=1, right=3, bottom=1, top=4))
|
||||
)
|
||||
|
||||
@@ -285,12 +283,12 @@ test_that("ggplot coordmap with various data types", {
|
||||
scale_x_date(expand = c(0 ,0)) +
|
||||
scale_y_datetime(expand = c(0, 0))
|
||||
png(tmpfile)
|
||||
m <- getGgplotCoordmap(print(p), 500, 400, 72)
|
||||
m <- getGgplotCoordmap(print(p), 1, 72)
|
||||
dev.off()
|
||||
|
||||
# Check domain
|
||||
expect_equal(
|
||||
sortList(m$panels[[1]]$domain),
|
||||
sortList(m[[1]]$domain),
|
||||
sortList(list(
|
||||
left = as.numeric(dat$xvar[1]),
|
||||
right = as.numeric(dat$xvar[2]),
|
||||
@@ -310,12 +308,12 @@ test_that("ggplot coordmap with various scales and coords", {
|
||||
scale_x_continuous(expand = c(0 ,0)) +
|
||||
scale_y_reverse(expand = c(0, 0))
|
||||
png(tmpfile)
|
||||
m <- getGgplotCoordmap(print(p), 500, 400, 72)
|
||||
m <- getGgplotCoordmap(print(p), 1, 72)
|
||||
dev.off()
|
||||
|
||||
# Check domain (y reversed)
|
||||
expect_equal(
|
||||
sortList(m$panels[[1]]$domain),
|
||||
sortList(m[[1]]$domain),
|
||||
sortList(list(left=0, right=5, bottom=20, top=10))
|
||||
)
|
||||
|
||||
@@ -325,14 +323,14 @@ test_that("ggplot coordmap with various scales and coords", {
|
||||
scale_y_continuous(expand = c(0 ,0)) +
|
||||
coord_flip()
|
||||
png(tmpfile)
|
||||
m <- getGgplotCoordmap(print(p), 500, 400, 72)
|
||||
m <- getGgplotCoordmap(print(p), 1, 72)
|
||||
dev.off()
|
||||
|
||||
# Check mapping vars
|
||||
expect_equal(m$panels[[1]]$mapping, list(x = "yvar", y = "xvar"))
|
||||
expect_equal(m[[1]]$mapping, list(x = "yvar", y = "xvar"))
|
||||
# Check domain (y reversed)
|
||||
expect_equal(
|
||||
sortList(m$panels[[1]]$domain),
|
||||
sortList(m[[1]]$domain),
|
||||
sortList(list(left=10, right=20, bottom=0, top=5))
|
||||
)
|
||||
|
||||
@@ -343,17 +341,17 @@ test_that("ggplot coordmap with various scales and coords", {
|
||||
scale_y_continuous(expand = c(0, 0)) +
|
||||
coord_trans(y = "log2")
|
||||
png(tmpfile)
|
||||
m <- getGgplotCoordmap(print(p), 500, 400, 72)
|
||||
m <- getGgplotCoordmap(print(p), 1, 72)
|
||||
dev.off()
|
||||
|
||||
# Check log bases
|
||||
expect_equal(
|
||||
sortList(m$panels[[1]]$log),
|
||||
sortList(m[[1]]$log),
|
||||
sortList(list(x=10, y=2))
|
||||
)
|
||||
# Check domains
|
||||
expect_equal(
|
||||
sortList(m$panels[[1]]$domain),
|
||||
sortList(m[[1]]$domain),
|
||||
sortList(list(left=-1, right=3, bottom=-2, top=4))
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user