Compare commits

..

3 Commits

Author SHA1 Message Date
Joe Cheng
dcb0b0c762 Apply lobstr::cst pruning to deep stack traces too 2018-03-14 14:25:49 -07:00
Joe Cheng
a02c32c153 Hide internal renderPlot reactive from stack trace 2018-03-13 15:49:33 -07:00
Joe Cheng
bb85525793 Use lobstr::cst style tree analysis to further prune stack traces 2018-03-13 15:48:02 -07:00
102 changed files with 645 additions and 4493 deletions

View File

@@ -1,7 +1,7 @@
Package: shiny
Type: Package
Title: Web Application Framework for R
Version: 1.1.0.9000
Version: 1.0.5.9000
Authors@R: c(
person("Winston", "Chang", role = c("aut", "cre"), email = "winston@rstudio.com"),
person("Joe", "Cheng", role = "aut", email = "joe@rstudio.com"),
@@ -62,10 +62,14 @@ License: GPL-3 | file LICENSE
Depends:
R (>= 3.0.2),
methods
Remotes:
r-lib/later,
rstudio/promises,
rstudio/httpuv
Imports:
utils,
grDevices,
httpuv (>= 1.4.3.9001),
httpuv (>= 1.3.5),
mime (>= 0.3),
jsonlite (>= 0.9.16),
xtable,
@@ -73,11 +77,10 @@ Imports:
htmltools (>= 0.3.5),
R6 (>= 2.0),
sourcetools,
later (>= 0.7.2),
promises (>= 1.0.1),
later (>= 0.7.1),
promises (>= 0.1.0.9001),
tools,
crayon,
rlang
crayon
Suggests:
datasets,
Cairo (>= 1.5-5),
@@ -89,10 +92,7 @@ Suggests:
magrittr
URL: http://shiny.rstudio.com
BugReports: https://github.com/rstudio/shiny/issues
Remotes:
tidyverse/ggplot2,
rstudio/httpuv
Collate:
Collate:
'app.R'
'bookmark-state-local.R'
'stack.R'
@@ -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

View File

@@ -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)
@@ -276,13 +268,9 @@ export(updateSliderInput)
export(updateTabsetPanel)
export(updateTextAreaInput)
export(updateTextInput)
export(updateVarSelectInput)
export(updateVarSelectizeInput)
export(urlModal)
export(validate)
export(validateCssUnit)
export(varSelectInput)
export(varSelectizeInput)
export(verbatimTextOutput)
export(verticalLayout)
export(wellPanel)

85
NEWS.md
View File

@@ -1,100 +1,41 @@
shiny 1.1.0.9000
===========
## Full changelog
### Minor new features and improvements
* 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))
* 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))
* Resolved [#2019](https://github.com/rstudio/shiny/issues/2019): `updateSliderInput` now changes the slider formatting if the input type changes. ([#2099](https://github.com/rstudio/shiny/pull/2099))
* Added namespace support when freezing reactiveValue keys. [#2080](https://github.com/rstudio/shiny/pull/2080)
### 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))
shiny 1.1.0
===========
This is a significant release for Shiny, with a major new feature that was nearly a year in the making: support for asynchronous operations! Until now, R's single-threaded nature meant that performing long-running calculations or tasks from Shiny would bring your app to a halt for other users of that process. This release of Shiny deeply integrates the [promises](https://rstudio.github.io/promises/) package to allow you to execute some tasks asynchronously, including as part of reactive expressions and outputs. See the [promises](https://rstudio.github.io/promises/) documentation to learn more.
shiny 1.0.5.9000
================
## Full changelog
### Breaking changes
* `extractStackTrace` and `formatStackTrace` are deprecated and will be removed in a future version of Shiny. As far as we can tell, nobody has been using these functions, and a refactor has made them vestigial; if you need this functionality, please file an issue.
### New features
* Support for asynchronous operations! Built-in render functions that expected a certain kind of object to be yielded from their `expr`, now generally can handle a promise for that kind of object. Reactive expressions and observers are now promise-aware as well. ([#1932](https://github.com/rstudio/shiny/pull/1932))
* Introduced two changes to the (undocumented but widely used) JavaScript function `Shiny.onInputChange(name, value)`. First, we changed the function name to `Shiny.setInputValue` (but don't worry--the old function name will continue to work). Second, until now, all calls to `Shiny.onInputChange(inputId, value)` have been "deduplicated"; that is, anytime an input is set to the same value it already has, the set is ignored. With Shiny v1.1, you can now add an options object as the third parameter: `Shiny.setInputValue("name", value, {priority: "event"})`. When the priority option is set to `"event"`, Shiny will always send the value and trigger reactivity, whether it is a duplicate or not. This closes [#928](https://github.com/rstudio/shiny/issues/928), which was the most upvoted open issue by far! Thanks, @daattali. ([#2018](https://github.com/rstudio/shiny/pull/2018))
### Minor new features and improvements
* Addressed [#1978](https://github.com/rstudio/shiny/issues/1978): `shiny:value` is now triggered when duplicate output data is received from the server. (Thanks, @andrewsali! [#1999](https://github.com/rstudio/shiny/pull/1999))
* If a shiny output contains a css class of `shiny-report-size`, its container height and width are now reported in `session$clientData`. So, for an output with an id with `"myID"`, the height/width can be accessed via `session$clientData[['output_myID_height']]`/`session$clientData[['output_myID_width']]`. Addresses [#1980](https://github.com/rstudio/shiny/issues/1980). (Thanks, @cpsievert! [#1981](https://github.com/rstudio/shiny/pull/1981))
* Added a new `autoclose = TRUE` parameter to `dateInput()` and `dateRangeInput()`. This closed [#1969](https://github.com/rstudio/shiny/issues/1969) which was a duplicate of much older issue, [#173](https://github.com/rstudio/shiny/issues/173). The default value is `TRUE` since that seems to be the common use case. However, this will cause existing apps with date inputs (that update to this version of Shiny) to have the datepicker be immediately closed once a date is selected. For most apps, this is actually desired behavior; if you wish to keep the datepicker open until the user clicks out of it use `autoclose = FALSE`. ([#1987](https://github.com/rstudio/shiny/pull/1987))
* Changed script tags in reactlog ([inst/www/reactive-graph.html](https://github.com/rstudio/shiny/blob/master/inst/www/reactive-graph.html)) from HTTP to HTTPS in order to avoid mixed content blocking by most browsers. (Thanks, [@jekriske-lilly](https://github.com/jekriske-lilly)! [#1844](https://github.com/rstudio/shiny/pull/1844))
* The version of Shiny is now accessible from Javascript, with `Shiny.version`. There is also a new function for comparing version strings, `Shiny.compareVersion()`. ([#1826](https://github.com/rstudio/shiny/pull/1826), [#1830](https://github.com/rstudio/shiny/pull/1830))
* Addressed [#1784](https://github.com/rstudio/shiny/issues/1784): `runApp()` will avoid port 6697, which is considered unsafe by Chrome.
* Addressed [#1851](https://github.com/rstudio/shiny/issues/1851): Stack traces are now smaller in some places `do.call()` is used. ([#1856](https://github.com/rstudio/shiny/pull/1856))
* Stack traces have been improved, with more aggressive de-noising and support for deep stack traces (stitching together multiple stack traces that are conceptually part of the same async operation).
* Addressed [#1859](https://github.com/rstudio/shiny/issues/1859): Server-side selectize is now significantly faster. (Thanks to @dselivanov [#1861](https://github.com/rstudio/shiny/pull/1861))
* [#1989](https://github.com/rstudio/shiny/issues/1989): The server side of outputs can now be removed (e.g. `output$plot <- NULL`). This is not usually necessary but it does allow some objects to be garbage collected, which might matter if you are dynamically creating and destroying many outputs. (Thanks, @mmuurr! [#2011](https://github.com/rstudio/shiny/pull/2011))
* Removed the (ridiculously outdated) "experimental feature" tag from the reference documentation for `renderUI`. ([#2036](https://github.com/rstudio/shiny/pull/2036))
* Addressed [#1907](https://github.com/rstudio/shiny/issues/1907): the `ignoreInit` argument was first added only to `observeEvent`. Later, we also added it to `eventReactive`, but forgot to update the documentation. Now done, thanks [@flo12392](https://github.com/flo12392)! ([#2036](https://github.com/rstudio/shiny/pull/2036))
### Bug fixes
* Fixed [#1006](https://github.com/rstudio/shiny/issues/1006): Slider inputs sometimes showed too many digits. ([#1956](https://github.com/rstudio/shiny/pull/1956))
* Fixed [#1958](https://github.com/rstudio/shiny/issues/1958): Slider inputs previously displayed commas after a decimal point. ([#1960](https://github.com/rstudio/shiny/pull/1960))
### Bug fixes
* The internal `URLdecode()` function previously was a copy of `httpuv::decodeURIComponent()`, assigned at build time; now it invokes the httpuv function at run time.
* Fixed [#1840](https://github.com/rstudio/shiny/issues/1840): with the release of Shiny 1.0.5, we accidently changed the relative positioning of the icon and the title text in `navbarMenu`s and `tabPanel`s. This fix reverts this behavior back (i.e. the icon should be to the left of the text and/or the downward arrow in case of `navbarMenu`s). ([#1848](https://github.com/rstudio/shiny/pull/1848))
* Fixed [#1600](https://github.com/rstudio/shiny/issues/1600): URL-encoded bookmarking did not work with sliders that had dates or date-times. ([#1961](https://github.com/rstudio/shiny/pull/1961))
* Fixed [#1962](https://github.com/rstudio/shiny/issues/1962): [File dragging and dropping](https://blog.rstudio.com/2017/08/15/shiny-1-0-4/) broke in the presence of jQuery version 3.0 as introduced by the [rhandsontable](https://jrowen.github.io/rhandsontable/) [htmlwidget](https://www.htmlwidgets.org/). ([#2005](https://github.com/rstudio/shiny/pull/2005))
* Improved the error handling inside the `addResourcePath()` function, to give end users more informative error messages when the `directoryPath` argument cannot be normalized. This is especially useful for `runtime: shiny_prerendered` Rmd documents, like `learnr` tutorials. ([#1968](https://github.com/rstudio/shiny/pull/1968))
* Changed script tags in reactlog ([inst/www/reactive-graph.html](https://github.com/rstudio/shiny/blob/master/inst/www/reactive-graph.html)) from HTTP to HTTPS in order to avoid mixed content blocking by most browsers. (Thanks, @jekriske-lilly! [#1844](https://github.com/rstudio/shiny/pull/1844))
* Addressed [#1784](https://github.com/rstudio/shiny/issues/1784): `runApp()` will avoid port 6697, which is considered unsafe by Chrome.
* Fixed [#2000](https://github.com/rstudio/shiny/issues/2000): Implicit calls to `xxxOutput` not working inside modules. (Thanks, @GregorDeCillia! [#2010](https://github.com/rstudio/shiny/pull/2010))
* Fixed [#2021](https://github.com/rstudio/shiny/issues/2021): Memory leak with `reactiveTimer` and `invalidateLater`. ([#2022](https://github.com/rstudio/shiny/pull/2022))
### Library updates
* Updated to ion.rangeSlider 2.2.0. ([#1955](https://github.com/rstudio/shiny/pull/1955))
## Known issues
In some rare cases, interrupting an application (by pressing Ctrl-C or Esc) may result in the message `Error in execCallbacks(timeoutSecs) : c++ exception (unknown reason)`. Although this message sounds alarming, it is harmless, and will go away in a future version of the later package (more information [here](https://github.com/r-lib/later/issues/55)).
shiny 1.0.5
===========
@@ -306,7 +247,7 @@ Now there's an official way to slow down reactive values and expressions that in
### Minor new features and improvements
* Addressed [#1486](https://github.com/rstudio/shiny/issues/1486) by adding a new argument to `observeEvent` and `eventReactive`, called `ignoreInit` (defaults to `FALSE` for backwards compatibility). When set to `TRUE`, the action (i.e. the second argument: `handlerExpr` and `valueExpr`, respectively) will not be triggered when the observer/reactive is first created/initialized. In other words, `ignoreInit = TRUE` ensures that the `observeEvent` (or `eventReactive`) is *never* run right away. For more info, see the documentation (`?observeEvent`). ([#1494](https://github.com/rstudio/shiny/pull/1494))
* Added a new argument to `observeEvent` called `once`. When set to `TRUE`, it results in the observer being destroyed (stop observing) after the first time that `handlerExpr` is run (i.e. `once = TRUE` guarantees that the observer only runs, at most, once). For more info, see the documentation (`?observeEvent`). ([#1494](https://github.com/rstudio/shiny/pull/1494))
* Addressed [#1358](https://github.com/rstudio/shiny/issues/1358): more informative error message when calling `runApp()` inside of an app's app.R (or inside ui.R or server.R). ([#1482](https://github.com/rstudio/shiny/pull/1482))
@@ -705,7 +646,7 @@ shiny 0.12.1
shiny 0.12.0
============
In addition to the changes listed below (in the *Full Changelog* section), there is an infrastructure change that could affect existing Shiny apps.
In addition to the changes listed below (in the *Full Changelog* section), there is an infrastructure change that could affect existing Shiny apps.
### JSON serialization
@@ -796,13 +737,13 @@ Shiny 0.11 switches away from the Bootstrap 2 web framework to the next version,
### Known issues for migration
* In Bootstrap 3, images in `<img>` tags are no longer automatically scaled to the width of their container. If you use `img()` in your UI code, or `<img>` tags in your raw HTML source, it's possible that they will be too large in the new version of Shiny. To address this you can add the `img-responsive` class:
```r
img(src = "picture.png", class = "img-responsive")
```
The R code above will generate the following HTML:
```html
<img src="picture.png" class="img-responsive">
```

View File

@@ -588,7 +588,7 @@ flexfill <- function(..., direction, flex, width = width, height = height) {
}
if (length(flex) > length(children)) {
flex <- flex[seq_along(children)]
flex <- flex[1:length(children)]
}
# The dimension along the main axis

View File

@@ -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)
}
)
)

View File

@@ -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)
}
)
)

View File

@@ -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.")
}
}

View File

@@ -131,17 +131,15 @@ captureStackTraces <- function(expr) {
.globals$deepStack <- NULL
createStackTracePromiseDomain <- function() {
# These are actually stateless, we wouldn't have to create a new one each time
# if we didn't want to. They're pretty cheap though.
d <- promises::new_promise_domain(
wrapOnFulfilled = function(onFulfilled) {
force(onFulfilled)
# Subscription time
if (deepStacksEnabled()) {
currentStack <- sys.calls()
currentParents <- sys.parents()
attr(currentStack, "parents") <- currentParents
calls <- sys.calls()
parents <- sys.parents()
attr(calls, "parents") <- parents
currentStack <- formatStackTrace(calls)
currentDeepStack <- .globals$deepStack
}
function(...) {
@@ -162,9 +160,10 @@ createStackTracePromiseDomain <- function() {
force(onRejected)
# Subscription time
if (deepStacksEnabled()) {
currentStack <- sys.calls()
currentParents <- sys.parents()
attr(currentStack, "parents") <- currentParents
calls <- sys.calls()
parents <- sys.parents()
attr(calls, "parents") <- parents
currentStack <- formatStackTrace(calls)
currentDeepStack <- .globals$deepStack
}
function(...) {
@@ -191,7 +190,7 @@ createStackTracePromiseDomain <- function() {
}
deepStacksEnabled <- function() {
getOption("shiny.deepstacktrace", TRUE)
getOption("shiny.deepstacktrace", FALSE)
}
doCaptureStack <- function(e) {
@@ -266,11 +265,20 @@ withLogErrors <- function(expr,
printError <- function(cond,
full = getOption("shiny.fullstacktrace", FALSE),
offset = getOption("shiny.stacktraceoffset", TRUE)) {
warning(call. = FALSE, immediate. = TRUE, sprintf("Error in %s: %s",
warning(call. = FALSE, immediate. = TRUE, sprintf("Error in %s: %s",
getCallNames(list(conditionCall(cond))), conditionMessage(cond)))
printStackTrace(cond, full = full, offset = offset)
lapply(rev(attr(cond, "deep.stack.trace", exact = TRUE)), function(st) {
message(
paste0(
"From earlier call:\n",
paste0(st, collapse = "\n"),
"\n"
)
)
})
invisible()
}
#' @rdname stacktrace
@@ -279,104 +287,71 @@ printStackTrace <- function(cond,
full = getOption("shiny.fullstacktrace", FALSE),
offset = getOption("shiny.stacktraceoffset", TRUE)) {
should_drop <- !full
should_strip <- !full
should_prune <- !full
stackTraceCalls <- c(
attr(cond, "deep.stack.trace", exact = TRUE),
list(attr(cond, "stack.trace", exact = TRUE))
)
stackTraceParents <- lapply(stackTraceCalls, attr, which = "parents", exact = TRUE)
stackTraceCallNames <- lapply(stackTraceCalls, getCallNames)
stackTraceCalls <- lapply(stackTraceCalls, offsetSrcrefs, offset = offset)
# Use dropTrivialFrames logic to remove trailing bits (.handleSimpleError, h)
if (should_drop) {
# toKeep is a list of logical vectors, of which elements (stack frames) to keep
toKeep <- lapply(stackTraceCallNames, dropTrivialFrames)
# We apply the list of logical vector indices to each data structure
stackTraceCalls <- mapply(stackTraceCalls, FUN = `[`, toKeep, SIMPLIFY = FALSE)
stackTraceCallNames <- mapply(stackTraceCallNames, FUN = `[`, toKeep, SIMPLIFY = FALSE)
stackTraceParents <- mapply(stackTraceParents, FUN = `[`, toKeep, SIMPLIFY = FALSE)
}
delayedAssign("all_true", {
# List of logical vectors that are all TRUE, the same shape as
# stackTraceCallNames. Delay the evaluation so we don't create it unless
# we need it, but if we need it twice then we don't pay to create it twice.
lapply(stackTraceCallNames, function(st) {
rep_len(TRUE, length(st))
})
})
# stripStackTraces and lapply(stackTraceParents, pruneStackTrace) return lists
# of logical vectors. Use mapply(FUN = `&`) to boolean-and each pair of the
# logical vectors.
toShow <- mapply(
if (should_strip) stripStackTraces(stackTraceCallNames) else all_true,
if (should_prune) lapply(stackTraceParents, pruneStackTrace) else all_true,
FUN = `&`,
SIMPLIFY = FALSE
)
dfs <- mapply(seq_along(stackTraceCalls), rev(stackTraceCalls), rev(stackTraceCallNames), rev(toShow), FUN = function(i, calls, nms, index) {
st <- data.frame(
num = rev(which(index)),
call = rev(nms[index]),
loc = rev(getLocs(calls[index])),
category = rev(getCallCategories(calls[index])),
stringsAsFactors = FALSE
)
if (i != 1) {
message("From earlier call:")
}
if (nrow(st) == 0) {
message(" [No stack trace available]")
} else {
width <- floor(log10(max(st$num))) + 1
formatted <- paste0(
" ",
formatC(st$num, width = width),
": ",
mapply(paste0(st$call, st$loc), st$category, FUN = function(name, category) {
if (category == "pkg")
crayon::silver(name)
else if (category == "user")
crayon::blue$bold(name)
else
crayon::white(name)
}),
stackTrace <- attr(cond, "stack.trace", exact = TRUE)
tryCatch(
if (!is.null(stackTrace)) {
message(paste0(
"Stack trace (innermost first):\n",
paste0(collapse = "\n",
formatStackTrace(stackTrace, full = full, offset = offset,
indent = " ")
),
"\n"
)
cat(file = stderr(), formatted, sep = "")
}
))
} else {
message("No stack trace available")
},
st
}, SIMPLIFY = FALSE)
error = function(cond) {
warning("Failed to write stack trace: ", cond)
}
)
invisible()
}
# Given sys.parents() (which corresponds to sys.calls()), return a logical index
# that prunes each subtree so that only the final branch remains. The result,
# when applied to sys.calls(), is a linear list of calls without any "wrapper"
# functions like tryCatch, try, with, hybrid_chain, etc. While these are often
# part of the active call stack, they rarely are helpful when trying to identify
# a broken bit of code.
prune <- function(parents) {
# Detect nodes that are not the last child. This is necessary, but not
# sufficient; we also need to drop nodes that are the last child, but one of
# their ancestors is not.
is_dupe <- duplicated(parents, fromLast = TRUE)
# The index of the most recently seen node that was actually kept instead of
# dropped.
current_node <- 0
# Loop over the parent indices. Anything that is not parented by current_node
# (a.k.a. last-known-good node), or is a dupe, can be discarded. Anything that
# is kept becomes the new current_node.
include <- vapply(seq_along(parents), function(i) {
if (!is_dupe[[i]] && parents[[i]] == current_node) {
current_node <<- i
TRUE
} else {
FALSE
}
}, FUN.VALUE = logical(1))
include
}
#' @details \code{extractStackTrace} takes a list of calls (e.g. as returned
#' from \code{conditionStackTrace(cond)}) and returns a data frame with one
#' row for each stack frame and the columns \code{num} (stack frame number),
#' \code{call} (a function name or similar), and \code{loc} (source file path
#' and line number, if available). It was deprecated after shiny 1.0.5 because
#' it doesn't support deep stack traces.
#' and line number, if available).
#' @rdname stacktrace
#' @export
extractStackTrace <- function(calls,
full = getOption("shiny.fullstacktrace", FALSE),
offset = getOption("shiny.stacktraceoffset", TRUE)) {
shinyDeprecated(NULL,
"extractStackTrace is deprecated. Please contact the Shiny team if you were using this functionality.",
version = "1.0.5")
parents <- attr(calls, "parents", exact = TRUE)
srcrefs <- getSrcRefs(calls)
if (offset) {
# Offset calls vs. srcrefs by 1 to make them more intuitive.
@@ -405,6 +380,7 @@ extractStackTrace <- function(calls,
if (toRemove > 0 && toRemove < 5) {
calls <- utils::head(calls, -toRemove)
callnames <- utils::head(callnames, -toRemove)
parents <- utils::head(parents, -toRemove)
}
# This uses a ref-counting scheme. It might make sense to switch this
@@ -415,11 +391,9 @@ extractStackTrace <- function(calls,
score <- rep.int(0, length(callnames))
score[callnames == "..stacktraceoff.."] <- -1
score[callnames == "..stacktraceon.."] <- 1
toShow <- (1 + cumsum(score)) > 0 & !(callnames %in% c("..stacktraceon..", "..stacktraceoff..", "..stacktracefloor.."))
toShow <- (1 + cumsum(score)) > 0 & !(callnames %in% c("..stacktraceon..", "..stacktraceoff.."))
# doTryCatch, tryCatchOne, and tryCatchList are not informative--they're
# just internals for tryCatch
toShow <- toShow & !(callnames %in% c("doTryCatch", "tryCatchOne", "tryCatchList"))
toShow <- toShow & prune(parents)
}
calls <- calls[toShow]
@@ -436,110 +410,8 @@ extractStackTrace <- function(calls,
)
}
stripStackTraces <- function(stackTraces, values = FALSE) {
score <- 1L # >=1: show, <=0: hide
lapply(seq_along(stackTraces), function(i) {
res <- stripOneStackTrace(stackTraces[[i]], i != 1, score)
score <<- res$score
toShow <- as.logical(res$trace)
if (values) {
as.character(stackTraces[[i]][toShow])
} else {
as.logical(toShow)
}
})
}
stripOneStackTrace <- function(stackTrace, truncateFloor, startingScore) {
prefix <- logical(0)
if (truncateFloor) {
indexOfFloor <- utils::tail(which(stackTrace == "..stacktracefloor.."), 1)
if (length(indexOfFloor)) {
stackTrace <- stackTrace[(indexOfFloor+1L):length(stackTrace)]
prefix <- rep_len(FALSE, indexOfFloor)
}
}
if (length(stackTrace) == 0) {
return(list(score = startingScore, character(0)))
}
score <- rep.int(0L, length(stackTrace))
score[stackTrace == "..stacktraceon.."] <- 1L
score[stackTrace == "..stacktraceoff.."] <- -1L
score <- startingScore + cumsum(score)
toShow <- score > 0 & !(stackTrace %in% c("..stacktraceon..", "..stacktraceoff..", "..stacktracefloor.."))
list(score = utils::tail(score, 1), trace = c(prefix, toShow))
}
# Given sys.parents() (which corresponds to sys.calls()), return a logical index
# that prunes each subtree so that only the final branch remains. The result,
# when applied to sys.calls(), is a linear list of calls without any "wrapper"
# functions like tryCatch, try, with, hybrid_chain, etc. While these are often
# part of the active call stack, they rarely are helpful when trying to identify
# a broken bit of code.
pruneStackTrace <- function(parents) {
# Detect nodes that are not the last child. This is necessary, but not
# sufficient; we also need to drop nodes that are the last child, but one of
# their ancestors is not.
is_dupe <- duplicated(parents, fromLast = TRUE)
# The index of the most recently seen node that was actually kept instead of
# dropped.
current_node <- 0
# Loop over the parent indices. Anything that is not parented by current_node
# (a.k.a. last-known-good node), or is a dupe, can be discarded. Anything that
# is kept becomes the new current_node.
include <- vapply(seq_along(parents), function(i) {
if (!is_dupe[[i]] && parents[[i]] == current_node) {
current_node <<- i
TRUE
} else {
FALSE
}
}, FUN.VALUE = logical(1))
include
}
dropTrivialFrames <- function(callnames) {
# Remove stop(), .handleSimpleError(), and h() calls from the end of
# the calls--they don't add any helpful information. But only remove
# the last *contiguous* block of them, and then, only if they are the
# last thing in the calls list.
hideable <- callnames %in% c(".handleSimpleError", "h", "base$wrapOnFulfilled")
# What's the last that *didn't* match stop/.handleSimpleError/h?
lastGoodCall <- max(which(!hideable))
toRemove <- length(callnames) - lastGoodCall
c(
rep_len(TRUE, length(callnames) - toRemove),
rep_len(FALSE, toRemove)
)
}
offsetSrcrefs <- function(calls, offset = TRUE) {
if (offset) {
srcrefs <- getSrcRefs(calls)
# Offset calls vs. srcrefs by 1 to make them more intuitive.
# E.g. for "foo [bar.R:10]", line 10 of bar.R will be part of
# the definition of foo().
srcrefs <- c(utils::tail(srcrefs, -1), list(NULL))
calls <- setSrcRefs(calls, srcrefs)
}
calls
}
#' @details \code{formatStackTrace} is similar to \code{extractStackTrace}, but
#' it returns a preformatted character vector instead of a data frame. It was
#' deprecated after shiny 1.0.5 because it doesn't support deep stack traces.
#' it returns a preformatted character vector instead of a data frame.
#' @param indent A string to prefix every line of the stack trace.
#' @rdname stacktrace
#' @export
@@ -547,10 +419,6 @@ formatStackTrace <- function(calls, indent = " ",
full = getOption("shiny.fullstacktrace", FALSE),
offset = getOption("shiny.stacktraceoffset", TRUE)) {
shinyDeprecated(NULL,
"extractStackTrace is deprecated. Please contact the Shiny team if you were using this functionality.",
version = "1.0.5")
st <- extractStackTrace(calls, full = full, offset = offset)
if (nrow(st) == 0) {
return(character(0))
@@ -623,5 +491,3 @@ conditionStackTrace <- function(cond) {
#' @rdname stacktrace
#' @export
..stacktraceoff.. <- function(expr) expr
..stacktracefloor.. <- function(expr) expr

View File

@@ -41,8 +41,6 @@
#' "nb", "nl-BE", "nl", "no", "pl", "pt-BR", "pt", "ro", "rs-latin", "rs",
#' "ru", "sk", "sl", "sq", "sr-latin", "sr", "sv", "sw", "th", "tr", "uk",
#' "vi", "zh-CN", and "zh-TW".
#' @param autoclose Whether or not to close the datepicker immediately when a
#' date is selected.
#'
#' @family input elements
#' @seealso \code{\link{dateRangeInput}}, \code{\link{updateDateInput}}
@@ -78,7 +76,7 @@
#' @export
dateInput <- function(inputId, label, value = NULL, min = NULL, max = NULL,
format = "yyyy-mm-dd", startview = "month", weekstart = 0, language = "en",
width = NULL, autoclose = TRUE) {
width = NULL) {
# If value is a date object, convert it to a string with yyyy-mm-dd format
# Same for min and max
@@ -101,8 +99,7 @@ dateInput <- function(inputId, label, value = NULL, min = NULL, max = NULL,
`data-date-start-view` = startview,
`data-min-date` = min,
`data-max-date` = max,
`data-initial-date` = value,
`data-date-autoclose` = if (autoclose) "true" else "false"
`data-initial-date` = value
),
datePickerDependency
)

View File

@@ -73,8 +73,7 @@
#' @export
dateRangeInput <- function(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) {
weekstart = 0, language = "en", separator = " to ", width = NULL) {
# If start and end are date objects, convert to a string with yyyy-mm-dd format
# Same for min and max
@@ -104,8 +103,7 @@ dateRangeInput <- function(inputId, label, start = NULL, end = NULL,
`data-date-start-view` = startview,
`data-min-date` = min,
`data-max-date` = max,
`data-initial-date` = start,
`data-date-autoclose` = if (autoclose) "true" else "false"
`data-initial-date` = start
),
span(class = "input-group-addon", separator),
tags$input(
@@ -117,8 +115,7 @@ dateRangeInput <- function(inputId, label, start = NULL, end = NULL,
`data-date-start-view` = startview,
`data-min-date` = min,
`data-max-date` = max,
`data-initial-date` = end,
`data-date-autoclose` = if (autoclose) "true" else "false"
`data-initial-date` = end
)
)
),

View File

@@ -33,7 +33,7 @@
#' @return A select list control that can be added to a UI definition.
#'
#' @family input elements
#' @seealso \code{\link{updateSelectInput}} \code{\link{varSelectInput}}
#' @seealso \code{\link{updateSelectInput}}
#'
#' @examples
#' ## Only run examples in interactive R sessions
@@ -59,9 +59,9 @@
#' shinyApp(
#' ui = fluidPage(
#' selectInput("state", "Choose a state:",
#' list(`East Coast` = list("NY", "NJ", "CT"),
#' `West Coast` = list("WA", "OR", "CA"),
#' `Midwest` = list("MN", "WI", "IA"))
#' list(`East Coast` = c("NY", "NJ", "CT"),
#' `West Coast` = c("WA", "OR", "CA"),
#' `Midwest` = c("MN", "WI", "IA"))
#' ),
#' textOutput("result")
#' ),
@@ -212,135 +212,3 @@ selectizeIt <- function(inputId, select, options, nonempty = FALSE) {
attachDependencies(select, selectizeDep)
}
#' Select variables from a data frame
#'
#' Create a select list that can be used to choose a single or multiple items
#' from the column names of a data frame.
#'
#' The resulting server \code{input} value will be returned as:
#' \itemize{
#' \item a symbol if \code{multiple = FALSE}. The \code{input} value should be
#' used with rlang's \code{\link[rlang]{!!}}. For example,
#' \code{ggplot2::aes(!!input$variable)}.
#' \item a list of symbols if \code{multiple = TRUE}. The \code{input} value
#' should be used with rlang's \code{\link[rlang]{!!!}} to expand
#' the symbol list as individual arguments. For example,
#' \code{dplyr::select(mtcars, !!!input$variabls)} which is
#' equivalent to \code{dplyr::select(mtcars, !!input$variabls[[1]], !!input$variabls[[2]], ..., !!input$variabls[[length(input$variabls)]])}.
#' }
#'
#' By default, \code{varSelectInput()} and \code{selectizeInput()} use the
#' JavaScript library \pkg{selectize.js}
#' (\url{https://github.com/selectize/selectize.js}) to instead of the basic
#' select input element. To use the standard HTML select input element, use
#' \code{selectInput()} with \code{selectize=FALSE}.
#'
#' @inheritParams selectInput
#' @param data A data frame. Used to retrieve the column names as choices for a \code{\link{selectInput}}
#' @return A variable select list control that can be added to a UI definition.
#'
#' @family input elements
#' @seealso \code{\link{updateSelectInput}}
#' @examples
#'
#' ## Only run examples in interactive R sessions
#' if (interactive()) {
#'
#' library(ggplot2)
#'
#' # single selection
#' shinyApp(
#' ui = fluidPage(
#' varSelectInput("variable", "Variable:", mtcars),
#' plotOutput("data")
#' ),
#' server = function(input, output) {
#' output$data <- renderPlot({
#' ggplot(mtcars, aes(!!input$variable)) + geom_histogram()
#' })
#' }
#' )
#'
#'
#' # multiple selections
#' \dontrun{
#' shinyApp(
#' ui = fluidPage(
#' varSelectInput("variables", "Variable:", mtcars, multiple = TRUE),
#' tableOutput("data")
#' ),
#' server = function(input, output) {
#' output$data <- renderTable({
#' if (length(input$variables) == 0) return(mtcars)
#' mtcars %>% dplyr::select(!!!input$variables)
#' }, rownames = TRUE)
#' }
#' )}
#'
#' }
#' @export
varSelectInput <- function(
inputId, label, data, selected = NULL,
multiple = FALSE, selectize = TRUE, width = NULL,
size = NULL
) {
# no place holders
choices <- colnames(data)
selectInputVal <- selectInput(
inputId = inputId,
label = label,
choices = choices,
selected = selected,
multiple = multiple,
selectize = selectize,
width = width,
size = size
)
# set the select tag class to be "symbol"
selectClass <- selectInputVal$children[[2]]$children[[1]]$attribs$class
if (is.null(selectClass)) {
newClass <- "symbol"
} else {
newClass <- paste(selectClass, "symbol", sep = " ")
}
selectInputVal$children[[2]]$children[[1]]$attribs$class <- newClass
selectInputVal
}
#' @rdname varSelectInput
#' @param ... Arguments passed to \code{varSelectInput()}.
#' @param options A list of options. See the documentation of \pkg{selectize.js}
#' for possible options (character option values inside \code{\link[base]{I}()} will
#' be treated as literal JavaScript code; see \code{\link{renderDataTable}()}
#' for details).
#' @param width The width of the input, e.g. \code{'400px'}, or \code{'100\%'};
#' see \code{\link{validateCssUnit}}.
#' @note The variable selectize input created from \code{varSelectizeInput()} allows
#' deletion of the selected option even in a single select input, which will
#' return an empty string as its value. This is the default behavior of
#' \pkg{selectize.js}. However, the selectize input created from
#' \code{selectInput(..., selectize = TRUE)} will ignore the empty string
#' value when it is a single choice input and the empty string is not in the
#' \code{choices} argument. This is to keep compatibility with
#' \code{selectInput(..., selectize = FALSE)}.
#' @export
varSelectizeInput <- function(inputId, ..., options = NULL, width = NULL) {
selectizeIt(
inputId,
varSelectInput(inputId, ..., selectize = FALSE, width = width),
options
)
}

View File

@@ -86,10 +86,24 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
version = "0.10.2.2")
}
dataType <- getSliderType(min, max, value)
if (inherits(min, "Date")) {
if (!inherits(max, "Date") || !inherits(value, "Date"))
stop("`min`, `max`, and `value must all be Date or non-Date objects")
dataType <- "date"
if (is.null(timeFormat)) {
timeFormat <- switch(dataType, date = "%F", datetime = "%F %T", number = NULL)
if (is.null(timeFormat))
timeFormat <- "%F"
} else if (inherits(min, "POSIXt")) {
if (!inherits(max, "POSIXt") || !inherits(value, "POSIXt"))
stop("`min`, `max`, and `value must all be POSIXt or non-POSIXt objects")
dataType <- "datetime"
if (is.null(timeFormat))
timeFormat <- "%F %T"
} else {
dataType <- "number"
}
# Restore bookmarked values here, after doing the type checking, because the
@@ -236,13 +250,7 @@ findStepSize <- function(min, max, step) {
# values to calculate the step size.
pretty_steps <- pretty(c(min, max), n = 100)
n_steps <- length(pretty_steps) - 1
# Fix for #2061: Windows has low-significance digits (like 17 digits out)
# even at the boundaries of pretty()'s output. Use signif(digits = 10),
# which should be way way less significant than any data we'd want to keep.
# It might make sense to use signif(steps[2] - steps[1], 10) instead, but
# for now trying to make the minimal change.
signif(digits = 10, (max(pretty_steps) - min(pretty_steps)) / n_steps)
(max(pretty_steps) - min(pretty_steps)) / n_steps
} else {
1

View File

@@ -95,7 +95,11 @@ getDefaultReactiveDomain <- function() {
#' @rdname domains
#' @export
withReactiveDomain <- function(domain, expr) {
promises::with_promise_domain(createVarPromiseDomain(.globals, "domain", domain), expr)
oldValue <- .globals$domain
.globals$domain <- domain
on.exit(.globals$domain <- oldValue)
expr
}
#

View File

@@ -278,9 +278,8 @@ ReactiveValues <- R6Class(
.allValuesDeps = 'Dependents',
# Dependents for all values
.valuesDeps = 'Dependents',
.dedupe = logical(0),
initialize = function(dedupe = TRUE) {
initialize = function() {
.label <<- paste('reactiveValues',
p_randomInt(1000, 10000),
sep="")
@@ -290,7 +289,6 @@ ReactiveValues <- R6Class(
.namesDeps <<- Dependents$new()
.allValuesDeps <<- Dependents$new()
.valuesDeps <<- Dependents$new()
.dedupe <<- dedupe
},
get = function(key) {
@@ -319,7 +317,7 @@ ReactiveValues <- R6Class(
hidden <- substr(key, 1, 1) == "."
if (exists(key, envir=.values, inherits=FALSE)) {
if (.dedupe && identical(.values[[key]], value)) {
if (identical(.values[[key]], value)) {
return(invisible())
}
}
@@ -783,6 +781,18 @@ Observable <- R6Class(
# If an error occurs, we want to propagate the error, but we also
# want to save a copy of it, so future callers of this reactive will
# get the same error (i.e. the error is cached).
# We stripStackTrace in the next line, just in case someone
# downstream of us (i.e. deeper into the call stack) used
# captureStackTraces; otherwise the entire stack would always be the
# same (i.e. you'd always see the whole stack trace of the *first*
# time the code was run and the condition raised; there'd be no way
# to see the stack trace of the call site that caused the cached
# exception to be re-raised, and you need that information to figure
# out what's triggering the re-raise).
#
# We use try(stop()) as an easy way to generate a try-error object
# out of this condition.
.value <<- cond
.error <<- TRUE
.visible <<- FALSE
@@ -959,12 +969,19 @@ Observer <- R6Class(
if (length(formals(observerFunc)) > 0)
stop("Can't make an observer from a function that takes parameters; ",
"only functions without parameters can be reactive.")
if (grepl("\\s", label, perl = TRUE)) {
funcLabel <- "<observer>"
} else {
funcLabel <- paste0("<observer:", label, ">")
registerDebugHook("observerFunc", environment(), label)
.func <<- function() {
tryCatch(
if (..stacktraceon)
..stacktraceon..(observerFunc())
else
observerFunc(),
# It's OK for shiny.silent.error errors to cause an observer to stop running
shiny.silent.error = function(e) NULL
# validation = function(e) NULL,
# shiny.output.cancel = function(e) NULL
)
}
.func <<- wrapFunctionLabel(observerFunc, funcLabel, ..stacktraceon = ..stacktraceon)
.label <<- label
.domain <<- domain
.priority <<- normalizePriority(priority)
@@ -1029,15 +1046,6 @@ Observer <- R6Class(
}
},
catch = function(e) {
# It's OK for shiny.silent.error errors to cause an observer to stop running
# shiny.silent.error = function(e) NULL
# validation = function(e) NULL,
# shiny.output.cancel = function(e) NULL
if (inherits(e, "shiny.silent.error")) {
return()
}
printError(e)
if (!is.null(.domain)) {
.domain$unhandledError(e)
@@ -1394,28 +1402,20 @@ reactiveTimer <- function(intervalMs=1000, session = getDefaultReactiveDomain())
force(session)
dependents <- Map$new()
timerHandle <- scheduleTask(intervalMs, function() {
timerCallbacks$schedule(intervalMs, function() {
# Quit if the session is closed
if (!is.null(session) && session$isClosed()) {
return(invisible())
}
timerHandle <<- scheduleTask(intervalMs, sys.function())
session$cycleStartAction(function() {
lapply(
dependents$values(),
function(dep.ctx) {
dep.ctx$invalidate()
NULL
})
})
timerCallbacks$schedule(intervalMs, sys.function())
lapply(
dependents$values(),
function(dep.ctx) {
dep.ctx$invalidate()
NULL
})
})
if (!is.null(session)) {
session$onEnded(timerHandle)
}
return(function() {
ctx <- .getReactiveEnvironment()$currentContext()
if (!dependents$containsKey(ctx$id)) {
@@ -1485,7 +1485,7 @@ reactiveTimer <- function(intervalMs=1000, session = getDefaultReactiveDomain())
invalidateLater <- function(millis, session = getDefaultReactiveDomain()) {
force(session)
ctx <- .getReactiveEnvironment()$currentContext()
timerHandle <- scheduleTask(millis, function() {
timerCallbacks$schedule(millis, function() {
if (is.null(session)) {
ctx$invalidate()
return(invisible())
@@ -1499,11 +1499,6 @@ invalidateLater <- function(millis, session = getDefaultReactiveDomain()) {
invisible()
})
if (!is.null(session)) {
session$onEnded(timerHandle)
}
invisible()
}
@@ -1821,20 +1816,15 @@ maskReactiveContext <- function(expr) {
#' the action/calculation and just let the user re-initiate it (like a
#' "Recalculate" button).
#'
#' Likewise, both \code{observeEvent} and \code{eventReactive} also take in an
#' \code{ignoreInit} argument. By default, both of these will run right when they
#' are created (except if, at that moment, \code{eventExpr} evaluates to \code{NULL}
#' Unlike what happens for \code{ignoreNULL}, only \code{observeEvent} takes in an
#' \code{ignoreInit} argument. By default, \code{observeEvent} will run right when
#' it is created (except if, at that moment, \code{eventExpr} evaluates to \code{NULL}
#' and \code{ignoreNULL} is \code{TRUE}). But when responding to a click of an action
#' button, it may often be useful to set \code{ignoreInit} to \code{TRUE}. For
#' example, if you're setting up an \code{observeEvent} for a dynamically created
#' button, then \code{ignoreInit = TRUE} will guarantee that the action (in
#' \code{handlerExpr}) will only be triggered when the button is actually clicked,
#' instead of also being triggered when it is created/initialized. Similarly,
#' if you're setting up an \code{eventReactive} that responds to a dynamically
#' created button used to refresh some data (then returned by that \code{eventReactive}),
#' then you should use \code{eventReactive([...], ignoreInit = TRUE)} if you want
#' to let the user decide if/when they want to refresh the data (since, depending
#' on the app, this may be a computationally expensive operation).
#' instead of also being triggered when it is created/initialized.
#'
#' Even though \code{ignoreNULL} and \code{ignoreInit} can be used for similar
#' purposes they are independent from one another. Here's the result of combining
@@ -1842,28 +1832,25 @@ maskReactiveContext <- function(expr) {
#'
#' \describe{
#' \item{\code{ignoreNULL = TRUE} and \code{ignoreInit = FALSE}}{
#' This is the default. This combination means that \code{handlerExpr}/
#' \code{valueExpr} will run every time that \code{eventExpr} is not
#' \code{NULL}. If, at the time of the creation of the
#' \code{observeEvent}/\code{eventReactive}, \code{eventExpr} happens
#' to \emph{not} be \code{NULL}, then the code runs.
#' This is the default. This combination means that \code{handlerExpr} will
#' run every time that \code{eventExpr} is not \code{NULL}. If, at the time
#' of the \code{observeEvent}'s creation, \code{handleExpr} happens to
#' \emph{not} be \code{NULL}, then the code runs.
#' }
#' \item{\code{ignoreNULL = FALSE} and \code{ignoreInit = FALSE}}{
#' This combination means that \code{handlerExpr}/\code{valueExpr} will
#' run every time no matter what.
#' This combination means that \code{handlerExpr} will run every time no
#' matter what.
#' }
#' \item{\code{ignoreNULL = FALSE} and \code{ignoreInit = TRUE}}{
#' This combination means that \code{handlerExpr}/\code{valueExpr} will
#' \emph{not} run when the \code{observeEvent}/\code{eventReactive} is
#' created (because \code{ignoreInit = TRUE}), but it will run every
#' other time.
#' This combination means that \code{handlerExpr} will \emph{not} run when
#' the \code{observeEvent} is created (because \code{ignoreInit = TRUE}),
#' but it will run every other time.
#' }
#' \item{\code{ignoreNULL = TRUE} and \code{ignoreInit = TRUE}}{
#' This combination means that \code{handlerExpr}/\code{valueExpr} will
#' \emph{not} run when the \code{observeEvent}/\code{eventReactive} is
#' created (because \code{ignoreInit = TRUE}). After that,
#' \code{handlerExpr}/\code{valueExpr} will run every time that
#' \code{eventExpr} is not \code{NULL}.
#' This combination means that \code{handlerExpr} will \emph{not} run when
#' the \code{observeEvent} is created (because \code{ignoreInit = TRUE}).
#' After that, \code{handlerExpr} will run every time that \code{eventExpr}
#' is not \code{NULL}.
#' }
#' }
#'
@@ -2003,80 +1990,35 @@ observeEvent <- function(eventExpr, handlerExpr,
initialized <- FALSE
o <- observe({
hybrid_chain(
{eventFunc()},
function(value) {
if (ignoreInit && !initialized) {
initialized <<- TRUE
return()
}
e <- eventFunc()
if (ignoreNULL && isNullEvent(value)) {
return()
}
if (ignoreInit && !initialized) {
initialized <<- TRUE
return()
}
if (once) {
on.exit(o$destroy())
}
if (ignoreNULL && isNullEvent(e)) {
return()
}
isolate(handlerFunc())
}
)
if (once) {
on.exit(o$destroy())
}
isolate(handlerFunc())
}, label = label, suspended = suspended, priority = priority, domain = domain,
autoDestroy = TRUE, ..stacktraceon = FALSE)
invisible(o)
}
#' @section \code{eventReactive} caching:
#'
#' Like regular \code{\link{reactive}} expressions, the most recent value of a
#' \code{eventReactive} is always cached. (Observers are not cached because
#' they are used for their side-effects, not their values.) If a
#' \code{reactive} or \code{eventReactive} named \code{r} is called with
#' \code{r()} and then called again (without being invalidated in between),
#' then the second call will simply return the most recent value.
#'
#' An \code{eventReactive} allows for caching of previous values, by using the
#' \code{cache} parameter. When this additional caching is used, a key-value
#' store is used, where the result of the \code{eventExpr} is used as the key.
#' More specifically, the result from the \code{eventExpr} is combined with
#' the \code{eventReactive}'s \code{label} (which defaults to a string
#' representation of the \code{expr} code), and they are serialized and hashed
#' to generate the key.
#'
#' When an additional cache is used, it allow for sharing cached values with
#' other sessions. If you use \code{cache="session"}, then a separate cache
#' will be used for each user session. If you use \code{cache="app"}, then the
#' cache for the \code{eventReactive} will be shared across multiple client
#' sessions accessing the same Shiny application -- because the \code{label}
#' will (by default) be the same when the \code{expr} code is the same, an
#' \code{eventReactive} in one session can share values with the corresponding
#' \code{eventReactive} in another session. Whenever they have the same result
#' for \code{eventExpr}, the value can be drawn from the cache instead of
#' being recomputed.
#'
#' Other types of caching are possible, by passing a cache object with
#' \code{$get()} and \code{$set()} methods. It is possible to cache the values
#' to disk, or in an external database, and have the cache persist across
#' application restarts. See \code{\link{renderCachedPlot}} for more
#' information about caching with Shiny.
#'
#'
#' @param cache Extra caching to use for \code{eventReactive}. Note that the
#' most recent value is always cached, but this option allows you to cache
#' previous values based on the value of \code{eventExpr}. If \code{NULL} (the
#' default), do not use extra caching. Other possible values are \code{"app"}
#' for an application-level cache, \code{"session"} for a session-level cache,
#' or a cache object with \code{$get()} and \code{$set()} methods. See
#' \code{\link{renderCachedPlot}} for more information about using caching.
#' @rdname observeEvent
#' @export
eventReactive <- function(eventExpr, valueExpr,
event.env = parent.frame(), event.quoted = FALSE,
value.env = parent.frame(), value.quoted = FALSE,
label = NULL, domain = getDefaultReactiveDomain(),
ignoreNULL = TRUE, ignoreInit = FALSE, cache = NULL) {
ignoreNULL = TRUE, ignoreInit = FALSE) {
eventFunc <- exprToFunction(eventExpr, event.env, event.quoted)
if (is.null(label))
@@ -2088,64 +2030,17 @@ eventReactive <- function(eventExpr, valueExpr,
initialized <- FALSE
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 (is.null(cache)) {
# No cache
return()
} else 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$getCache()
} else {
stop('`cache` must either be NULL, "app", "session", or a cache object with methods, `$get`, and `$set`.')
}
}
ensureCacheSetup()
invisible(reactive({
hybrid_chain(
eventFunc(),
function(value) {
if (ignoreInit && !initialized) {
initialized <<- TRUE
req(FALSE)
}
e <- eventFunc()
req(!ignoreNULL || !isNullEvent(value))
if (ignoreInit && !initialized) {
initialized <<- TRUE
req(FALSE)
}
if (is.null(cache)) {
return( isolate(handlerFunc()) )
req(!ignoreNULL || !isNullEvent(e))
} else {
key <- digest::digest(list(value, label), "sha256")
cached_value <- cache$get(key)
if (!is.key_missing(cached_value)) {
return(cached_value)
}
result <- isolate(handlerFunc())
cache$set(key, result)
return(result)
}
}
)
isolate(handlerFunc())
}, label = label, domain = domain, ..stacktraceon = FALSE))
}

View File

@@ -1,582 +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}.
#'
#' @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), "sha256")
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)
)
}
}

View File

@@ -93,22 +93,13 @@ renderPlot <- function(expr, width='auto', height='auto', res=72, ...,
# return a promise). The idea is that the (cached) return value from this
# reactive can be used for varying width/heights, as it includes the
# displaylist, which is resolution independent.
drawReactive <- reactive(label = "plotObj", {
drawReactive <- reactive(label = "plotObj", ..stacktraceon = FALSE, {
hybrid_chain(
{
# If !execOnResize, don't invalidate when width/height changes.
dims <- if (execOnResize) getDims() else isolate(getDims())
pixelratio <- session$clientData$pixelratio %OR% 1
do.call("drawPlot", c(
list(
name = outputName,
session = session,
func = func,
width = dims$width,
height = dims$height,
pixelratio = pixelratio,
res = res
), args))
drawPlot(outputName, session, func, dims$width, dims$height, pixelratio, res)
},
catch = function(reason) {
# Non-isolating read. A common reason for errors in plotting is because
@@ -133,12 +124,7 @@ renderPlot <- function(expr, width='auto', height='auto', res=72, ...,
function(result) {
dims <- getDims()
pixelratio <- session$clientData$pixelratio %OR% 1
result <- do.call("resizeSavedPlot", c(
list(name, shinysession, result, dims$width, dims$height, pixelratio, res),
args
))
result$img
resizeSavedPlot(name, shinysession, result, dims$width, dims$height, pixelratio, res)
}
)
}
@@ -153,28 +139,26 @@ renderPlot <- function(expr, width='auto', height='auto', res=72, ...,
markRenderFunction(outputFunc, renderFunc, outputArgs = outputArgs)
}
resizeSavedPlot <- function(name, session, result, width, height, pixelratio, res, ...) {
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, height, pixelratio, res)
}, width = width*pixelratio, height = height*pixelratio, res = res*pixelratio, ...)
}, 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, ...) {
@@ -251,7 +235,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() {
@@ -430,10 +413,10 @@ getPrevPlotCoordmap <- function(width, height) {
),
# The bounds of the plot area, in DOM pixels
range = list(
left = graphics::grconvertX(usrBounds[1], 'user', 'ndc') * width,
right = graphics::grconvertX(usrBounds[2], 'user', 'ndc') * width,
bottom = (1-graphics::grconvertY(usrBounds[3], 'user', 'ndc')) * height - 1,
top = (1-graphics::grconvertY(usrBounds[4], 'user', 'ndc')) * height - 1
left = graphics::grconvertX(usrBounds[1], 'user', 'nfc') * width,
right = graphics::grconvertX(usrBounds[2], 'user', 'nfc') * width,
bottom = (1-graphics::grconvertY(usrBounds[3], 'user', 'nfc')) * height - 1,
top = (1-graphics::grconvertY(usrBounds[4], 'user', 'nfc')) * height - 1
),
log = list(
x = if (graphics::par('xlog')) 10 else NULL,
@@ -560,11 +543,9 @@ find_panel_info_api <- function(b) {
# ggplot object. The original uses quoted expressions; convert to
# character.
mapping <- layers$mapping[[1]]
# In ggplot2 <=2.2.1, the mappings are expressions. In later versions, they
# are quosures. `deparse(quo_squash(x))` will handle both cases.
# as.character results in unexpected behavior for expressions like `wt/2`,
# which is why we use deparse.
mapping <- lapply(mapping, function(x) deparse(rlang::quo_squash(x)))
# lapply'ing as.character results in unexpected behavior for expressions
# like `wt/2`; deparse handles it correctly.
mapping <- lapply(mapping, deparse)
# If either x or y is not present, give it a NULL entry.
mapping <- mergeVectors(list(x = NULL, y = NULL), mapping)
@@ -746,9 +727,8 @@ find_panel_info_non_api <- function(b, ggplot_format) {
mappings <- c(list(mappings), layer_mappings)
mappings <- Reduce(x = mappings, init = list(x = NULL, y = NULL),
function(init, m) {
# Can't use m$x/m$y; you get a partial match with xintercept/yintercept
if (is.null(init[["x"]]) && !is.null(m[["x"]])) init$x <- m[["x"]]
if (is.null(init[["y"]]) && !is.null(m[["y"]])) init$y <- m[["y"]]
if (is.null(init$x) && !is.null(m$x)) init$x <- m$x
if (is.null(init$y) && !is.null(m$y)) init$y <- m$y
init
}
)

View File

@@ -142,7 +142,6 @@ registerInputHandler("shiny.matrix", function(data, ...) {
return(m)
})
registerInputHandler("shiny.number", function(val, ...){
ifelse(is.null(val), NA, val)
})
@@ -221,21 +220,3 @@ registerInputHandler("shiny.file", function(val, shinysession, name) {
val
})
# to be used with !!!answer
registerInputHandler("shiny.symbolList", function(val, ...) {
if (is.null(val)) {
list()
} else {
lapply(val, as.symbol)
}
})
# to be used with !!answer
registerInputHandler("shiny.symbol", function(val, ...) {
if (is.null(val) || identical(val, "")) {
NULL
} else {
as.symbol(val)
}
})

View File

@@ -53,23 +53,21 @@ registerClient <- function(client) {
#' @export
addResourcePath <- function(prefix, directoryPath) {
prefix <- prefix[1]
if (!grepl('^[a-z0-9\\-_][a-z0-9\\-_.]*$', prefix, ignore.case = TRUE, perl = TRUE)) {
if (!grepl('^[a-z0-9\\-_][a-z0-9\\-_.]*$', prefix, ignore.case=TRUE, perl=TRUE)) {
stop("addResourcePath called with invalid prefix; please see documentation")
}
if (prefix %in% c('shared')) {
stop("addResourcePath called with the reserved prefix '", prefix, "'; ",
"please use a different prefix")
}
normalizedPath <- tryCatch(normalizePath(directoryPath, mustWork = TRUE),
error = function(e) {
stop("Couldn't normalize path in `addResourcePath`, with arguments: ",
"`prefix` = '", prefix, "'; `directoryPath` = '" , directoryPath, "'")
}
)
.globals$resources[[prefix]] <- list(
directoryPath = normalizedPath,
func = staticHandler(normalizedPath)
)
directoryPath <- normalizePath(directoryPath, mustWork=TRUE)
existing <- .globals$resources[[prefix]]
.globals$resources[[prefix]] <- list(directoryPath=directoryPath,
func=staticHandler(directoryPath))
}
resourcePathHandler <- function(req) {
@@ -279,19 +277,7 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
shinysession$setShowcase(mode)
}
# In shinysession$createBookmarkObservers() above, observers may be
# created, which puts the shiny session in busyCount > 0 state. That
# prevents the manageInputs here from taking immediate effect, by
# default. The manageInputs here needs to take effect though, because
# otherwise the bookmark observers won't find the clientData they are
# looking for. So use `now = TRUE` to force the changes to be
# immediate.
#
# FIXME: break createBookmarkObservers into two separate steps, one
# before and one after manageInputs, and put the observer creation
# in the latter. Then add an assertion that busyCount == 0L when
# this manageInputs is called.
shinysession$manageInputs(msg$data, now = TRUE)
shinysession$manageInputs(msg$data)
# The client tells us what singletons were rendered into
# the initial page
@@ -419,10 +405,7 @@ startApp <- function(appObj, port, host, quiet) {
if (is.numeric(port) || is.integer(port)) {
if (!quiet) {
hostString <- host
if (httpuv::ipFamily(host) == 6L)
hostString <- paste0("[", hostString, "]")
message('\n', 'Listening on http://', hostString, ':', port)
message('\n', 'Listening on http://', host, ':', port)
}
return(startServer(host, port, handlerManager$createHttpuvApp()))
} else if (is.character(port)) {
@@ -579,16 +562,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 +577,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
@@ -782,17 +756,8 @@ runApp <- function(appDir=getwd(),
}, add = TRUE)
if (!is.character(port)) {
browseHost <- host
if (identical(host, "0.0.0.0")) {
# http://0.0.0.0/ doesn't work on QtWebKit (i.e. RStudio viewer)
browseHost <- "127.0.0.1"
} else if (identical(host, "::")) {
browseHost <- "::1"
}
if (httpuv::ipFamily(browseHost) == 6L) {
browseHost <- paste0("[", browseHost, "]")
}
# http://0.0.0.0/ doesn't work on QtWebKit (i.e. RStudio viewer)
browseHost <- if (identical(host, "0.0.0.0")) "127.0.0.1" else host
appUrl <- paste("http://", browseHost, ":", port, sep="")
if (is.function(launch.browser))
@@ -817,7 +782,7 @@ runApp <- function(appDir=getwd(),
..stacktraceoff..(
captureStackTraces({
while (!.globals$stopped) {
..stacktracefloor..(serviceApp())
serviceApp()
Sys.sleep(0.001)
}
})

125
R/shiny.R
View File

@@ -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,
@@ -719,8 +706,8 @@ ShinySession <- R6Class(
private$flushCallbacks <- Callbacks$new()
private$flushedCallbacks <- Callbacks$new()
private$inputReceivedCallbacks <- Callbacks$new()
private$.input <- ReactiveValues$new(dedupe = FALSE)
private$.clientData <- ReactiveValues$new(dedupe = TRUE)
private$.input <- ReactiveValues$new()
private$.clientData <- ReactiveValues$new()
private$timingRecorder <- ShinyServerTimingRecorder$new()
self$progressStack <- Stack$new()
self$files <- Map$new()
@@ -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()
@@ -976,9 +961,8 @@ ShinySession <- R6Class(
stop("x must be a reactivevalues object")
impl <- .subset2(x, 'impl')
key <- .subset2(x, 'ns')(name)
impl$freeze(key)
self$onFlushed(function() impl$thaw(key))
impl$freeze(name)
self$onFlushed(function() impl$thaw(name))
},
onSessionEnded = function(sessionEndedCallback) {
@@ -1040,16 +1024,9 @@ ShinySession <- R6Class(
# name not working unless name was eagerly evaluated. Yikes!
force(name)
# If overwriting an output object, destroy the previous copy of it
# If overwriting an output object, suspend the previous copy of it
if (!is.null(private$.outputs[[name]])) {
private$.outputs[[name]]$destroy()
}
if (is.null(func)) {
# If func is null, give it an "empty" output function so it can go
# through the logic below. If we simply returned at this point, the
# previous output (if any) would continue to show in the client.
func <- missingOutput
private$.outputs[[name]]$suspend()
}
if (is.function(func)) {
@@ -1086,11 +1063,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)
@@ -1193,10 +1166,8 @@ ShinySession <- R6Class(
# Schedule execution of onFlushed callbacks
on.exit({
withReactiveDomain(self, {
# ..stacktraceon matches with the top-level ..stacktraceoff..
private$flushedCallbacks$invoke(..stacktraceon = TRUE)
})
# ..stacktraceon matches with the top-level ..stacktraceoff..
private$flushedCallbacks$invoke(..stacktraceon = TRUE)
}, add = TRUE)
if (!hasPendingUpdates()) {
@@ -1333,47 +1304,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.
@@ -1924,16 +1854,10 @@ ShinySession <- R6Class(
}
}
},
# Set the normal and client data input variables. Normally, managing
# inputs doesn't take immediate effect when there are observers that
# are pending execution or currently executing (including having
# started async operations that have yielded control, but not yet
# completed). The `now` argument can force this. It should generally
# not be used, but we're adding it to get around a show-stopping bug
# for Shiny v1.1 (see the call site for more details).
manageInputs = function(data, now = FALSE) {
# Set the normal and client data input variables
manageInputs = function(data) {
force(data)
doManageInputs <- function() {
self$cycleStartAction(function() {
private$inputReceivedCallbacks$invoke(data)
data_names <- names(data)
@@ -1951,12 +1875,7 @@ ShinySession <- R6Class(
private$.clientData$mset(input_clientdata)
self$manageHiddenOutputs()
}
if (isTRUE(now)) {
doManageInputs()
} else {
self$cycleStartAction(doManageInputs)
}
})
},
outputOptions = function(name, ...) {
# If no name supplied, return the list of options for all outputs
@@ -2117,16 +2036,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.
@@ -2195,9 +2104,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
@@ -2257,7 +2164,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
@@ -2309,5 +2216,3 @@ ShinyServerTimingRecorder <- R6Class("ShinyServerTimingRecorder",
}
)
)
missingOutput <- function(...) req(FALSE)

View File

@@ -1,4 +1,4 @@
utils::globalVariables('func')
globalVariables('func')
#' Mark a function as a render function
#'
@@ -111,17 +111,13 @@ useRenderFunction <- function(renderFunc, inline = FALSE) {
}
id <- createUniqueId(8, "out")
o <- getDefaultReactiveDomain()$output
if (!is.null(o)) {
o[[id]] <- renderFunc
# If there's a namespace, we must respect it
id <- getDefaultReactiveDomain()$ns(id)
}
# Make the id the first positional argument
outputArgs <- c(list(id), outputArgs)
o <- getDefaultReactiveDomain()$output
if (!is.null(o))
o[[id]] <- renderFunc
if (is.logical(formals(outputFunction)[["inline"]]) && !("inline" %in% names(outputArgs))) {
outputArgs[["inline"]] <- inline
}
@@ -437,7 +433,8 @@ renderText <- function(expr, env=parent.frame(), quoted=FALSE,
#' UI Output
#'
#' Renders reactive HTML using the Shiny UI library.
#' \bold{Experimental feature.} Makes a reactive version of a function that
#' generates HTML using the Shiny UI library.
#'
#' The corresponding HTML output tag should be \code{div} and have the CSS class
#' name \code{shiny-html-output} (or use \code{\link{uiOutput}}).
@@ -451,7 +448,7 @@ renderText <- function(expr, env=parent.frame(), quoted=FALSE,
#' call to \code{\link{uiOutput}} when \code{renderUI} is used in an
#' interactive R Markdown document.
#'
#' @seealso \code{\link{uiOutput}}
#' @seealso conditionalPanel
#' @export
#' @examples
#' ## Only run examples in interactive R sessions

View File

@@ -42,17 +42,6 @@ TimerCallbacks <- R6Class(
return(id)
},
unschedule = function(id) {
toRemoveIndices <- .times$id %in% id
toRemoveIds <- .times[toRemoveIndices, "id", drop = TRUE]
if (length(toRemoveIds) > 0) {
.times <<- .times[!toRemoveIndices,]
for (toRemoveId in as.character(toRemoveIds)) {
.funcs$remove(toRemoveId)
}
}
return(id %in% toRemoveIds)
},
timeToNextEvent = function() {
if (dim(.times)[1] == 0)
return(Inf)
@@ -90,9 +79,13 @@ timerCallbacks <- TimerCallbacks$new()
scheduleTask <- function(millis, callback) {
cancelled <- FALSE
id <- timerCallbacks$schedule(millis, callback)
timerCallbacks$schedule(millis, function() {
if (!cancelled)
callback()
})
function() {
invisible(timerCallbacks$unschedule(id))
cancelled <<- TRUE
callback <<- NULL # to allow for callback to be gc'ed
}
}

View File

@@ -383,17 +383,13 @@ updateNumericInput <- function(session, inputId, label = NULL, value = NULL,
session$sendInputMessage(inputId, message)
}
#' Update Slider Input Widget
#'
#' Change the value of a slider input on the client.
#' Change the value of a slider input on the client
#'
#' @template update-input
#' @param value The value to set for the input object.
#' @param min Minimum value.
#' @param max Maximum value.
#' @param step Step size.
#' @param timeFormat Date and POSIXt formatting.
#' @param timezone The timezone offset for POSIXt objects.
#'
#' @seealso \code{\link{sliderInput}}
#'
@@ -426,15 +422,22 @@ updateNumericInput <- function(session, inputId, label = NULL, value = NULL,
#' }
#' @export
updateSliderInput <- function(session, inputId, label = NULL, value = NULL,
min = NULL, max = NULL, step = NULL, timeFormat = NULL, timezone = NULL)
min = NULL, max = NULL, step = NULL)
{
dataType <- getSliderType(min, max, value)
# Make sure that value, min, max all have the same type, because we need
# special handling for dates and datetimes.
vals <- dropNulls(list(value, min, max))
if (is.null(timeFormat)) {
timeFormat <- switch(dataType, date = "%F", datetime = "%F %T", number = NULL)
type <- unique(lapply(vals, function(x) {
if (inherits(x, "Date")) "date"
else if (inherits(x, "POSIXt")) "datetime"
else "number"
}))
if (length(type) > 1) {
stop("Type mismatch for value, min, and max")
}
if (dataType == "date" || dataType == "datetime") {
if ((length(type) == 1) && (type == "date" || type == "datetime")) {
to_ms <- function(x) 1000 * as.numeric(as.POSIXct(x))
if (!is.null(min)) min <- to_ms(min)
if (!is.null(max)) max <- to_ms(max)
@@ -446,10 +449,7 @@ updateSliderInput <- function(session, inputId, label = NULL, value = NULL,
value = formatNoSci(value),
min = formatNoSci(min),
max = formatNoSci(max),
step = formatNoSci(step),
`data-type` = dataType,
`time-format` = timeFormat,
timezone = timezone
step = formatNoSci(step)
))
session$sendInputMessage(inputId, message)
}
@@ -576,7 +576,7 @@ updateRadioButtons <- function(session, inputId, label = NULL, choices = NULL,
#' @template update-input
#' @inheritParams selectInput
#'
#' @seealso \code{\link{selectInput}} \code{\link{varSelectInput}}
#' @seealso \code{\link{selectInput}}
#'
#' @examples
#' ## Only run examples in interactive R sessions
@@ -643,85 +643,32 @@ updateSelectizeInput <- function(session, inputId, label = NULL, choices = NULL,
return(updateSelectInput(session, inputId, label, choices, selected))
}
noOptGroup <- TRUE
if (is.list(choices)) {
# check if list is nested
for (i in seq_along(choices)) {
if (is.list(choices[[i]]) || length(choices[[i]]) > 1) {
noOptGroup <- FALSE
break()
}
}
}
# convert choices to a data frame so it returns [{label: , value: , group: },...]
choices <- if (is.atomic(choices) || noOptGroup) {
# fast path for vectors and flat lists
if (is.list(choices)) {
choices <- unlist(choices)
}
if (is.null(names(choices))) {
# server side updateSelectizeInput
value <- unname(selected)
attr(choices, 'selected_value') <- value
# convert a single vector to a data frame so it returns {label: , value: }
# other objects return arbitrary JSON {x: , y: , foo: , ...}
choices <- if (is.atomic(choices)) {
# fast path
if(is.null(names(choices))) {
lab <- as.character(choices)
} else {
lab <- names(choices)
# replace empty names like: choices = c(a = 1, 2)
# in this case: names(choices) = c("a", "")
# int this case: names(choices) = c("a", "")
# with replacement below choices will be: lab = c("a", "2")
empty_names_indices <- lab == ""
lab[empty_names_indices] <- as.character(choices[empty_names_indices])
}
# lab shold be lower-case for faster case-insensitive matching - grepl(... , fixed = TRUE)
lab <- tolower(lab)
data.frame(label = lab, value = choices, stringsAsFactors = FALSE)
} else {
# slow path for nested lists/optgroups
list_names <- names(choices)
if (is.null(list_names)) {
list_names <- rep("", length(choices))
}
choice_list <- mapply(choices, list_names, FUN = function (choice, name) {
group <- ""
lab <- name
if (lab == "") lab <- as.character(choice)
if (is.list(choice) || length(choice) > 1) {
group <- rep(name, length(choice))
choice <- unlist(choice)
if (is.null(names(choice))) {
lab <- as.character(choice)
} else {
lab <- names(choice)
# replace empty names like: choices = c(a = 1, 2)
# in this case: names(choices) = c("a", "")
# with replacement below choices will be: lab = c("a", "2")
empty_names_indices <- lab == ""
lab[empty_names_indices] <- as.character(choice[empty_names_indices])
}
}
list(
label = lab,
value = as.character(choice),
group = group
)
}, SIMPLIFY = FALSE)
extract_vector <- function(x, name) {
vecs <- lapply(x, `[[`, name)
do.call(c, vecs)
}
data.frame(
label = extract_vector(choice_list, "label"),
value = extract_vector(choice_list, "value"),
group = extract_vector(choice_list, "group"),
stringsAsFactors = FALSE, row.names = NULL
)
# slow path
as.data.frame(choices, stringsAsFactors = FALSE)
}
value <- unname(selected)
attr(choices, 'selected_value') <- value
message <- dropNulls(list(
label = label,
value = value,
@@ -729,53 +676,13 @@ updateSelectizeInput <- function(session, inputId, label = NULL, choices = NULL,
))
session$sendInputMessage(inputId, message)
}
#' @rdname updateSelectInput
#' @inheritParams varSelectInput
#' @export
updateVarSelectInput <- function(session, inputId, label = NULL, data = NULL, selected = NULL) {
if (is.null(data)) {
choices <- NULL
} else {
choices <- colnames(data)
}
updateSelectInput(
session = session,
inputId = inputId,
label = label,
choices = choices,
selected = selected
)
}
#' @rdname updateSelectInput
#' @export
updateVarSelectizeInput <- function(session, inputId, label = NULL, data = NULL, selected = NULL, options = list(), server = FALSE) {
if (is.null(data)) {
choices <- NULL
} else {
choices <- colnames(data)
}
updateSelectizeInput(
session = session,
inputId = inputId,
label = label,
choices = choices,
selected = selected,
options = options,
server = server
)
}
selectizeJSON <- function(data, req) {
query <- parseQueryString(req$QUERY_STRING)
# extract the query variables, conjunction (and/or), search string, maximum options
var <- c(safeFromJSON(query$field))
# all keywords in lower-case, for case-insensitive matching
key <- unique(strsplit(tolower(query$query), '\\s+')[[1]])
if (identical(key, '')) key <- character(0)
mop <- as.numeric(query$maxop)
vfd <- query$value # the value field name
@@ -788,13 +695,19 @@ selectizeJSON <- function(data, req) {
matches <- do.call(
cbind,
lapply(key, function(k) {
grepl(k, tolower(as.character(data[[v]])), fixed = TRUE)
if(is.character(data[[v]])) {
# according to updateSelectizeInput() we know that
# `data[[v]]` already in lower case
grepl(k, data[[v]], fixed = TRUE)
} else {
grepl(k, tolower(as.character(data[[v]])), fixed = TRUE)
}
})
)
# merge column matches using OR, and match multiple keywords in one column
# using the conjunction setting (AND or OR)
matches <- rowSums(matches)
if (query$conju == 'and')
if(query$conju == 'and')
idx <- idx | (matches == length(key))
else
idx <- idx | matches

View File

@@ -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.
@@ -1693,50 +1674,3 @@ setVisible <- function(value, visible) {
(value)
}
}
createVarPromiseDomain <- function(env, name, value) {
force(env)
force(name)
force(value)
promises::new_promise_domain(
wrapOnFulfilled = function(onFulfilled) {
function(...) {
orig <- env[[name]]
env[[name]] <- value
on.exit(env[[name]] <- orig)
onFulfilled(...)
}
},
wrapOnRejected = function(onRejected) {
function(...) {
orig <- env[[name]]
env[[name]] <- value
on.exit(env[[name]] <- orig)
onRejected(...)
}
},
wrapSync = function(expr) {
orig <- env[[name]]
env[[name]] <- value
on.exit(env[[name]] <- orig)
force(expr)
}
)
}
getSliderType <- function(min, max, value) {
vals <- dropNulls(list(value, min, max))
type <- unique(lapply(vals, function(x) {
if (inherits(x, "Date")) "date"
else if (inherits(x, "POSIXt")) "datetime"
else "number"
}))
if (length(type) > 1) {
stop("Type mismatch for `min`, `max`, and `value`. Each must be Date, POSIXt, or number.")
}
type[[1]]
}

View File

@@ -41,7 +41,6 @@ sd_section("UI Inputs",
"numericInput",
"radioButtons",
"selectInput",
"varSelectInput",
"sliderInput",
"submitButton",
"textInput",
@@ -105,7 +104,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 +195,7 @@ sd_section("Utility functions",
"exprToFunction",
"installExprFunction",
"parseQueryString",
"getCurrentOutputInfo",
"plotPNG",
"sizeGrowthRatio",
"exportTestValues",
"setSerializer",
"snapshotExclude",
@@ -210,10 +206,7 @@ sd_section("Utility functions",
"shinyDeprecated",
"serverInfo",
"shiny-options",
"onStop",
"diskCache",
"memoryCache",
"key_missing"
"onStop"
)
)
sd_section("Plot interaction",

View File

@@ -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;
@@ -386,10 +381,3 @@ pre.shiny-text-output.noplaceholder:empty {
.shiny-file-input-over {
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(76, 174, 76, .6);
}
/* Overrides bootstrap-datepicker3.css styling for invalid date ranges.
See https://github.com/rstudio/shiny/issues/2042 for details. */
.datepicker table tbody tr td.disabled,
.datepicker table tbody tr td.disabled:hover {
color: #aaa;
}

View File

@@ -14,7 +14,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
var exports = window.Shiny = window.Shiny || {};
exports.version = "1.1.0.9000"; // Version number inserted by Grunt
exports.version = "1.0.5.9000"; // Version number inserted by Grunt
var origPushState = window.history.pushState;
window.history.pushState = function () {
@@ -762,34 +762,26 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
this.lastChanceCallback = [];
};
(function () {
this.setInput = function (name, value, opts) {
this.setInput = function (name, value) {
var self = this;
this.pendingData[name] = value;
if (!this.reentrant) {
if (opts.priority === "event") {
this.$sendNow();
} else if (!this.timerId) {
this.timerId = setTimeout(this.$sendNow.bind(this), 0);
}
}
};
this.$sendNow = function () {
if (this.reentrant) {
console.trace("Unexpected reentrancy in InputBatchSender!");
}
this.reentrant = true;
try {
this.timerId = null;
$.each(this.lastChanceCallback, function (i, callback) {
callback();
});
var currentData = this.pendingData;
this.pendingData = {};
this.shinyapp.sendInput(currentData);
} finally {
this.reentrant = false;
if (!this.timerId && !this.reentrant) {
this.timerId = setTimeout(function () {
self.reentrant = true;
try {
$.each(self.lastChanceCallback, function (i, callback) {
callback();
});
self.timerId = null;
var currentData = self.pendingData;
self.pendingData = {};
self.shinyapp.sendInput(currentData);
} finally {
self.reentrant = false;
}
}, 0);
}
};
}).call(InputBatchSender.prototype);
@@ -799,7 +791,11 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
this.lastSentValues = this.reset(initialValues);
};
(function () {
this.setInput = function (name, value, opts) {
this.setInput = function (name, value) {
// Note that opts is not passed to setInput at this stage of the input
// decorator stack. If in the future this setInput keeps track of opts, it
// would be best not to store the `el`, because that could prevent it from
// being GC'd.
var _splitInputNameType = splitInputNameType(name);
var inputName = _splitInputNameType.name;
@@ -807,11 +803,11 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
var jsonValue = JSON.stringify(value);
if (opts.priority !== "event" && this.lastSentValues[inputName] && this.lastSentValues[inputName].jsonValue === jsonValue && this.lastSentValues[inputName].inputType === inputType) {
if (this.lastSentValues[inputName] && this.lastSentValues[inputName].jsonValue === jsonValue && this.lastSentValues[inputName].inputType === inputType) {
return;
}
this.lastSentValues[inputName] = { jsonValue: jsonValue, inputType: inputType };
this.target.setInput(name, value, opts);
this.target.setInput(name, value);
};
this.reset = function () {
var values = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
@@ -854,7 +850,6 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
evt.value = value;
evt.binding = opts.binding;
evt.el = opts.el;
evt.priority = opts.priority;
$(document).trigger(evt);
@@ -862,9 +857,9 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
name = evt.name;
if (evt.inputType !== '') name += ':' + evt.inputType;
// Most opts aren't passed along to lower levels in the input decorator
// opts aren't passed along to lower levels in the input decorator
// stack.
this.target.setInput(name, evt.value, { priority: opts.priority });
this.target.setInput(name, evt.value);
}
};
}).call(InputEventDecorator.prototype);
@@ -877,7 +872,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
this.setInput = function (name, value, opts) {
this.$ensureInit(name);
if (opts.priority !== "deferred") this.inputRatePolicies[name].immediateCall(name, value, opts);else this.inputRatePolicies[name].normalCall(name, value, opts);
if (opts.immediate) this.inputRatePolicies[name].immediateCall(name, value, opts);else this.inputRatePolicies[name].normalCall(name, value, opts);
};
this.setRatePolicy = function (name, mode, millis) {
if (mode === 'direct') {
@@ -929,25 +924,11 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
// Merge opts with defaults, and return a new object.
function addDefaultInputOpts(opts) {
opts = $.extend({
priority: "immediate",
return $.extend({
immediate: false,
binding: null,
el: null
}, opts);
if (opts && typeof opts.priority !== "undefined") {
switch (opts.priority) {
case "deferred":
case "immediate":
case "event":
break;
default:
throw new Error("Unexpected input value mode: '" + opts.priority + "'");
}
}
return opts;
}
function splitInputNameType(name) {
@@ -1284,22 +1265,17 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
};
this.receiveOutput = function (name, value) {
if (this.$values[name] === value) return undefined;
this.$values[name] = value;
delete this.$errors[name];
var binding = this.$bindings[name];
var evt = jQuery.Event('shiny:value');
evt.name = name;
evt.value = value;
evt.binding = binding;
if (this.$values[name] === value) {
$(binding ? binding.el : document).trigger(evt);
return undefined;
}
this.$values[name] = value;
delete this.$errors[name];
$(binding ? binding.el : document).trigger(evt);
if (!evt.isDefaultPrevented() && binding) {
binding.onValueChange(evt.value);
}
@@ -2987,7 +2963,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
return function (e) {
if (e === null) {
exports.setInputValue(inputId, null);
exports.onInputChange(inputId, null);
return;
}
@@ -2995,7 +2971,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
// If outside of plotting region
if (!coordmap.isInPanel(offset)) {
if (nullOutside) {
exports.setInputValue(inputId, null);
exports.onInputChange(inputId, null);
return;
}
if (clip) return;
@@ -3016,7 +2992,8 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
coords.range = panel.range;
coords.log = panel.log;
exports.setInputValue(inputId, coords, { priority: "event" });
coords[".nonce"] = Math.random();
exports.onInputChange(inputId, coords);
};
};
};
@@ -3203,7 +3180,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
// We're in a new or reset state
if (isNaN(coords.xmin)) {
exports.setInputValue(inputId, null);
exports.onInputChange(inputId, null);
// Must tell other brushes to clear.
imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
brushId: inputId, outputId: null
@@ -3230,7 +3207,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
coords.outputId = outputId;
// Send data to server
exports.setInputValue(inputId, coords);
exports.onInputChange(inputId, coords);
$el.data("mostRecentBrush", true);
imageOutputBinding.find(document).trigger("shiny-internal:brushed", coords);
@@ -3864,7 +3841,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
};
exports.resetBrush = function (brushId) {
exports.setInputValue(brushId, null);
exports.onInputChange(brushId, null);
imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
brushId: brushId, outputId: null
});
@@ -4449,33 +4426,6 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
if (slider.$cache && slider.$cache.input) slider.$cache.input.trigger('change');else console.log("Couldn't force ion slider to update");
}
function getTypePrettifyer(dataType, timeFormat, timezone) {
var timeFormatter;
var prettify;
if (dataType === 'date') {
timeFormatter = strftime.utc();
prettify = function prettify(num) {
return timeFormatter(timeFormat, new Date(num));
};
} else if (dataType === 'datetime') {
if (timezone) timeFormatter = strftime.timezone(timezone);else timeFormatter = strftime;
prettify = function prettify(num) {
return timeFormatter(timeFormat, new Date(num));
};
} else {
// The default prettify function for ion.rangeSlider adds thousands
// separators after the decimal mark, so we have our own version here.
// (#1958)
prettify = function prettify(num) {
// When executed, `this` will refer to the `IonRangeSlider.options`
// object.
return formatNumber(num, this.prettify_separator);
};
}
return prettify;
}
var sliderInputBinding = {};
$.extend(sliderInputBinding, textInputBinding, {
find: function find(scope) {
@@ -4554,30 +4504,12 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
msg.from = data.value;
}
}
var sliderFeatures = ['min', 'max', 'step'];
for (var i = 0; i < sliderFeatures.length; i++) {
var feats = sliderFeatures[i];
if (data.hasOwnProperty(feats)) {
msg[feats] = data[feats];
}
}
if (data.hasOwnProperty('min')) msg.min = data.min;
if (data.hasOwnProperty('max')) msg.max = data.max;
if (data.hasOwnProperty('step')) msg.step = data.step;
if (data.hasOwnProperty('label')) $el.parent().find('label[for="' + $escape(el.id) + '"]').text(data.label);
var domElements = ['data-type', 'time-format', 'timezone'];
for (var i = 0; i < domElements.length; i++) {
var elem = domElements[i];
if (data.hasOwnProperty(elem)) {
$el.data(elem, data[elem]);
}
}
var dataType = $el.data('data-type');
var timeFormat = $el.data('time-format');
var timezone = $el.data('timezone');
msg.prettify = getTypePrettifyer(dataType, timeFormat, timezone);
$el.data('immediate', true);
try {
slider.update(msg);
@@ -4598,9 +4530,31 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
var $el = $(el);
var dataType = $el.data('data-type');
var timeFormat = $el.data('time-format');
var timezone = $el.data('timezone');
var timeFormatter;
opts.prettify = getTypePrettifyer(dataType, timeFormat, timezone);
// Set up formatting functions
if (dataType === 'date') {
timeFormatter = strftime.utc();
opts.prettify = function (num) {
return timeFormatter(timeFormat, new Date(num));
};
} else if (dataType === 'datetime') {
var timezone = $el.data('timezone');
if (timezone) timeFormatter = strftime.timezone(timezone);else timeFormatter = strftime;
opts.prettify = function (num) {
return timeFormatter(timeFormat, new Date(num));
};
} else {
// The default prettify function for ion.rangeSlider adds thousands
// separators after the decimal mark, so we have our own version here.
// (#1958)
opts.prettify = function (num) {
// When executed, `this` will refer to the `IonRangeSlider.options`
// object.
return formatNumber(num, this.prettify_separator);
};
}
$el.ionRangeSlider(opts);
},
@@ -5066,18 +5020,6 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
find: function find(scope) {
return $(scope).find('select');
},
getType: function getType(el) {
var $el = $(el);
if (!$el.hasClass("symbol")) {
// default character type
return null;
}
if ($el.attr("multiple") === "multiple") {
return 'shiny.symbolList';
} else {
return 'shiny.symbol';
}
},
getId: function getId(el) {
return InputBinding.prototype.getId.call(this, el) || el.name;
},
@@ -5129,7 +5071,8 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
if (data.hasOwnProperty('url')) {
selectize = this._selectize(el);
selectize.clearOptions();
var loaded = false;
var thiz = this,
loaded = false;
selectize.settings.load = function (query, callback) {
var settings = selectize.settings;
$.ajax({
@@ -5146,19 +5089,8 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
callback();
},
success: function success(res) {
// res = [{label: '1', value: '1', group: '1'}, ...]
// success is called after options are added, but
// groups need to be added manually below
$.each(res, function (index, elem) {
selectize.addOptionGroup(elem.group, { group: elem.group });
});
callback(res);
if (!loaded && data.hasOwnProperty('value')) {
selectize.setValue(data.value);
} else if (settings.maxItems === 1) {
// first item selected by default only for single-select
selectize.setValue(res[0].value);
}
if (!loaded && data.hasOwnProperty('value')) thiz.setValue(el, data.value);
loaded = true;
}
});
@@ -5194,10 +5126,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
var options = $.extend({
labelField: 'label',
valueField: 'value',
searchField: ['label'],
optgroupField: 'group',
optgroupLabelField: 'group',
optgroupValueField: 'group'
searchField: ['label']
}, JSON.parse(config.html()));
// selectize created from selectInput()
if (typeof config.data('nonempty') !== 'undefined') {
@@ -5843,7 +5772,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
// Attach a dragenter handler to $el and all of its children. When the first
// child is entered, trigger a draghoverstart event.
$el.on("dragenter.dragHover", function (e) {
if (collection.length === 0) {
if (collection.size() === 0) {
$el.trigger("draghoverstart" + ns, e.originalEvent);
}
// Every child that has fired dragenter is added to the collection.
@@ -5858,7 +5787,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
collection = collection.not(e.originalEvent.target);
// When the collection has no elements, all of the children have been
// removed, and produce draghoverend event.
if (collection.length === 0) {
if (collection.size() === 0) {
$el.trigger("draghoverend" + ns, e.originalEvent);
}
});
@@ -6138,7 +6067,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
inputs = new InputValidateDecorator(inputs);
exports.setInputValue = exports.onInputChange = function (name, value, opts) {
exports.onInputChange = function (name, value, opts) {
opts = addDefaultInputOpts(opts);
inputs.setInput(name, value, opts);
};
@@ -6152,11 +6081,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
var type = binding.getType(el);
if (type) id = id + ":" + type;
var opts = {
priority: allowDeferred ? "deferred" : "immediate",
binding: binding,
el: el
};
var opts = { immediate: !allowDeferred, binding: binding, el: el };
inputs.setInput(id, value, opts);
}
}
@@ -6319,7 +6244,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
// The server needs to know the size of each image and plot output element,
// in case it is auto-sizing
$('.shiny-image-output, .shiny-plot-output, .shiny-report-size').each(function () {
$('.shiny-image-output, .shiny-plot-output').each(function () {
var id = getIdFromEl(this);
if (this.offsetWidth !== 0 || this.offsetHeight !== 0) {
initialValues['.clientdata_output_' + id + '_width'] = this.offsetWidth;
@@ -6327,7 +6252,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
}
});
function doSendImageSize() {
$('.shiny-image-output, .shiny-plot-output, .shiny-report-size').each(function () {
$('.shiny-image-output, .shiny-plot-output').each(function () {
var id = getIdFromEl(this);
if (this.offsetWidth !== 0 || this.offsetHeight !== 0) {
inputs.setInput('.clientdata_output_' + id + '_width', this.offsetWidth);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

@@ -62,7 +62,5 @@ Other input elements: \code{\link{checkboxGroupInput}},
\code{\link{numericInput}}, \code{\link{passwordInput}},
\code{\link{radioButtons}}, \code{\link{selectInput}},
\code{\link{sliderInput}}, \code{\link{submitButton}},
\code{\link{textAreaInput}}, \code{\link{textInput}},
\code{\link{varSelectInput}}
\code{\link{textAreaInput}}, \code{\link{textInput}}
}
\concept{input elements}

View File

@@ -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

View File

@@ -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"},

View File

@@ -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.}
@@ -94,7 +93,5 @@ Other input elements: \code{\link{actionButton}},
\code{\link{numericInput}}, \code{\link{passwordInput}},
\code{\link{radioButtons}}, \code{\link{selectInput}},
\code{\link{sliderInput}}, \code{\link{submitButton}},
\code{\link{textAreaInput}}, \code{\link{textInput}},
\code{\link{varSelectInput}}
\code{\link{textAreaInput}}, \code{\link{textInput}}
}
\concept{input elements}

View File

@@ -46,6 +46,5 @@ Other input elements: \code{\link{actionButton}},
\code{\link{passwordInput}}, \code{\link{radioButtons}},
\code{\link{selectInput}}, \code{\link{sliderInput}},
\code{\link{submitButton}}, \code{\link{textAreaInput}},
\code{\link{textInput}}, \code{\link{varSelectInput}}
\code{\link{textInput}}
}
\concept{input elements}

View File

@@ -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

View File

@@ -6,7 +6,7 @@
\usage{
dateInput(inputId, label, value = NULL, min = NULL, max = NULL,
format = "yyyy-mm-dd", startview = "month", weekstart = 0,
language = "en", width = NULL, autoclose = TRUE)
language = "en", width = NULL)
}
\arguments{
\item{inputId}{The \code{input} slot that will be used to access the value.}
@@ -43,9 +43,6 @@ Other valid values include "ar", "az", "bg", "bs", "ca", "cs", "cy", "da",
\item{width}{The width of the input, e.g. \code{'400px'}, or \code{'100\%'};
see \code{\link{validateCssUnit}}.}
\item{autoclose}{Whether or not to close the datepicker immediately when a
date is selected.}
}
\description{
Creates a text input which, when clicked on, brings up a calendar that
@@ -107,7 +104,5 @@ Other input elements: \code{\link{actionButton}},
\code{\link{numericInput}}, \code{\link{passwordInput}},
\code{\link{radioButtons}}, \code{\link{selectInput}},
\code{\link{sliderInput}}, \code{\link{submitButton}},
\code{\link{textAreaInput}}, \code{\link{textInput}},
\code{\link{varSelectInput}}
\code{\link{textAreaInput}}, \code{\link{textInput}}
}
\concept{input elements}

View File

@@ -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)
}
\arguments{
\item{inputId}{The \code{input} slot that will be used to access the value.}
@@ -50,9 +49,6 @@ Other valid values include "ar", "az", "bg", "bs", "ca", "cs", "cy", "da",
\item{width}{The width of the input, e.g. \code{'400px'}, or \code{'100\%'};
see \code{\link{validateCssUnit}}.}
\item{autoclose}{Whether or not to close the datepicker immediately when a
date is selected.}
}
\description{
Creates a pair of text inputs which, when clicked on, bring up calendars that
@@ -125,6 +121,5 @@ Other input elements: \code{\link{actionButton}},
\code{\link{passwordInput}}, \code{\link{radioButtons}},
\code{\link{selectInput}}, \code{\link{sliderInput}},
\code{\link{submitButton}}, \code{\link{textAreaInput}},
\code{\link{textInput}}, \code{\link{varSelectInput}}
\code{\link{textInput}}
}
\concept{input elements}

View File

@@ -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).}

View File

@@ -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}.
}
}
}

View File

@@ -5,6 +5,7 @@
\alias{getDefaultReactiveDomain}
\alias{withReactiveDomain}
\alias{onReactiveDomainEnded}
\alias{domains}
\title{Reactive domains}
\usage{
getDefaultReactiveDomain()

View File

@@ -3,6 +3,7 @@
\name{downloadButton}
\alias{downloadButton}
\alias{downloadLink}
\alias{downloadLink}
\title{Create a download button or link}
\usage{
downloadButton(outputId, label = "Download", class = NULL, ...)

View File

@@ -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

View File

@@ -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.}
@@ -98,6 +97,5 @@ Other input elements: \code{\link{actionButton}},
\code{\link{passwordInput}}, \code{\link{radioButtons}},
\code{\link{selectInput}}, \code{\link{sliderInput}},
\code{\link{submitButton}}, \code{\link{textAreaInput}},
\code{\link{textInput}}, \code{\link{varSelectInput}}
\code{\link{textInput}}
}
\concept{input elements}

View File

@@ -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.}

View File

@@ -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.
}

View File

@@ -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"},

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}}.
}

View File

@@ -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}.
}
}
}

View File

@@ -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)
}

View File

@@ -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}

View File

@@ -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.}

View File

@@ -53,6 +53,5 @@ Other input elements: \code{\link{actionButton}},
\code{\link{passwordInput}}, \code{\link{radioButtons}},
\code{\link{selectInput}}, \code{\link{sliderInput}},
\code{\link{submitButton}}, \code{\link{textAreaInput}},
\code{\link{textInput}}, \code{\link{varSelectInput}}
\code{\link{textInput}}
}
\concept{input elements}

View File

@@ -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

View File

@@ -7,16 +7,14 @@
\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,
ignoreInit = FALSE, cache = NULL)
event.quoted = FALSE, value.env = parent.frame(), value.quoted = FALSE,
label = NULL, domain = getDefaultReactiveDomain(), ignoreNULL = TRUE,
ignoreInit = FALSE)
}
\arguments{
\item{eventExpr}{A (quoted or unquoted) expression that represents the event;
@@ -84,14 +82,6 @@ this is the calling environment.}
\item{value.quoted}{Is the \code{valueExpr} expression quoted? By default,
this is \code{FALSE}. This is useful when you want to use an expression
that is stored in a variable; to do so, it must be quoted with \code{quote()}.}
\item{cache}{Extra caching to use for \code{eventReactive}. Note that the
most recent value is always cached, but this option allows you to cache
previous values based on the value of \code{eventExpr}. If \code{NULL} (the
default), do not use extra caching. Other possible values are \code{"app"}
for an application-level cache, \code{"session"} for a session-level cache,
or a cache object with \code{$get()} and \code{$set()} methods. See
\code{\link{renderCachedPlot}} for more information about using caching.}
}
\value{
\code{observeEvent} returns an observer reference class object (see
@@ -145,20 +135,15 @@ whereas \code{ignoreNULL=FALSE} is desirable if you want to initially perform
the action/calculation and just let the user re-initiate it (like a
"Recalculate" button).
Likewise, both \code{observeEvent} and \code{eventReactive} also take in an
\code{ignoreInit} argument. By default, both of these will run right when they
are created (except if, at that moment, \code{eventExpr} evaluates to \code{NULL}
Unlike what happens for \code{ignoreNULL}, only \code{observeEvent} takes in an
\code{ignoreInit} argument. By default, \code{observeEvent} will run right when
it is created (except if, at that moment, \code{eventExpr} evaluates to \code{NULL}
and \code{ignoreNULL} is \code{TRUE}). But when responding to a click of an action
button, it may often be useful to set \code{ignoreInit} to \code{TRUE}. For
example, if you're setting up an \code{observeEvent} for a dynamically created
button, then \code{ignoreInit = TRUE} will guarantee that the action (in
\code{handlerExpr}) will only be triggered when the button is actually clicked,
instead of also being triggered when it is created/initialized. Similarly,
if you're setting up an \code{eventReactive} that responds to a dynamically
created button used to refresh some data (then returned by that \code{eventReactive}),
then you should use \code{eventReactive([...], ignoreInit = TRUE)} if you want
to let the user decide if/when they want to refresh the data (since, depending
on the app, this may be a computationally expensive operation).
instead of also being triggered when it is created/initialized.
Even though \code{ignoreNULL} and \code{ignoreInit} can be used for similar
purposes they are independent from one another. Here's the result of combining
@@ -166,68 +151,29 @@ these:
\describe{
\item{\code{ignoreNULL = TRUE} and \code{ignoreInit = FALSE}}{
This is the default. This combination means that \code{handlerExpr}/
\code{valueExpr} will run every time that \code{eventExpr} is not
\code{NULL}. If, at the time of the creation of the
\code{observeEvent}/\code{eventReactive}, \code{eventExpr} happens
to \emph{not} be \code{NULL}, then the code runs.
This is the default. This combination means that \code{handlerExpr} will
run every time that \code{eventExpr} is not \code{NULL}. If, at the time
of the \code{observeEvent}'s creation, \code{handleExpr} happens to
\emph{not} be \code{NULL}, then the code runs.
}
\item{\code{ignoreNULL = FALSE} and \code{ignoreInit = FALSE}}{
This combination means that \code{handlerExpr}/\code{valueExpr} will
run every time no matter what.
This combination means that \code{handlerExpr} will run every time no
matter what.
}
\item{\code{ignoreNULL = FALSE} and \code{ignoreInit = TRUE}}{
This combination means that \code{handlerExpr}/\code{valueExpr} will
\emph{not} run when the \code{observeEvent}/\code{eventReactive} is
created (because \code{ignoreInit = TRUE}), but it will run every
other time.
This combination means that \code{handlerExpr} will \emph{not} run when
the \code{observeEvent} is created (because \code{ignoreInit = TRUE}),
but it will run every other time.
}
\item{\code{ignoreNULL = TRUE} and \code{ignoreInit = TRUE}}{
This combination means that \code{handlerExpr}/\code{valueExpr} will
\emph{not} run when the \code{observeEvent}/\code{eventReactive} is
created (because \code{ignoreInit = TRUE}). After that,
\code{handlerExpr}/\code{valueExpr} will run every time that
\code{eventExpr} is not \code{NULL}.
This combination means that \code{handlerExpr} will \emph{not} run when
the \code{observeEvent} is created (because \code{ignoreInit = TRUE}).
After that, \code{handlerExpr} will run every time that \code{eventExpr}
is not \code{NULL}.
}
}
}
\section{\code{eventReactive} caching}{
Like regular \code{\link{reactive}} expressions, the most recent value of a
\code{eventReactive} is always cached. (Observers are not cached because
they are used for their side-effects, not their values.) If a
\code{reactive} or \code{eventReactive} named \code{r} is called with
\code{r()} and then called again (without being invalidated in between),
then the second call will simply return the most recent value.
An \code{eventReactive} allows for caching of previous values, by using the
\code{cache} parameter. When this additional caching is used, a key-value
store is used, where the result of the \code{eventExpr} is used as the key.
More specifically, the result from the \code{eventExpr} is combined with
the \code{eventReactive}'s \code{label} (which defaults to a string
representation of the \code{expr} code), and they are serialized and hashed
to generate the key.
When an additional cache is used, it allow for sharing cached values with
other sessions. If you use \code{cache="session"}, then a separate cache
will be used for each user session. If you use \code{cache="app"}, then the
cache for the \code{eventReactive} will be shared across multiple client
sessions accessing the same Shiny application -- because the \code{label}
will (by default) be the same when the \code{expr} code is the same, an
\code{eventReactive} in one session can share values with the corresponding
\code{eventReactive} in another session. Whenever they have the same result
for \code{eventExpr}, the value can be drawn from the cache instead of
being recomputed.
Other types of caching are possible, by passing a cache object with
\code{$get()} and \code{$set()} methods. It is possible to cache the values
to disk, or in an external database, and have the cache persist across
application restarts. See \code{\link{renderCachedPlot}} for more
information about caching with Shiny.
}
\examples{
## Only run this example in interactive R sessions
if (interactive()) {

View File

@@ -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.

View File

@@ -55,6 +55,5 @@ Other input elements: \code{\link{actionButton}},
\code{\link{numericInput}}, \code{\link{radioButtons}},
\code{\link{selectInput}}, \code{\link{sliderInput}},
\code{\link{submitButton}}, \code{\link{textAreaInput}},
\code{\link{textInput}}, \code{\link{varSelectInput}}
\code{\link{textInput}}
}
\concept{input elements}

View File

@@ -3,17 +3,18 @@
\name{plotOutput}
\alias{plotOutput}
\alias{imageOutput}
\alias{plotOutput}
\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.}

View File

@@ -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.}
@@ -110,6 +109,5 @@ Other input elements: \code{\link{actionButton}},
\code{\link{numericInput}}, \code{\link{passwordInput}},
\code{\link{selectInput}}, \code{\link{sliderInput}},
\code{\link{submitButton}}, \code{\link{textAreaInput}},
\code{\link{textInput}}, \code{\link{varSelectInput}}
\code{\link{textInput}}
}
\concept{input elements}

View File

@@ -1,309 +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}.
}
\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}}.
}

View File

@@ -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.}

View File

@@ -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.}

View File

@@ -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}},
@@ -21,7 +20,8 @@ call to \code{\link{uiOutput}} when \code{renderUI} is used in an
interactive R Markdown document.}
}
\description{
Renders reactive HTML using the Shiny UI library.
\bold{Experimental feature.} Makes a reactive version of a function that
generates HTML using the Shiny UI library.
}
\details{
The corresponding HTML output tag should be \code{div} and have the CSS class
@@ -48,5 +48,5 @@ shinyApp(ui, server)
}
\seealso{
\code{\link{uiOutput}}
conditionalPanel
}

View File

@@ -98,9 +98,9 @@ shinyApp(
shinyApp(
ui = fluidPage(
selectInput("state", "Choose a state:",
list(`East Coast` = list("NY", "NJ", "CT"),
`West Coast` = list("WA", "OR", "CA"),
`Midwest` = list("MN", "WI", "IA"))
list(`East Coast` = c("NY", "NJ", "CT"),
`West Coast` = c("WA", "OR", "CA"),
`Midwest` = c("MN", "WI", "IA"))
),
textOutput("result")
),
@@ -113,7 +113,7 @@ shinyApp(
}
}
\seealso{
\code{\link{updateSelectInput}} \code{\link{varSelectInput}}
\code{\link{updateSelectInput}}
Other input elements: \code{\link{actionButton}},
\code{\link{checkboxGroupInput}},
@@ -122,6 +122,5 @@ Other input elements: \code{\link{actionButton}},
\code{\link{numericInput}}, \code{\link{passwordInput}},
\code{\link{radioButtons}}, \code{\link{sliderInput}},
\code{\link{submitButton}}, \code{\link{textAreaInput}},
\code{\link{textInput}}, \code{\link{varSelectInput}}
\code{\link{textInput}}
}
\concept{input elements}

View File

@@ -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())

View File

@@ -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}}.
}

View File

@@ -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)
@@ -126,6 +125,5 @@ Other input elements: \code{\link{actionButton}},
\code{\link{numericInput}}, \code{\link{passwordInput}},
\code{\link{radioButtons}}, \code{\link{selectInput}},
\code{\link{submitButton}}, \code{\link{textAreaInput}},
\code{\link{textInput}}, \code{\link{varSelectInput}}
\code{\link{textInput}}
}
\concept{input elements}

View File

@@ -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.}

View File

@@ -99,12 +99,10 @@ manipulating stack traces.
from \code{conditionStackTrace(cond)}) and returns a data frame with one
row for each stack frame and the columns \code{num} (stack frame number),
\code{call} (a function name or similar), and \code{loc} (source file path
and line number, if available). It was deprecated after shiny 1.0.5 because
it doesn't support deep stack traces.
and line number, if available).
\code{formatStackTrace} is similar to \code{extractStackTrace}, but
it returns a preformatted character vector instead of a data frame. It was
deprecated after shiny 1.0.5 because it doesn't support deep stack traces.
it returns a preformatted character vector instead of a data frame.
\code{conditionStackTrace} and \code{conditionStackTrace<-} are
accessor functions for getting/setting stack traces on conditions.

View File

@@ -72,6 +72,5 @@ Other input elements: \code{\link{actionButton}},
\code{\link{numericInput}}, \code{\link{passwordInput}},
\code{\link{radioButtons}}, \code{\link{selectInput}},
\code{\link{sliderInput}}, \code{\link{textAreaInput}},
\code{\link{textInput}}, \code{\link{varSelectInput}}
\code{\link{textInput}}
}
\concept{input elements}

View File

@@ -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}

View File

@@ -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.}
@@ -69,6 +68,5 @@ Other input elements: \code{\link{actionButton}},
\code{\link{numericInput}}, \code{\link{passwordInput}},
\code{\link{radioButtons}}, \code{\link{selectInput}},
\code{\link{sliderInput}}, \code{\link{submitButton}},
\code{\link{textInput}}, \code{\link{varSelectInput}}
\code{\link{textInput}}
}
\concept{input elements}

View File

@@ -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.}
@@ -51,6 +50,5 @@ Other input elements: \code{\link{actionButton}},
\code{\link{numericInput}}, \code{\link{passwordInput}},
\code{\link{radioButtons}}, \code{\link{selectInput}},
\code{\link{sliderInput}}, \code{\link{submitButton}},
\code{\link{textAreaInput}}, \code{\link{varSelectInput}}
\code{\link{textAreaInput}}
}
\concept{input elements}

View File

@@ -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}

View File

@@ -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

View File

@@ -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

View File

@@ -3,8 +3,6 @@
\name{updateSelectInput}
\alias{updateSelectInput}
\alias{updateSelectizeInput}
\alias{updateVarSelectInput}
\alias{updateVarSelectizeInput}
\title{Change the value of a select input on the client}
\usage{
updateSelectInput(session, inputId, label = NULL, choices = NULL,
@@ -12,12 +10,6 @@ updateSelectInput(session, inputId, label = NULL, choices = NULL,
updateSelectizeInput(session, inputId, label = NULL, choices = NULL,
selected = NULL, options = list(), server = FALSE)
updateVarSelectInput(session, inputId, label = NULL, data = NULL,
selected = NULL)
updateVarSelectizeInput(session, inputId, label = NULL, data = NULL,
selected = NULL, options = list(), server = FALSE)
}
\arguments{
\item{session}{The \code{session} object passed to function given to
@@ -48,8 +40,6 @@ for details).}
the select options dynamically on searching, instead of writing all
\code{choices} into the page at once (i.e., only use the client-side
version of \pkg{selectize.js})}
\item{data}{A data frame. Used to retrieve the column names as choices for a \code{\link{selectInput}}}
}
\description{
Change the value of a select input on the client
@@ -104,5 +94,5 @@ shinyApp(ui, server)
}
}
\seealso{
\code{\link{selectInput}} \code{\link{varSelectInput}}
\code{\link{selectInput}}
}

View File

@@ -2,11 +2,10 @@
% Please edit documentation in R/update-input.R
\name{updateSliderInput}
\alias{updateSliderInput}
\title{Update Slider Input Widget}
\title{Change the value of a slider input on the client}
\usage{
updateSliderInput(session, inputId, label = NULL, value = NULL,
min = NULL, max = NULL, step = NULL, timeFormat = NULL,
timezone = NULL)
min = NULL, max = NULL, step = NULL)
}
\arguments{
\item{session}{The \code{session} object passed to function given to
@@ -23,13 +22,9 @@ updateSliderInput(session, inputId, label = NULL, value = NULL,
\item{max}{Maximum value.}
\item{step}{Step size.}
\item{timeFormat}{Date and POSIXt formatting.}
\item{timezone}{The timezone offset for POSIXt objects.}
}
\description{
Change the value of a slider input on the client.
Change the value of a slider input on the client
}
\details{
The input updater functions send a message to the client, telling it to

View File

@@ -1,129 +0,0 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/input-select.R
\name{varSelectInput}
\alias{varSelectInput}
\alias{varSelectizeInput}
\title{Select variables from a data frame}
\usage{
varSelectInput(inputId, label, data, selected = NULL, multiple = FALSE,
selectize = TRUE, width = NULL, size = NULL)
varSelectizeInput(inputId, ..., options = NULL, width = NULL)
}
\arguments{
\item{inputId}{The \code{input} slot that will be used to access the value.}
\item{label}{Display label for the control, or \code{NULL} for no label.}
\item{data}{A data frame. Used to retrieve the column names as choices for a \code{\link{selectInput}}}
\item{selected}{The initially selected value (or multiple values if
\code{multiple = TRUE}). If not specified then defaults to the first value
for single-select lists and no values for multiple select lists.}
\item{multiple}{Is selection of multiple items allowed?}
\item{selectize}{Whether to use \pkg{selectize.js} or not.}
\item{width}{The width of the input, e.g. \code{'400px'}, or \code{'100\%'};
see \code{\link{validateCssUnit}}.}
\item{size}{Number of items to show in the selection box; a larger number
will result in a taller box. Not compatible with \code{selectize=TRUE}.
Normally, when \code{multiple=FALSE}, a select input will be a drop-down
list, but when \code{size} is set, it will be a box instead.}
\item{...}{Arguments passed to \code{varSelectInput()}.}
\item{options}{A list of options. See the documentation of \pkg{selectize.js}
for possible options (character option values inside \code{\link[base]{I}()} will
be treated as literal JavaScript code; see \code{\link{renderDataTable}()}
for details).}
}
\value{
A variable select list control that can be added to a UI definition.
}
\description{
Create a select list that can be used to choose a single or multiple items
from the column names of a data frame.
}
\details{
The resulting server \code{input} value will be returned as:
\itemize{
\item a symbol if \code{multiple = FALSE}. The \code{input} value should be
used with rlang's \code{\link[rlang]{!!}}. For example,
\code{ggplot2::aes(!!input$variable)}.
\item a list of symbols if \code{multiple = TRUE}. The \code{input} value
should be used with rlang's \code{\link[rlang]{!!!}} to expand
the symbol list as individual arguments. For example,
\code{dplyr::select(mtcars, !!!input$variabls)} which is
equivalent to \code{dplyr::select(mtcars, !!input$variabls[[1]], !!input$variabls[[2]], ..., !!input$variabls[[length(input$variabls)]])}.
}
By default, \code{varSelectInput()} and \code{selectizeInput()} use the
JavaScript library \pkg{selectize.js}
(\url{https://github.com/selectize/selectize.js}) to instead of the basic
select input element. To use the standard HTML select input element, use
\code{selectInput()} with \code{selectize=FALSE}.
}
\note{
The variable selectize input created from \code{varSelectizeInput()} allows
deletion of the selected option even in a single select input, which will
return an empty string as its value. This is the default behavior of
\pkg{selectize.js}. However, the selectize input created from
\code{selectInput(..., selectize = TRUE)} will ignore the empty string
value when it is a single choice input and the empty string is not in the
\code{choices} argument. This is to keep compatibility with
\code{selectInput(..., selectize = FALSE)}.
}
\examples{
## Only run examples in interactive R sessions
if (interactive()) {
library(ggplot2)
# single selection
shinyApp(
ui = fluidPage(
varSelectInput("variable", "Variable:", mtcars),
plotOutput("data")
),
server = function(input, output) {
output$data <- renderPlot({
ggplot(mtcars, aes(!!input$variable)) + geom_histogram()
})
}
)
# multiple selections
\dontrun{
shinyApp(
ui = fluidPage(
varSelectInput("variables", "Variable:", mtcars, multiple = TRUE),
tableOutput("data")
),
server = function(input, output) {
output$data <- renderTable({
if (length(input$variables) == 0) return(mtcars)
mtcars \%>\% dplyr::select(!!!input$variables)
}, rownames = TRUE)
}
)}
}
}
\seealso{
\code{\link{updateSelectInput}}
Other input elements: \code{\link{actionButton}},
\code{\link{checkboxGroupInput}},
\code{\link{checkboxInput}}, \code{\link{dateInput}},
\code{\link{dateRangeInput}}, \code{\link{fileInput}},
\code{\link{numericInput}}, \code{\link{passwordInput}},
\code{\link{radioButtons}}, \code{\link{selectInput}},
\code{\link{sliderInput}}, \code{\link{submitButton}},
\code{\link{textAreaInput}}, \code{\link{textInput}}
}
\concept{input elements}

View File

@@ -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())

View File

@@ -101,7 +101,7 @@ function initShiny() {
inputs = new InputValidateDecorator(inputs);
exports.setInputValue = exports.onInputChange = function(name, value, opts) {
exports.onInputChange = function(name, value, opts) {
opts = addDefaultInputOpts(opts);
inputs.setInput(name, value, opts);
};
@@ -116,11 +116,7 @@ function initShiny() {
if (type)
id = id + ":" + type;
let opts = {
priority: allowDeferred ? "deferred" : "immediate",
binding: binding,
el: el
};
let opts = { immediate: !allowDeferred, binding: binding, el: el };
inputs.setInput(id, value, opts);
}
}
@@ -281,7 +277,7 @@ function initShiny() {
// The server needs to know the size of each image and plot output element,
// in case it is auto-sizing
$('.shiny-image-output, .shiny-plot-output, .shiny-report-size').each(function() {
$('.shiny-image-output, .shiny-plot-output').each(function() {
var id = getIdFromEl(this);
if (this.offsetWidth !== 0 || this.offsetHeight !== 0) {
initialValues['.clientdata_output_' + id + '_width'] = this.offsetWidth;
@@ -289,7 +285,7 @@ function initShiny() {
}
});
function doSendImageSize() {
$('.shiny-image-output, .shiny-plot-output, .shiny-report-size').each(function() {
$('.shiny-image-output, .shiny-plot-output').each(function() {
var id = getIdFromEl(this);
if (this.offsetWidth !== 0 || this.offsetHeight !== 0) {
inputs.setInput('.clientdata_output_' + id + '_width', this.offsetWidth);

View File

@@ -323,7 +323,7 @@ $.extend(fileInputBinding, {
// Attach a dragenter handler to $el and all of its children. When the first
// child is entered, trigger a draghoverstart event.
$el.on("dragenter.dragHover", e => {
if (collection.length === 0) {
if (collection.size() === 0) {
$el.trigger("draghoverstart" + ns, e.originalEvent);
}
// Every child that has fired dragenter is added to the collection.
@@ -338,7 +338,7 @@ $.extend(fileInputBinding, {
collection = collection.not(e.originalEvent.target);
// When the collection has no elements, all of the children have been
// removed, and produce draghoverend event.
if (collection.length === 0) {
if (collection.size() === 0) {
$el.trigger("draghoverend" + ns, e.originalEvent);
}
});

View File

@@ -3,18 +3,6 @@ $.extend(selectInputBinding, {
find: function(scope) {
return $(scope).find('select');
},
getType: function(el) {
var $el = $(el);
if (!$el.hasClass("symbol")) {
// default character type
return null;
}
if ($el.attr("multiple") === "multiple") {
return 'shiny.symbolList';
} else {
return 'shiny.symbol';
}
},
getId: function(el) {
return InputBinding.prototype.getId.call(this, el) || el.name;
},
@@ -67,7 +55,7 @@ $.extend(selectInputBinding, {
if (data.hasOwnProperty('url')) {
selectize = this._selectize(el);
selectize.clearOptions();
var loaded = false;
var thiz = this, loaded = false;
selectize.settings.load = function(query, callback) {
var settings = selectize.settings;
$.ajax({
@@ -84,19 +72,9 @@ $.extend(selectInputBinding, {
callback();
},
success: function(res) {
// res = [{label: '1', value: '1', group: '1'}, ...]
// success is called after options are added, but
// groups need to be added manually below
$.each(res, function(index, elem) {
selectize.addOptionGroup(elem.group, { group: elem.group });
});
callback(res);
if (!loaded && data.hasOwnProperty('value')) {
selectize.setValue(data.value);
} else if (settings.maxItems === 1) {
// first item selected by default only for single-select
selectize.setValue(res[0].value);
}
if (!loaded && data.hasOwnProperty('value'))
thiz.setValue(el, data.value);
loaded = true;
}
});
@@ -133,10 +111,7 @@ $.extend(selectInputBinding, {
var options = $.extend({
labelField: 'label',
valueField: 'value',
searchField: ['label'],
optgroupField: 'group',
optgroupLabelField: 'group',
optgroupValueField: 'group'
searchField: ['label']
}, JSON.parse(config.html()));
// selectize created from selectInput()
if (typeof(config.data('nonempty')) !== 'undefined') {

View File

@@ -6,38 +6,6 @@ function forceIonSliderUpdate(slider) {
console.log("Couldn't force ion slider to update");
}
function getTypePrettifyer(dataType, timeFormat, timezone) {
var timeFormatter;
var prettify;
if (dataType === 'date') {
timeFormatter = strftime.utc();
prettify = function(num) {
return timeFormatter(timeFormat, new Date(num));
};
} else if (dataType === 'datetime') {
if (timezone)
timeFormatter = strftime.timezone(timezone);
else
timeFormatter = strftime;
prettify = function(num) {
return timeFormatter(timeFormat, new Date(num));
};
} else {
// The default prettify function for ion.rangeSlider adds thousands
// separators after the decimal mark, so we have our own version here.
// (#1958)
prettify = function(num) {
// When executed, `this` will refer to the `IonRangeSlider.options`
// object.
return formatNumber(num, this.prettify_separator);
};
}
return prettify;
}
var sliderInputBinding = {};
$.extend(sliderInputBinding, textInputBinding, {
find: function(scope) {
@@ -122,31 +90,13 @@ $.extend(sliderInputBinding, textInputBinding, {
msg.from = data.value;
}
}
var sliderFeatures = ['min', 'max', 'step'];
for (var i = 0; i < sliderFeatures.length; i++) {
var feats = sliderFeatures[i];
if (data.hasOwnProperty(feats)) {
msg[feats] = data[feats];
}
}
if (data.hasOwnProperty('min')) msg.min = data.min;
if (data.hasOwnProperty('max')) msg.max = data.max;
if (data.hasOwnProperty('step')) msg.step = data.step;
if (data.hasOwnProperty('label'))
$el.parent().find('label[for="' + $escape(el.id) + '"]').text(data.label);
var domElements = ['data-type', 'time-format', 'timezone'];
for (var i = 0; i < domElements.length; i++) {
var elem = domElements[i];
if (data.hasOwnProperty(elem)) {
$el.data(elem, data[elem]);
}
}
var dataType = $el.data('data-type');
var timeFormat = $el.data('time-format');
var timezone = $el.data('timezone');
msg.prettify = getTypePrettifyer(dataType, timeFormat, timezone);
$el.data('immediate', true);
try {
slider.update(msg);
@@ -168,9 +118,36 @@ $.extend(sliderInputBinding, textInputBinding, {
var $el = $(el);
var dataType = $el.data('data-type');
var timeFormat = $el.data('time-format');
var timezone = $el.data('timezone');
var timeFormatter;
opts.prettify = getTypePrettifyer(dataType, timeFormat, timezone);
// Set up formatting functions
if (dataType === 'date') {
timeFormatter = strftime.utc();
opts.prettify = function(num) {
return timeFormatter(timeFormat, new Date(num));
};
} else if (dataType === 'datetime') {
var timezone = $el.data('timezone');
if (timezone)
timeFormatter = strftime.timezone(timezone);
else
timeFormatter = strftime;
opts.prettify = function(num) {
return timeFormatter(timeFormat, new Date(num));
};
} else {
// The default prettify function for ion.rangeSlider adds thousands
// separators after the decimal mark, so we have our own version here.
// (#1958)
opts.prettify = function(num) {
// When executed, `this` will refer to the `IonRangeSlider.options`
// object.
return formatNumber(num, this.prettify_separator);
};
}
$el.ionRangeSlider(opts);
},

View File

@@ -189,34 +189,26 @@ var InputBatchSender = function(shinyapp) {
this.lastChanceCallback = [];
};
(function() {
this.setInput = function(name, value, opts) {
this.setInput = function(name, value) {
var self = this;
this.pendingData[name] = value;
if (!this.reentrant) {
if (opts.priority === "event") {
this.$sendNow();
} else if (!this.timerId) {
this.timerId = setTimeout(this.$sendNow.bind(this), 0);
}
}
};
this.$sendNow = function() {
if (this.reentrant) {
console.trace("Unexpected reentrancy in InputBatchSender!");
}
this.reentrant = true;
try {
this.timerId = null;
$.each(this.lastChanceCallback, (i, callback) => {
callback();
});
var currentData = this.pendingData;
this.pendingData = {};
this.shinyapp.sendInput(currentData);
} finally {
this.reentrant = false;
if (!this.timerId && !this.reentrant) {
this.timerId = setTimeout(function() {
self.reentrant = true;
try {
$.each(self.lastChanceCallback, function(i, callback) {
callback();
});
self.timerId = null;
var currentData = self.pendingData;
self.pendingData = {};
self.shinyapp.sendInput(currentData);
} finally {
self.reentrant = false;
}
}, 0);
}
};
}).call(InputBatchSender.prototype);
@@ -227,18 +219,21 @@ var InputNoResendDecorator = function(target, initialValues) {
this.lastSentValues = this.reset(initialValues);
};
(function() {
this.setInput = function(name, value, opts) {
this.setInput = function(name, value) {
// Note that opts is not passed to setInput at this stage of the input
// decorator stack. If in the future this setInput keeps track of opts, it
// would be best not to store the `el`, because that could prevent it from
// being GC'd.
const { name: inputName, inputType: inputType } = splitInputNameType(name);
const jsonValue = JSON.stringify(value);
if (opts.priority !== "event" &&
this.lastSentValues[inputName] &&
if (this.lastSentValues[inputName] &&
this.lastSentValues[inputName].jsonValue === jsonValue &&
this.lastSentValues[inputName].inputType === inputType) {
return;
}
this.lastSentValues[inputName] = { jsonValue, inputType };
this.target.setInput(name, value, opts);
this.target.setInput(name, value);
};
this.reset = function(values = {}) {
// Given an object with flat name-value format:
@@ -276,7 +271,6 @@ var InputEventDecorator = function(target) {
evt.value = value;
evt.binding = opts.binding;
evt.el = opts.el;
evt.priority = opts.priority;
$(document).trigger(evt);
@@ -284,9 +278,9 @@ var InputEventDecorator = function(target) {
name = evt.name;
if (evt.inputType !== '') name += ':' + evt.inputType;
// Most opts aren't passed along to lower levels in the input decorator
// opts aren't passed along to lower levels in the input decorator
// stack.
this.target.setInput(name, evt.value, { priority: opts.priority });
this.target.setInput(name, evt.value);
}
};
}).call(InputEventDecorator.prototype);
@@ -300,7 +294,7 @@ var InputRateDecorator = function(target) {
this.setInput = function(name, value, opts) {
this.$ensureInit(name);
if (opts.priority !== "deferred")
if (opts.immediate)
this.inputRatePolicies[name].immediateCall(name, value, opts);
else
this.inputRatePolicies[name].normalCall(name, value, opts);
@@ -365,25 +359,11 @@ const InputValidateDecorator = function(target) {
// Merge opts with defaults, and return a new object.
function addDefaultInputOpts(opts) {
opts = $.extend({
priority: "immediate",
return $.extend({
immediate: false,
binding: null,
el: null
}, opts);
if (opts && typeof(opts.priority) !== "undefined") {
switch (opts.priority) {
case "deferred":
case "immediate":
case "event":
break;
default:
throw new Error("Unexpected input value mode: '" + opts.priority + "'");
}
}
return opts;
}

View File

@@ -436,7 +436,7 @@ imageutils.initCoordmap = function($el, coordmap) {
return function(e) {
if (e === null) {
exports.setInputValue(inputId, null);
exports.onInputChange(inputId, null);
return;
}
@@ -444,7 +444,7 @@ imageutils.initCoordmap = function($el, coordmap) {
// If outside of plotting region
if (!coordmap.isInPanel(offset)) {
if (nullOutside) {
exports.setInputValue(inputId, null);
exports.onInputChange(inputId, null);
return;
}
if (clip)
@@ -466,7 +466,8 @@ imageutils.initCoordmap = function($el, coordmap) {
coords.range = panel.range;
coords.log = panel.log;
exports.setInputValue(inputId, coords, {priority: "event"});
coords[".nonce"] = Math.random();
exports.onInputChange(inputId, coords);
};
};
};
@@ -661,7 +662,7 @@ imageutils.createBrushHandler = function(inputId, $el, opts, coordmap, outputId)
// We're in a new or reset state
if (isNaN(coords.xmin)) {
exports.setInputValue(inputId, null);
exports.onInputChange(inputId, null);
// Must tell other brushes to clear.
imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
brushId: inputId, outputId: null
@@ -688,7 +689,7 @@ imageutils.createBrushHandler = function(inputId, $el, opts, coordmap, outputId)
coords.outputId = outputId;
// Send data to server
exports.setInputValue(inputId, coords);
exports.onInputChange(inputId, coords);
$el.data("mostRecentBrush", true);
imageOutputBinding.find(document).trigger("shiny-internal:brushed", coords);
@@ -1372,7 +1373,7 @@ imageutils.createBrush = function($el, opts, coordmap, expandPixels) {
};
exports.resetBrush = function(brushId) {
exports.setInputValue(brushId, null);
exports.onInputChange(brushId, null);
imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
brushId: brushId, outputId: null
});

View File

@@ -331,22 +331,18 @@ var ShinyApp = function() {
};
this.receiveOutput = function(name, value) {
if (this.$values[name] === value)
return undefined;
this.$values[name] = value;
delete this.$errors[name];
var binding = this.$bindings[name];
var evt = jQuery.Event('shiny:value');
evt.name = name;
evt.value = value;
evt.binding = binding;
if (this.$values[name] === value) {
$(binding ? binding.el : document).trigger(evt);
return undefined;
}
this.$values[name] = value;
delete this.$errors[name];
$(binding ? binding.el : document).trigger(evt);
if (!evt.isDefaultPrevented() && binding) {
binding.onValueChange(evt.value);
}

View File

@@ -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))
})

View File

@@ -1,10 +1,7 @@
context("Parse Shiny Input")
test_that("A new type can be registered successfully", {
expect_error(
registerInputHandler("shiny.someType", function(){}),
NA
)
registerInputHandler("shiny.someType", function(){})
})
test_that("A duplicated type throws", {
@@ -48,34 +45,3 @@ test_that("Nulls are not converted to NAs in parsing", {
list(method="init", data=list(obs=500L, nullObs=NULL))
)
})
test_that("characters turn into symbols", {
handler <- inputHandlers$get("shiny.symbol")
x <- "mpg"
expect_identical(
handler(x),
as.symbol(x)
)
expect_identical(
handler(NULL),
NULL
)
})
test_that("character vectors turn into symbol lists", {
handler <- inputHandlers$get("shiny.symbolList")
x <- list("mpg")
expect_identical(
handler(x),
list(as.symbol(x[[1]]))
)
x <- list("mpg", "cyl", "disp")
expect_identical(
handler(x),
list(as.symbol(x[[1]]), as.symbol(x[[2]]), as.symbol(x[[3]]))
)
expect_identical(
handler(NULL),
list()
)
})

View File

@@ -1,13 +0,0 @@
context("Validate Compiled shiny.js File")
test_that("{{ VERSION }} was replaced", {
jsFiles <- system.file(
file.path("www", "shared", c("shiny.js", "shiny.min.js")),
package = "shiny"
)
lapply(jsFiles, function(jsFile) {
jsFileContent <- paste(suppressWarnings(readLines(jsFile)), collapse = "\n")
expect_false(grepl("\\{\\{\\sVERSION\\s\\}\\}", jsFileContent))
})
})

View File

@@ -56,14 +56,3 @@ test_that("reactiveValues with namespace", {
expect_equivalent(isolate(names(rv1)), c("baz", "qux-quux"))
expect_equivalent(isolate(names(rv2)), c("quux"))
})
test_that("implicit output respects module namespace", {
output <- new.env(parent = emptyenv())
ns <- NS("test")
result <- withReactiveDomain(list(output = output, ns = ns),
as.tags(renderText("hi"))
)
# Does the automatically-generated output id include the correct namespace qualifier?
# (See issue #2000)
expect_equivalent(result$attribs$id, ns(ls(output)))
})

View File

@@ -779,8 +779,8 @@ test_that("classes of reactive object", {
})
test_that("{} and NULL also work in reactive()", {
expect_error(reactive({}), NA)
expect_error(reactive(NULL), NA)
reactive({})
reactive(NULL)
})
test_that("shiny.suppressMissingContextError option works", {
@@ -1127,20 +1127,3 @@ test_that("debounce/throttle work properly (with priming)", {
test_that("debounce/throttle work properly (without priming)", {
run_debounce_throttle(FALSE)
})
test_that("reactive domain works across async handlers", {
obj <- new.env()
hasReactiveDomain <- NULL
withReactiveDomain(obj, {
promises::then(
promises::promise_resolve(TRUE),
~{hasReactiveDomain <<- identical(getDefaultReactiveDomain(), obj)}
)
})
while (is.null(hasReactiveDomain) && !later::loop_empty()) {
later::run_now()
}
testthat::expect_true(hasReactiveDomain)
})

View File

@@ -1,47 +0,0 @@
context("deepstacks")
describe("deep stack trace filtering", {
it("passes smoke test", {
st <- list(
c(
common <- c("1", "2", "..stacktraceoff..", "3", "..stacktracefloor.."),
"4", "..stacktraceon..", "5"
),
c(common, "6", "..stacktraceoff..", "7"),
c(common, "8", "..stacktraceon.."),
c(common, "9")
)
expect_equal(
stripStackTraces(values = TRUE, st),
jsonlite::fromJSON('[["1", "2", "5"],["6"],[],["9"]]')
)
})
it("handles null cases", {
expect_equal(
stripStackTraces(values = TRUE, list(c())),
list(character(0))
)
})
it("handles various edge cases", {
expect_equal(
stripStackTraces(values = TRUE, list(
c("..stacktraceoff..", "..stacktraceoff..")
)),
list(character(0))
)
expect_equal(
stripStackTraces(values = TRUE, list(
c("..stacktraceoff..", "..stacktraceoff.."),
c(),
c("..stacktraceon.."),
c("..stacktraceon.."),
c("1")
)),
list(character(0), character(0), character(0), character(0), "1")
)
})
})

View File

@@ -1,32 +0,0 @@
context("stack pruning")
capture <- function() {
list(
calls = sys.calls(),
parents = sys.parents()
)
}
capture_1 <- function() {
capture()
}
capture_2 <- function() {
capture_1()
}
res <- do.call(
identity,
list(
identity(capture_2())
)
)
res$calls <- tail(res$calls, 5)
res$parents <- tail(res$parents - (length(res$parents) - 5), 5)
describe("stack pruning", {
it("passes basic example", {
expect_equal(pruneStackTrace(res$parents), c(F, F, T, T, T))
expect_equal(lapply(list(res$parents), pruneStackTrace), list(c(F, F, T, T, T)))
})
})

View File

@@ -51,48 +51,44 @@ test_that("integration tests", {
df <- causeError(full = FALSE)
# dumpTests(df)
expect_equal(df$num, c(56L, 55L, 54L, 38L, 37L, 36L, 35L,
34L, 33L, 32L, 31L, 30L))
expect_equal(df$call, c("A", "B", "<reactive:C>", "C", "renderTable",
"func", "force", "withVisible", "withCallingHandlers", "globals$domain$wrapSync",
expect_equal(df$num, c(50L, 49L, 48L, 35L, 34L, 33L, 32L,
31L, 30L, 29L, 28L, 27L))
expect_equal(df$call, c("A", "B", "<reactive:C>", "C", "renderTable",
"func", "force", "withVisible", "withCallingHandlers", "globals$domain$wrapSync",
"promises::with_promise_domain", "captureStackTraces"))
expect_equal(nzchar(df$loc), c(TRUE, TRUE, TRUE, FALSE, TRUE,
expect_equal(nzchar(df$loc), c(TRUE, TRUE, TRUE, FALSE, TRUE,
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE))
df <- causeError(full = TRUE)
# dumpTests(df)
expect_equal(df$num, c(59L, 58L, 57L, 56L, 55L, 54L, 53L,
52L, 51L, 50L, 49L, 48L, 47L, 46L, 45L, 44L, 43L, 42L, 41L,
40L, 39L, 38L, 37L, 36L, 35L, 34L, 33L, 32L, 31L, 30L, 29L,
28L, 27L, 26L, 25L, 24L, 23L, 22L, 21L, 20L, 19L, 18L, 17L,
16L, 15L, 14L, 13L, 12L, 11L, 10L, 9L, 8L, 7L, 6L, 5L, 4L,
3L, 2L, 1L))
expect_equal(df$call, c("h", ".handleSimpleError", "stop",
"A", "B", "<reactive:C>", "..stacktraceon..", ".func", "withVisible",
"withCallingHandlers", "contextFunc", "env$runWith", "force",
"globals$domain$wrapSync", "promises::with_promise_domain",
"withReactiveDomain", "globals$domain$wrapSync", "promises::with_promise_domain",
"ctx$run", "self$.updateValue", "..stacktraceoff..", "C",
"renderTable", "func", "force", "withVisible", "withCallingHandlers",
"globals$domain$wrapSync", "promises::with_promise_domain",
"captureStackTraces", "doTryCatch", "tryCatchOne", "tryCatchList",
"tryCatch", "do", "hybrid_chain", "origRenderFunc", "renderTable({ C() }, server = FALSE)",
"..stacktraceon..", "contextFunc", "env$runWith", "force",
"globals$domain$wrapSync", "promises::with_promise_domain",
"withReactiveDomain", "globals$domain$wrapSync", "promises::with_promise_domain",
"ctx$run", "..stacktraceoff..", "isolate", "withCallingHandlers",
"globals$domain$wrapSync", "promises::with_promise_domain",
"captureStackTraces", "doTryCatch", "tryCatchOne", "tryCatchList",
expect_equal(df$num, c(53L, 52L, 51L, 50L, 49L, 48L, 47L,
46L, 45L, 44L, 43L, 42L, 41L, 40L, 39L, 38L, 37L, 36L, 35L,
34L, 33L, 32L, 31L, 30L, 29L, 28L, 27L, 26L, 25L, 24L, 23L,
22L, 21L, 20L, 19L, 18L, 17L, 16L, 15L, 14L, 13L, 12L, 11L,
10L, 9L, 8L, 7L, 6L, 5L, 4L, 3L, 2L, 1L))
expect_equal(df$call, c("h", ".handleSimpleError", "stop",
"A", "B", "<reactive:C>", "..stacktraceon..", ".func", "withVisible",
"withCallingHandlers", "contextFunc", "env$runWith", "withReactiveDomain",
"globals$domain$wrapSync", "promises::with_promise_domain",
"ctx$run", "self$.updateValue", "..stacktraceoff..", "C",
"renderTable", "func", "force", "withVisible", "withCallingHandlers",
"globals$domain$wrapSync", "promises::with_promise_domain",
"captureStackTraces", "doTryCatch", "tryCatchOne", "tryCatchList",
"tryCatch", "do", "hybrid_chain", "origRenderFunc", "renderTable({ C() }, server = FALSE)",
"..stacktraceon..", "contextFunc", "env$runWith", "withReactiveDomain",
"globals$domain$wrapSync", "promises::with_promise_domain",
"ctx$run", "..stacktraceoff..", "isolate", "withCallingHandlers",
"globals$domain$wrapSync", "promises::with_promise_domain",
"captureStackTraces", "doTryCatch", "tryCatchOne", "tryCatchList",
"tryCatch", "try"))
expect_equal(nzchar(df$loc), c(FALSE, FALSE, FALSE, TRUE,
TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, TRUE, FALSE,
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
FALSE, TRUE, FALSE, FALSE, FALSE, TRUE, FALSE, FALSE, FALSE,
FALSE))
expect_equal(nzchar(df$loc), c(FALSE, FALSE, FALSE, TRUE,
TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, FALSE,
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, FALSE, FALSE, FALSE,
FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, FALSE, FALSE, TRUE,
FALSE, FALSE, FALSE, FALSE))
})
test_that("shiny.error", {

View File

@@ -23,26 +23,3 @@ test_that("Scheduling works", {
expect_false(timerCallbacks$executeElapsed())
expect_equal(0, nrow(timerCallbacks$takeElapsed()))
})
test_that("Unscheduling works", {
origTimes <- timerCallbacks$.times
origFuncKeys <- timerCallbacks$.funcs$keys()
taskHandle <- scheduleTask(1000, function() {
message("Whatever")
})
# Unregister
taskHandle()
expect_identical(timerCallbacks$.times, origTimes)
expect_identical(timerCallbacks$.funcs$keys(), origFuncKeys)
})
test_that("Vectorized unscheduling works", {
key1 <- timerCallbacks$schedule(1000, function() {})
key2 <- timerCallbacks$schedule(1000, function() {})
key3 <- timerCallbacks$schedule(1000, function() {})
expect_identical(timerCallbacks$unschedule(key2), TRUE)
expect_identical(timerCallbacks$unschedule(c(key1, key2, key3)), c(TRUE, FALSE, TRUE))
})

View File

@@ -21,42 +21,3 @@ test_that("sliderInput steps don't have rounding errors", {
# Need to use expect_identical; expect_equal is too forgiving of rounding error
expect_identical(findStepSize(-5.5, 4, NULL), 0.1)
})
test_that("selectInputUI has a select at an expected location", {
for (multiple in c(TRUE, FALSE)) {
for (selected in list(NULL, "", "A")) {
for (selectize in c(TRUE, FALSE)) {
selectInputVal <- selectInput(
inputId = "testId",
label = "test label",
choices = c("A", "B", "C"),
selected = selected,
multiple = multiple,
selectize = selectize
)
# if this getter is changed, varSelectInput getter needs to be changed
selectHtml <- selectInputVal$children[[2]]$children[[1]]
expect_true(inherits(selectHtml, "shiny.tag"))
expect_equal(selectHtml$name, "select")
if (!is.null(selectHtml$attribs$class)) {
expect_false(grepl(selectHtml$attribs$class, "symbol"))
}
varSelectInputVal <- varSelectInput(
inputId = "testId",
label = "test label",
data = data.frame(A = 1:2, B = 3:4, C = 5:6),
selected = selected,
multiple = multiple,
selectize = selectize
)
# if this getter is changed, varSelectInput getter needs to be changed
varSelectHtml <- varSelectInputVal$children[[2]]$children[[1]]
expect_true(inherits(varSelectHtml, "shiny.tag"))
expect_equal(varSelectHtml$name, "select")
expect_true(grepl("symbol", varSelectHtml$attribs$class, fixed = TRUE))
}
}
}
})

Some files were not shown because too many files have changed in this diff Show More